# 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`
`/.well-known/oauth-protected-resource/mcp` | | **RFC 8414** / OIDC | Authorization Server Metadata | `/.well-known/oauth-authorization-server`
`/.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`
`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://:4208/.well-known/oauth-protected-resource" ``` - The client queries `GET /.well-known/oauth-protected-resource`, receiving: ```json { "resource": "http://:4208/mcp", "authorization_servers": ["http://: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://:4208", "authorization_endpoint": "http://:4208/oauth/authorize", "token_endpoint": "http://:4208/oauth/token", "registration_endpoint": "http://:4208/oauth/register", "introspection_endpoint": "http://: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://: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?`. - The user is authenticated in the Dashfront frontend SPA. - The frontend calls `GET /api/mcp/authorize?` 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:/?code=&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=&client_id=&redirect_uri=...&code_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: "/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 `. - **`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://: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://: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? ``` **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": "", "redirect_uri": "", "code_challenge": "", "code_challenge_method": "", "state": "", "scope": "", "action": "allow" } ``` **Response (200 OK):** ```json { "ok": true, "redirectUrl": "http://127.0.0.1:54754/?code=&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": "", "redirect_uri": "", "state": "", "action": "deny" } ``` **Response (200 OK):** ```json { "ok": false, "redirectUrl": "http://127.0.0.1:54754/?error=access_denied&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`.