Repository navigation
[MCP] Streamable HTTP transport: session, lifecycle, Origin, contentTypes reuse #614
Description
Activity
- addedenhancementNew feature or requestNew feature or requestarea:componentsComponents / applications subsystemComponents / applications subsystemarea:mcpModel Context Protocol (MCP) server: protocol, profiles, stdio CLIModel Context Protocol (MCP) server: protocol, profiles, stdio CLIfeature:mcp-v1Rollout of native MCP server v1 (HarperFast/harper#465). Removed when v1 closes.Rollout of native MCP server v1 (HarperFast/harper#465). Removed when v1 closes.
on May 19, 2026 - added a parent issue
on May 19, 2026 Heads-up from foundation PR #649
Foundation #649 lands a few choices that affect #614's design surface. Worth knowing before you start:
1.
registerMcpProfileAPI now acceptsrouteOptionsThe component's API signature is:
registerMcpProfile({ profile: 'operations' | 'application', host, // FastifyLike { post(path, opts?, handler) } config, // the merged config tree from getConfigObj() routeOptions?: Record<string, unknown>, })
When
routeOptionsis provided, the host'spost()is called in 3-arg form:host.post(path, routeOptions, handler). The operations call site in #649 uses this to wirepreValidation: [authHandler]. #614 will be the first real consumer — pass the actual Streamable HTTP handler (instead of the stub) plus the samerouteOptionsshape.2. Application profile call site is intentionally deferred to #614
#649 ships only the operations-profile call site. The application-port wiring has to land alongside the real transport because there's no clean global startup hook to register a do-nothing 503 listener on the HTTP port (
server.http(listener, ...)callers all bring their own real listener).When you build the application profile, the simplest path is to either:
- Call
server.http(...)from inside a newbootstrapApplicationMcp()incomponents/mcp/index.ts, invoked from the same startup site that brings up REST/GraphQL routes, or - Follow the
REST.handleApplication(scope)pattern (seeserver/REST.ts:288-303) and register MCP as a built-in plugin incomponents/componentLoader.ts:TRUSTED_RESOURCE_PLUGINS.
Both work; pick whichever is easier to test.
3. The stub 503 body shape is intentional placeholder, not a contract
components/mcp/index.ts:createStubHandlerreturns 503 with body{"error":"mcp_not_implemented","profile":"<profile>"}plusRetry-After: 0. #614 should swap that for the real Streamable HTTP transport response. The route registration plumbing (gating, mount path resolution, auth) stays — only the handler body changes.4. Presence-based config is the gate
Following team convention (matches
replication), there's noenabledflag. Profile is registered iffgetConfigObj().mcp?.<profile>is truthy. #614 will reuse the operations gate as-is; only needs to add the equivalent application gate at its chosen startup site.5. Joi-default propagation gap (Harper-wide, flagged separately)
config/configUtils.js:483-490only re-applies six specific Joi defaults back to the merged config object. Any Joi.default(...)undermcp.*(likemountPath: '/mcp') lives only onvalidation.valueand is discarded —env.get(MCP_*)won't see it. The foundation routes around this by gating on nested block presence viagetConfigObj()and falling back toDEFAULT_MOUNT_PATH = '/mcp'inside the component. #614 should do the same — don't rely on Joi defaults flowing intoenv.get(...).Worth keeping in mind if you add new defaulted keys under
mcp.*: either re-apply them inconfigUtils.validateConfig, or always read viagetConfigObj()+ in-code fallback.- Call
- added a commit that references this issue
on May 26, 2026 - added a commit that references this issue
on May 26, 2026
Metadata
Metadata
Assignees
Labels
Type
Fields
Priority
Scope. Implement the MCP Streamable HTTP transport (POST/GET/DELETE
/mcp), session management (Mcp-Session-Id), required headers (MCP-Protocol-Version,Origin), and the fullinitialize/notifications/initializedlifecycle. Reuse Harper's existingRequest/Headersabstraction and thetext/event-streamserializer incontentTypes.ts.Design reference. Sections "Transports → Streamable HTTP", "Security headers (MCP §transports → Security Warning)", "Server-side integration with existing Harper machinery", "Lifecycle Notes", and "HTTP Headers" in #465.
Acceptance criteria
POST /mcpaccepts JSON-RPC, returns eitherapplication/jsonor upgrades totext/event-streamvia the existing serializer atserver/serverHelpers/contentTypes.ts:127-161. No new Fastify API surface added.GET /mcpreturns 405 when no SSE channel is offered, or opens atext/event-streamchannel for server-initiated messages.DELETE /mcpterminates the session whensession.allowClientDelete: true; returns 405 otherwise.Mcp-Session-Idissued oninitialize(UUIDv4); enforced on subsequent requests; 400 if missing, 404 if terminated.MCP-Protocol-Versionrequired afterinitialize; 400 on unsupported. v1 supports2025-06-18(preferred) and2025-03-26(backcompat).Originheader validated against existinghttp_corsAccessList/operationsApi_network_corsAccessList; 403 on mismatch. No newmcp.allowedOriginskey.notifications/initialized).initializereturns servercapabilities(tools.listChanged: true,resources.listChanged: true,logging: {}); subsequent requests reach a stub handler.initialize→notifications/initializedover HTTP.Out of scope. No tools, no resources, no rate limiting, no audit. Resumability via
Last-Event-IDis deferred to v2 — the SSE serializer'sidfield is reserved but not used.Stacks on. #613 (component scaffold).
Branch & PR conventions
feat/mcp-transportmain(after [MCP] Component scaffold + config schema + boot gating #613 merges).Closes #<self>; references Expose Operations (API) through an MCP Wrapper #465.Smoke test
Tracking. Part of #465. Sub-issue #2 of 11.