Files
dashback/mcp.md
2026-10-14 00:08:11 +02:00

405 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Model Context Protocol (MCP) in Dashback
This document explains the architecture, database schema, OAuth 2.1 authentication flow, and endpoint implementation of the Model Context Protocol (MCP) server in Dashback.
---
## 1. Overview & Purpose
The Dashback backend provides an MCP server running over **Streamable HTTP** at `/mcp`. It exposes internal tools (`dashback_request`, `dash-subagent`, and `search-articles`) to external MCP clients such as:
- Visual Studio Code / GitHub Copilot
- Claude Desktop
- Cursor / Windsurf
All operations executed via these tools are cryptographically authenticated and scoped to the authorizing Dashback user (`google_auth_id`) and their selected company.
---
## 2. Authentication: OAuth 2.1 Specification Compliance
MCP remote HTTP transports require standardized authorization following [OAuth 2.1](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-13). Dashback implements a full, built-in OAuth 2.1 authorization server supporting:
| Standard | Purpose | Endpoint(s) |
|---|---|---|
| **RFC 9728** | Protected Resource Metadata (PRM) | `/.well-known/oauth-protected-resource`<br>`/.well-known/oauth-protected-resource/mcp` |
| **RFC 8414** / OIDC | Authorization Server Metadata | `/.well-known/oauth-authorization-server`<br>`/.well-known/openid-configuration` |
| **RFC 7591** | Dynamic Client Registration (DCR) | `POST /oauth/register` |
| **RFC 6749** & **RFC 7636** | Authorization Code Flow with PKCE (`S256`) | `GET /oauth/authorize`<br>`POST /oauth/authorize` |
| **RFC 6749** | Token Exchange & Refresh | `POST /oauth/token` |
| **RFC 7662** | Token Introspection | `POST /oauth/introspect` |
| **RFC 8252** §7.3 | Native App Loopback Interface Redirection | Port-agnostic redirect URI matching for `127.0.0.1` and `localhost` |
---
## 3. Database Schema (`dashdbschema`)
Three new tables were added to both **MSSQL** (`src/providers/mssql/schema.js`) and **SQLite** (`src/providers/sqlite3/schema.js`):
### 3.1. `mcp_client`
Stores dynamically registered MCP clients (e.g., VS Code instances).
- `id` (INTEGER / INT IDENTITY, PK)
- `client_id` (NVARCHAR(100) / TEXT, UNIQUE NOT NULL)
- `client_secret` (NVARCHAR(255) / TEXT, NULL)
- `client_name` (NVARCHAR(255) / TEXT, NOT NULL)
- `redirect_uris` (NVARCHAR(MAX) / TEXT, NOT NULL, JSON array or URI list)
- `grant_types` (NVARCHAR(255) / TEXT, NULL, e.g. `'authorization_code,refresh_token'`)
- `response_types` (NVARCHAR(255) / TEXT, NULL, e.g. `'code'`)
- `scope` (NVARCHAR(255) / TEXT, NULL, default `'mcp:tools'`)
- `created_at` (DATETIME2 / TEXT)
### 3.2. `mcp_auth_code`
Stores short-lived authorization codes (5-minute TTL) during the PKCE handshake.
- `code` (NVARCHAR(128) / TEXT, PK)
- `client_id` (NVARCHAR(100) / TEXT, NOT NULL)
- `google_auth_id` (INT / INTEGER, FK `google_auth(id) ON DELETE CASCADE`)
- `redirect_uri` (NVARCHAR(2048) / TEXT, NOT NULL)
- `code_challenge` (NVARCHAR(255) / TEXT, NOT NULL)
- `code_challenge_method` (NVARCHAR(32) / TEXT, NOT NULL, e.g. `'S256'`)
- `scope` (NVARCHAR(255) / TEXT, NULL)
- `expires_at` (DATETIME2 / TEXT, NOT NULL)
- `used` (TINYINT / INTEGER, NOT NULL DEFAULT 0)
- `created_at` (DATETIME2 / TEXT)
### 3.3. `mcp_token`
Stores refresh tokens (30-day TTL) for long-lived MCP connections.
- `id` (INTEGER / INT IDENTITY, PK)
- `refresh_token` (NVARCHAR(128) / TEXT, UNIQUE NOT NULL)
- `client_id` (NVARCHAR(100) / TEXT, NOT NULL)
- `google_auth_id` (INT / INTEGER, FK `google_auth(id) ON DELETE CASCADE`)
- `scope` (NVARCHAR(255) / TEXT, NULL)
- `expires_at` (DATETIME2 / TEXT, NOT NULL)
- `revoked` (TINYINT / INTEGER, NOT NULL DEFAULT 0)
- `created_at` (DATETIME2 / TEXT)
### Store Methods (`libdashstore`)
Under `src/store/mcp/` in `dashdbschema`:
- `store.mcp.registerClient(input)`
- `store.mcp.getClient(clientId)`
- `store.mcp.listClients()`
- `store.mcp.deleteClient(clientId)`
- `store.mcp.createAuthCode(input)`
- `store.mcp.getAuthCode(code)`
- `store.mcp.consumeAuthCode(code)`
- `store.mcp.createRefreshToken(input)`
- `store.mcp.getRefreshToken(refreshToken)`
- `store.mcp.revokeRefreshToken(refreshToken)`
Wrapped with execution timing logs in `dashback` via `src/db/mcp.js` and `STORE_NAMESPACES`.
---
## 4. Endpoints & Flow Breakdown
All routes are defined in [`src/api/routes/mcp.js`](src/api/routes/mcp.js).
### Step 1: Protected Resource Discovery
When an MCP client connects to `POST /mcp` without credentials:
- Server responds with **`401 Unauthorized`** and header:
```http
WWW-Authenticate: Bearer error="invalid_token", error_description="Missing Authorization header", resource_metadata="http://<host>:4208/.well-known/oauth-protected-resource"
```
- The client queries `GET /.well-known/oauth-protected-resource`, receiving:
```json
{
"resource": "http://<host>:4208/mcp",
"authorization_servers": ["http://<host>:4208"],
"scopes_supported": ["mcp:tools"],
"resource_name": "Dashback MCP Server"
}
```
### Step 2: Authorization Server Discovery
The client queries `GET /.well-known/oauth-authorization-server`, receiving:
```json
{
"issuer": "http://<host>:4208",
"authorization_endpoint": "http://<host>:4208/oauth/authorize",
"token_endpoint": "http://<host>:4208/oauth/token",
"registration_endpoint": "http://<host>:4208/oauth/register",
"introspection_endpoint": "http://<host>:4208/oauth/introspect",
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"code_challenge_methods_supported": ["S256"],
"scopes_supported": ["mcp:tools"]
}
```
### Step 3: Dynamic Client Registration
The client registers with `POST /oauth/register`:
```json
{
"client_name": "Visual Studio Code",
"redirect_uris": [
"https://vscode.dev/redirect",
"http://127.0.0.1/",
"http://127.0.0.1:33418/"
],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"]
}
```
The server stores the client in `mcp_client` and returns status `201 Created` with a unique `client_id` (UUID).
### Step 4: User Authorization & Consent via Frontend
When the MCP client directs the user's browser to authenticate:
- The MCP client reads `authorization_endpoint` from `/.well-known/oauth-authorization-server`, which resolves to `http://<host>:4209/oauth/authorize`.
- If an agent or client hits `GET /oauth/authorize` directly on the backend, the backend redirects (HTTP 302) to `${frontendUrl}/oauth/authorize?<queryParams>`.
- The user is authenticated in the Dashfront frontend SPA.
- The frontend calls `GET /api/mcp/authorize?<queryParams>` to fetch client details and user context for the consent dialog.
- When the user clicks **Authorize**, the frontend calls `POST /api/mcp/authorize` (`action: "allow"`).
- The server generates a single-use authorization code (in `mcp_auth_code`) and returns `{ ok: true, redirectUrl: "http://127.0.0.1:<port>/?code=<code>&state=<state>" }`.
- The frontend sets `window.location.href = data.redirectUrl`, delivering the code to the client loopback listener.
- **Loopback Port Matching (RFC 8252 §7.3)**: Native applications bind to ephemeral ports (e.g. `54754`). The server uses `isRedirectUriAllowed()` to permit any port on `127.0.0.1` and `localhost` when the registered URI is on loopback.
### Step 5: Token Exchange
The MCP client sends a POST request with PKCE verifier to `POST /oauth/token`:
```http
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code=<code>&client_id=<id>&redirect_uri=...&code_verifier=<verifier>
```
The server:
1. Validates `code`, client ID, expiration, and unused status.
2. Computes `BASE64URL(SHA256(code_verifier))` and compares it against `code_challenge`.
3. Marks the code as used in `mcp_auth_code`.
4. Issues a signed JWT access token (1-hour TTL) with payload `{ sub: userId, id: userId, client_id, scope: "mcp:tools", aud: "<host>/mcp" }`.
5. Issues a 30-day random hex refresh token stored in `mcp_token`.
### Step 6: MCP Session Handling (`/mcp`)
The client connects to `/mcp` with `Authorization: Bearer <access_token>`.
- **`requireBearerAuth`**: Validates the JWT and verifies that the `userId` exists and is active in `google_auth`.
- **`StreamableHTTPServerTransport`** (`@modelcontextprotocol/sdk`):
- `POST /mcp`: Handles session `initialize` (generating a session UUID and returning `Mcp-Session-Id` header) and subsequent JSON-RPC tool calls.
- `GET /mcp`: Handles server-sent events (SSE) streaming connections.
- `DELETE /mcp`: Closes the transport session.
---
## 5. The Exposed Tools
The MCP server exposes three tools to external clients:
### 1. `dashback_request`
Performs HTTP requests against the Dashback API:
```javascript
server.registerTool('dashback_request', {
title: 'Dashback API Request',
description: 'Perform HTTP requests against the Dashback API',
inputSchema: {
method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']),
path: z.string().describe('e.g. /articles, /orders, /category'),
query: z.record(z.string(), z.any()).nullable().optional(),
body: z.union([z.string(), z.record(z.string(), z.any())]).nullable().optional(),
note: z.string().nullable().optional(),
},
}, async ({ method = 'GET', path, query = null, body = null, note }) => { ... });
```
### 2. `dash-subagent`
Delegates a self-contained task to an isolated subagent with access to Dashback tools and VFS:
```javascript
server.registerTool('dash-subagent', {
title: 'Dashback Subagent',
description: 'Delegate a task to an isolated subagent with access to Dashback tools and VFS.',
inputSchema: {
task: z.string().describe('Specific instructions and goal for the subagent.'),
skills: z.array(z.string()).nullable().optional().describe('Array of SKILL names or slugs to inject.'),
conversation_id: z.number().int().positive().nullable().optional().describe('Existing subagent conversation ID to follow up on, or null to create a new one.'),
max_iterations: z.number().int().min(1).max(30).nullable().optional().describe('Safety bound on tool loops (default 15, max 30).'),
model: z.string().nullable().optional().describe('Optional OpenRouter model override.'),
note: z.string().nullable().optional(),
},
}, async ({ task, skills, conversation_id, max_iterations, model, note }, extra) => { ... });
```
### 3. `search-articles`
Searches articles across name, brand, EAN, SKU, and supplier SKU (AND logic, ordered by name):
```javascript
server.registerTool('search-articles', {
title: 'Search Articles',
description: 'Search articles by name, brand, EAN, SKU, or supplier SKU (AND logic, ordered by name).',
inputSchema: {
searchquery: z.string().nullable().optional().describe('Keywords matching name, brand, EAN, SKU, or supplier SKU (AND logic).'),
excludewords: z.string().nullable().optional().describe('Keywords to exclude (AND logic).'),
offset: z.number().int().min(0).nullable().optional().describe('Pagination offset (default 0).'),
limit: z.number().int().min(1).max(20).nullable().optional().describe('Window size 1–20 (default 3).'),
},
}, async ({ searchquery, excludewords, offset, limit }) => { ... });
```
Because all tool executions use credentials scoped to the authenticated user's `google_auth_id`, all Dashback route-level permissions, company isolation (`req.user.selectedCompanyId`), audit logging, and data scoping apply automatically.
---
## 6. How to Connect from VS Code / GitHub Copilot
### Configuration
Add the server to your VS Code MCP configuration (either in `.vscode/mcp.json` or your global VS Code settings):
```json
{
"dashback": {
"url": "http://127.0.0.1:4208/mcp",
"type": "http"
}
}
```
*(If connecting from another device over LAN, use `http://<server-ip>:4208/mcp`)*.
### First Connection Flow:
1. Open VS Code with Copilot Chat.
2. VS Code contacts `/mcp`, triggers OAuth 2.1 discovery, and asks you to open a browser for authorization.
3. Click **Open** and press **Authorize** on the Dashback consent screen.
4. VS Code receives the tokens and displays `#dashback_request` in Copilot Chat tools.
---
## 7. Frontend Developer Implementation Guide
The frontend (`dashfront`, port 4209) needs two views / features:
1. **Consent Screen Route (`/oauth/authorize`)**: Where external MCP clients send users to approve access.
2. **Authorizations Management (Settings/Security)**: Where users view authorized MCP clients and revoke access.
### 7.1. Consent Screen Route: `/oauth/authorize`
When an MCP client initiates authentication, the user is navigated to:
```
http://<host>:4209/oauth/authorize?client_id=...&redirect_uri=...&response_type=code&code_challenge=...&code_challenge_method=S256&state=...&scope=mcp:tools
```
#### Step 1: Ensure User Authentication
- Check if the user is authenticated in Dashfront.
- If not logged in, redirect to `/login` preserving the full URL as `returnTo` so they return here after Google SSO.
#### Step 2: Fetch Authorization Prompt Metadata
The frontend makes an authenticated GET request:
```http
GET /api/mcp/authorize?<all_query_params_from_url>
```
**Response (200 OK):**
```json
{
"client": {
"clientId": "642a188e-036f-4f5a-8448-95207096c4ef",
"clientName": "Visual Studio Code",
"scope": "mcp:tools"
},
"user": {
"id": 1,
"email": "user@example.com"
},
"params": {
"clientId": "642a188e-036f-4f5a-8448-95207096c4ef",
"redirectUri": "http://127.0.0.1:54754/",
"codeChallenge": "...",
"codeChallengeMethod": "S256",
"state": "...",
"scope": "mcp:tools"
}
}
```
#### Step 3: Render Consent UI
Display a clean dialog or card:
- Application requesting access: `client.clientName`
- Authorizing account: `user.email`
- Permissions requested: `dashback_request` API tools (`mcp:tools`)
- Action buttons:
- **Authorize** (Allow)
- **Cancel** (Deny)
#### Step 4: Submit Consent Decision
When the user clicks **Authorize**:
```http
POST /api/mcp/authorize
Content-Type: application/json
{
"client_id": "<params.clientId>",
"redirect_uri": "<params.redirectUri>",
"code_challenge": "<params.codeChallenge>",
"code_challenge_method": "<params.codeChallengeMethod>",
"state": "<params.state>",
"scope": "<params.scope>",
"action": "allow"
}
```
**Response (200 OK):**
```json
{
"ok": true,
"redirectUrl": "http://127.0.0.1:54754/?code=<auth_code>&state=<state>"
}
```
The frontend immediately navigates the browser:
```javascript
window.location.href = res.data.redirectUrl;
```
When the user clicks **Cancel**:
```http
POST /api/mcp/authorize
Content-Type: application/json
{
"client_id": "<params.clientId>",
"redirect_uri": "<params.redirectUri>",
"state": "<params.state>",
"action": "deny"
}
```
**Response (200 OK):**
```json
{
"ok": false,
"redirectUrl": "http://127.0.0.1:54754/?error=access_denied&state=<state>"
}
```
Navigate: `window.location.href = res.data.redirectUrl;`.
---
### 7.2. Authorizations Management (Settings / Security)
In the user profile or security settings, users can view active MCP client authorizations and revoke them.
#### List Active Authorizations
```http
GET /api/mcp/authorizations
```
**Response (200 OK):**
```json
{
"authorizations": [
{
"clientDbId": 3,
"clientId": "0a2e6bbb-05a5-4b03-bed1-9b853b5aec34",
"clientName": "Visual Studio Code",
"scope": "mcp:tools",
"clientCreatedAt": "2026-10-12T20:36:46.036Z",
"authorizedAt": "2026-10-12T20:39:28.148Z",
"tokenCount": 1
}
]
}
```
#### Revoke an Authorization
When the user clicks **Revoke** / **Delete**:
```http
DELETE /api/mcp/authorizations/:clientId
```
**Response (200 OK):**
```json
{
"ok": true,
"message": "Authorizations revoked"
}
```
This immediately revokes all active refresh tokens for that client and the user. Any future token refresh requests by that client will be rejected with `invalid_grant`.