MCP

Route & Transport

The MCP server endpoint and its supported transport.

Endpoint

Handoff serves MCP from a single route: /api/mcp. GET, POST, and DELETE all funnel into the same handler, the transport, not the HTTP method, distinguishes request types (see API → /api/mcp).

Transport

The route uses the MCP SDK's WebStandardStreamableHTTPServerTransport, configured stateless (sessionIdGenerator: undefined). There is no SSE transport and no session affinity, every request builds a fresh server instance and is handled independently. This also means there's nothing to "resume": a client reconnecting after a network blip just makes a new request.

Correction vs. earlier design notes

The original MCP RFC describes a Streamable HTTP / SSE transport. The shipped implementation is stateless Streamable HTTP only: no SSE. If you're integrating a client, target Streamable HTTP.

// src/app/app/api/mcp/route.ts
export const runtime = 'nodejs'
export const dynamic = 'force-dynamic'

async function handleMcp(request: Request) {
  const auth = verifyHandoffApiAuth(request) // see Authentication
  const transport = new WebStandardStreamableHTTPServerTransport({ sessionIdGenerator: undefined })
  const server = createHandoffMcpServer(auth, request) // fresh McpServer per request
  await server.connect(transport)
  return transport.handleRequest(request)
}

Auth happens before the server is built

verifyHandoffApiAuth(request) runs first; the resolved auth context (user id, role, scope string) is passed into createHandoffMcpServer(auth, request) and closed over by every tool handler, so scope checks happen per-tool-call, not per-connection. See Authentication for the two modes (workspace vs. registry) and the OAuth flows.

Project context hydration

Handoff doesn't hydrate a session on connect, because there is no persistent session. Instead, the first tool an agent typically calls is handoff_get_project_context, which reads:

  • The active stack profile (bootstrap-handlebars by default, or whatever HANDOFF_DEFAULT_STACK_PROFILE / the caller's stackProfile argument resolves to), see Stack guides.
  • HANDOFF_PROJECT_NAME and HANDOFF_FIGMA_PROJECT_ID for project identity.
  • The workspace's saved Design.MD / brand voice / component reference settings.

These are plain deployment-level environment variables (Vercel env / .env), set independently of handoff.config.js, the MCP hydration layer (project-profile.ts) reads them directly and does not parse the config file itself. The Config type does declare a projectProfile / project_profile block, but nothing reads it, the env vars are the only wired path. See Workspace → handoff.config.* resolution for how the config file itself is located and read for everything else.

On this page