Files
dashback/mcp.md
2026-10-18 14:29:01 +02:00

16 KiB

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. 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.

Step 1: Protected Resource Discovery

When an MCP client connects to POST /mcp without credentials:

  • Server responds with 401 Unauthorized and header:
    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:
    {
      "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:

{
  "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:

{
  "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).

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:

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:

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:

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):

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):

{
  "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.

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:

GET /api/mcp/authorize?<all_query_params_from_url>

Response (200 OK):

{
  "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"
  }
}

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)

When the user clicks Authorize:

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):

{
  "ok": true,
  "redirectUrl": "http://127.0.0.1:54754/?code=<auth_code>&state=<state>"
}

The frontend immediately navigates the browser:

window.location.href = res.data.redirectUrl;

When the user clicks Cancel:

POST /api/mcp/authorize
Content-Type: application/json

{
  "client_id": "<params.clientId>",
  "redirect_uri": "<params.redirectUri>",
  "state": "<params.state>",
  "action": "deny"
}

Response (200 OK):

{
  "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

GET /api/mcp/authorizations

Response (200 OK):

{
  "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:

DELETE /api/mcp/authorizations/:clientId

Response (200 OK):

{
  "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.