405 lines
16 KiB
Markdown
405 lines
16 KiB
Markdown
# 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(10).max(50).nullable().optional().describe('Safety bound on tool loops (min 10, default 20, max 50).'),
|
|
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`.
|
|
|