MCPcopy Create free account
hub / github.com/UsefulSoftwareCo/executor / mcpDispatch

Function mcpDispatch

packages/hosts/mcp/src/envelope.ts:336–395  ·  view source on GitHub ↗
(resource: McpResource)

Source from the content-addressed store, hash-verified

334
335/** Dispatch an MCP request through authenticate -> store.dispatch -> transport. */
336const mcpDispatch = (resource: McpResource) =>
337 Effect.gen(function* () {
338 const httpRequest = yield* HttpServerRequest.HttpServerRequest;
339 const auth = yield* McpAuthProvider;
340 const store = yield* McpSessionStore;
341 const request = yield* toWebRequest(httpRequest);
342
343 // CORS preflight: answer before auth so unauthenticated clients can probe.
344 if (request.method === "OPTIONS") {
345 return fromWebResponse(corsPreflightResponse());
346 }
347
348 // Streamable-HTTP only defines GET/POST/DELETE on the endpoint. Any other
349 // method (PUT/PATCH/…) is rejected with a JSON-RPC 405 BEFORE auth/dispatch —
350 // otherwise it would fall through and spin up a session engine for a method
351 // the transport can't serve.
352 if (!ALLOWED_MCP_METHODS.has(request.method)) {
353 return fromWebResponse(jsonRpcResponse(405, -32001, "Method not allowed"));
354 }
355
356 const sessionId = request.headers.get("mcp-session-id");
357
358 // Authenticate (and, for session-aware providers, authorize) on EVERY
359 // request. Non-authenticated outcomes render directly. Session teardown is
360 // only safe after the store can validate the authenticated principal and MCP
361 // resource; an auth-level Forbidden may not carry either.
362 const outcome = yield* auth.authenticate(request);
363 if (!Predicate.isTagged(outcome, "Authenticated")) {
364 return fromWebResponse(renderAuthError(auth, request, outcome));
365 }
366 const principal = outcome.principal;
367
368 // No session id: per the streamable-HTTP transport contract, only POST opens
369 // a session. A GET needs an existing id (400); a DELETE on nothing is a
370 // no-op (204). Both short-circuit BEFORE dispatch so the store never spins up
371 // an engine for a bare GET/DELETE.
372 if (!sessionId) {
373 if (request.method === "GET") {
374 return fromWebResponse(
375 jsonRpcResponse(400, -32000, "mcp-session-id header required for SSE"),
376 );
377 }
378 if (request.method === "DELETE") {
379 return fromWebResponse(
380 new Response(null, { status: 204, headers: { "access-control-allow-origin": "*" } }),
381 );
382 }
383 }
384
385 const result: McpDispatchResult = yield* store.dispatch({
386 request,
387 principal,
388 resource,
389 sessionId,
390 method: request.method,
391 });
392 return fromWebResponse(
393 result instanceof Response ? result : renderDispatchError(result, request.method),

Callers 1

mcpRouteFunction · 0.85

Calls 7

toWebRequestFunction · 0.85
fromWebResponseFunction · 0.85
renderDispatchErrorFunction · 0.85
corsPreflightResponseFunction · 0.70
jsonRpcResponseFunction · 0.70
renderAuthErrorFunction · 0.70
getMethod · 0.65

Tested by

no test coverage detected