MCP Server
The Model Context Protocol (MCP) server glues the chat runtime to every tool in dapp/app/lib/mcp. This page zooms into the HTTP entry point, authentication layers, dispatcher, and the helper clients that proxy JSON-RPC calls back into the chat UI.
Component Map
| Layer | File | Key Dependencies |
|---|---|---|
| Next.js route | dapp/app/api/mcp/route.ts | aiServerConfig, readJwtFromAny, verifyAccessToken |
| MCP server shell | dapp/app/lib/mcp/server/index.ts | verifyMCPAuth, toolRegistry, aiPublicConfig |
| Dispatcher | dapp/app/lib/mcp/server/dispatcher.ts | @schemas/dto/mcp (Zod), verifyMCPAuth, tool handlers |
| Auth helpers | dapp/app/lib/mcp/server/auth.ts | @lib/jwt/server, Next.js cookies() |
| Tool registry | dapp/app/lib/mcp/tools/index.ts | aiServerConfig, pineconeConfig |
| Chat bridge | dapp/app/lib/llm/tool-bridge.ts | toolRegistry, mode configs, JSON-RPC client |
| External client | dapp/app/lib/mcp/client.ts | CallToolResultSchema, ListToolsResultSchema, ai.tool |
Request Lifecycle
JSON-RPC compatibility matters: both callMcpTool and MCPClient send { method: 'tools/call', params: { name, arguments } } payloads and expect a CallToolResult object (content[], optional isError).
1. Route policy (dapp/app/api/mcp/route.ts)
- Parses the body up front so
readJwtFromAnycan inspect headers, payload fields, or cookies for bearer tokens. - If
aiServerConfig.requiresJwtis true, the route verifies the token withverifyAccessTokenbefore handing the request tomcpServer.handleRequest. - Logs method metadata and standardizes parse errors (
code: -32700).
export async function POST(req: Request): Promise<Response> {
const body = await req.json().catch(() => {
throw new Error('Invalid JSON body');
});
if (aiServerConfig.requiresJwt) {
const bearer = readJwtFromAny(req, body);
if (!bearer) {
return new Response(JSON.stringify({ error: 'Unauthorized: missing JWT' }), {
status: 401,
headers: { 'Content-Type': 'application/json' },
});
}
try {
await verifyAccessToken(bearer);
} catch {
return new Response(JSON.stringify({ error: 'Unauthorized: invalid JWT' }), {
status: 401,
headers: { 'Content-Type': 'application/json' },
});
}
}
return mcpServer.handleRequest(req, body);
}- Sets
export const runtime = 'nodejs'to force the route onto the Node runtime (streaming SSE consumers rely on it). GETrequests respond with405and an inline explanation so probes know the endpoint is JSON-RPC-only.
2. Server shell (dapp/app/lib/mcp/server/index.ts)
- Initializes with the same
requiresJwtflag exposed publicly (aiPublicConfig) so the UI can reflect whether auth is required. - Runs a second
verifyMCPAuthpass. When the incoming payload istools/call, it looks up the tool intoolRegistryand forces verification again if the tool specifiesrequiresJwt(even when the global flag is off). - Returns JSON-RPC style errors (
code: -32001/-32603) and ensures HTTP status codes mirror auth vs server failures.
3. Dispatcher (dapp/app/lib/mcp/server/dispatcher.ts)
- Validates every request against
MCPRequestSchemafrom@schemas/dto/mcp, catching malformed JSON-RPC calls before any tool logic runs. tools/listsimply streams the already-registered tool metadata back.tools/calllocates the tool definition, runs anotherverifyMCPAuthwhenrequiresJwtis true, and injects the verified claims intoargs.__jwtso handlers can rely onaddress,sub, andtokenIdwithout re-parsing the token.- All handlers are expected to return
{ content: [{ type, text|data }], isError?: boolean }. Errors bubble through as in-band content so SSE chips and the LLM receive the same failure reason.
4. Authentication helper (dapp/app/lib/mcp/server/auth.ts)
- Prioritizes the
Authorizationheader, but also inspectsbody.jwt,body.data.jwt, and finally the Next.jscookies()store foraccess_token/jwtcookies. - Uses
verifyAccessTokenfrom@lib/jwt/serverand normalizes the claims so downstream code always seessub,address, andaddrfields (strings only). - Returns the token ID when the verifier surfaces it so tool handlers can correlate mutations or quota entries.
5. Tool runtime (dapp/app/lib/mcp/tools/*)
toolRegistryonly registers a tool when its dependencies are configured (e.g., send-crypto requiresAI_PRIVATE_KEY, Pinecone tools requirepineconeConfig.isConfigured). This keeps the server from advertising tools that would fail at runtime.- Each tool exports
requiresJwtand aninputSchema. The schema is used for OpenAI manifests (getOpenAIToolSchemas) while therequiresJwtflag stays server-side. tool-errors.tsdefines thefail()helper so handlers can abort early and surface canonical red chips without throwing raw strings.
6. Chat bridge (dapp/app/lib/llm/tool-bridge.ts)
getOpenAIToolSchemas(mode)filters the registry by each mode’smcpToolswhitelist (dapp/app/lib/llm/modes/configs/*.ts), ensuring rap battles and freestyle chats receive different manifests.callMcpToolconstructs a JSON-RPC payload, forwards the user’s bearer token (or an override), and parses the JSON response, surfacing HTTP, JSON, and RPC errors separately for easier logging.formatToolResultchooses the most human-friendly text portion from a tool’s result before handing it back to the LLM, while SSE consumers display the structured JSON.
7. External MCP client (dapp/app/lib/mcp/client.ts)
MCPClientis a lightweight fetch wrapper that shares the same JSON-RPC contract, Zod-validated responses (CallToolResultSchema,ListToolsResultSchema), and timeout handling.createMCPToolsuses theaipackage’stool()helper to register schema-less proxies against OpenAI / Anthropic runtimes without tripping invalid parameter errors. It reusesMCPClientto execute actual calls.
Extending the Server
- Add the tool — export a definition in
dapp/app/lib/mcp/tools/<your-tool>.ts, setrequiresJwtonly if you truly need JWT claims, and register it intoolRegistry. - Expose the schema — if the tool should be available to the LLM, whitelist it inside the relevant mode config’s
mcpToolsarray. - Document the behavior — create a companion
.mdxpage (like the other tool docs) and link to it from the catalog table. - Optional auth tweaks — if the tool needs non-standard claims, extend
verifyMCPAuthor provide a handler-specific validator; all__jwtdata is injected underargs.__jwt.
Remember that both the UI presenters and the LLM runtime lean on the content array. Always include at least one text entry for the model and reserve JSON blobs for chips / telemetry.