u
This commit is contained in:
371
mcp.md
Normal file
371
mcp.md
Normal file
@@ -0,0 +1,371 @@
|
||||
# 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 the internal **`dashback_request`** tool (the same tool provided to the local agent via `src/tools/dashback-request.js`) to external MCP clients such as:
|
||||
- Visual Studio Code / GitHub Copilot
|
||||
- Claude Desktop
|
||||
- Cursor / Windsurf
|
||||
|
||||
All operations executed via `dashback_request` 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 Tool: `dashback_request`
|
||||
|
||||
The MCP server registers the exact same tool exposed to the internal local agent (`src/tools/dashback-request.js`):
|
||||
|
||||
```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 }) => {
|
||||
// Generates a 5-minute JWT signed with env.jwtSecret scoped to the authenticated user's ID
|
||||
// Performs fetch against http://127.0.0.1:<port>/api<path>
|
||||
// Returns status and truncated body (up to 20,000 characters)
|
||||
});
|
||||
```
|
||||
|
||||
Because the internal fetch uses a JWT containing the 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`.
|
||||
|
||||
Reference in New Issue
Block a user