Summary
Implement the stateless Streamable HTTP endpoint handler at POST /api/v1/mcp. This fills in the empty route shell from #7426 with the per-request McpServer lifecycle, authentication enforcement, and the Fastify-to-MCP transport bridge. Tool definitions are not part of this task; the endpoint ships with an empty tool list and a registration hook for subsequent tasks to plug into.
Prerequisites
Requirements
Endpoint handler
POST /api/v1/mcp: each request creates a fresh McpServer and StreamableHTTPServerTransport (stateless, sessionIdGenerator: undefined)
- Call
reply.hijack() then delegate to transport.handleRequest(request.raw, reply.raw, request.body). The third parsedBody argument is a supported SDK parameter for frameworks that pre-parse the request body.
- After handling, call
mcpServer.close() and transport.close() to clean up per-request resources
GET /api/v1/mcp and DELETE /api/v1/mcp return 405 (not applicable for stateless mode)
Authentication
verifySession (inherited from the parent EE routes hook) handles PAT resolution from the Authorization header, no additional auth wiring needed
- Add a preHandler that rejects requests without a resolved
request.session.User (e.g. project/device tokens, missing auth) with 401
- Reject requests where the authenticated user is a platform admin with 403. Admin-owned PATs must not be usable with MCP.
- Errors return structured JSON with
code and error fields, descriptive enough for an LLM agent to relay to the user
Tool registration hook
- Expose a mechanism for tool definition modules to register themselves on the per-request McpServer
- Tool modules export entries of
{ name, description, inputSchema, annotations, handler }, endpoint iterates and calls mcpServer.registerTool() for each
- Ships with zero tools:
tools/list returns empty array, tools/call rejects unknown tools
Tests
POST /api/v1/mcp with valid PAT returns a valid MCP JSON-RPC response (tools/list returns empty array)
POST /api/v1/mcp without auth returns 401
POST /api/v1/mcp with an admin-owned PAT returns 403
GET /api/v1/mcp returns 405
DELETE /api/v1/mcp returns 405
- Stateless behavior: no session state leaks between sequential requests
References
Summary
Implement the stateless Streamable HTTP endpoint handler at
POST /api/v1/mcp. This fills in the empty route shell from #7426 with the per-request McpServer lifecycle, authentication enforcement, and the Fastify-to-MCP transport bridge. Tool definitions are not part of this task; the endpoint ships with an empty tool list and a registration hook for subsequent tasks to plug into.Prerequisites
Requirements
Endpoint handler
POST /api/v1/mcp: each request creates a freshMcpServerandStreamableHTTPServerTransport(stateless,sessionIdGenerator: undefined)reply.hijack()then delegate totransport.handleRequest(request.raw, reply.raw, request.body). The thirdparsedBodyargument is a supported SDK parameter for frameworks that pre-parse the request body.mcpServer.close()andtransport.close()to clean up per-request resourcesGET /api/v1/mcpandDELETE /api/v1/mcpreturn 405 (not applicable for stateless mode)Authentication
verifySession(inherited from the parent EE routes hook) handles PAT resolution from the Authorization header, no additional auth wiring neededrequest.session.User(e.g. project/device tokens, missing auth) with 401codeanderrorfields, descriptive enough for an LLM agent to relay to the userTool registration hook
{ name, description, inputSchema, annotations, handler }, endpoint iterates and callsmcpServer.registerTool()for eachtools/listreturns empty array,tools/callrejects unknown toolsTests
POST /api/v1/mcpwith valid PAT returns a valid MCP JSON-RPC response (tools/listreturns empty array)POST /api/v1/mcpwithout auth returns 401POST /api/v1/mcpwith an admin-owned PAT returns 403GET /api/v1/mcpreturns 405DELETE /api/v1/mcpreturns 405References
McpServerfrom@modelcontextprotocol/sdk/server/mcp.js,StreamableHTTPServerTransportfrom@modelcontextprotocol/sdk/server/streamableHttp.jsverifySession:forge/routes/auth/index.js:65-165forge/ee/routes/mcpServer/index.js(reference only, not to be reused as-is)