Skip to content

[recipes] Fix CORS + JSON-RPC envelope on work-operating-model-activation#329

Draft
drewyd wants to merge 1 commit into
NateBJones-Projects:mainfrom
drewyd:fix/work-operating-model-cors
Draft

[recipes] Fix CORS + JSON-RPC envelope on work-operating-model-activation#329
drewyd wants to merge 1 commit into
NateBJones-Projects:mainfrom
drewyd:fix/work-operating-model-cors

Conversation

@drewyd
Copy link
Copy Markdown

@drewyd drewyd commented May 26, 2026

Problem

The work-operating-model-activation recipe's MCP Edge Function is unreachable from browser-based MCP clients (Claude.ai web UI being the canonical case). Two issues compound:

  1. CORS preflight fails. The function has no OPTIONS handler. Browsers reject the subsequent POST because no Access-Control-Allow-Origin/Allow-Headers/Allow-Methods headers come back on the preflight.
  2. Unauthorized response is not valid JSON-RPC 2.0. When the access key check fails, the function returns a bare { error: "Unauthorized" } body at HTTP 401. MCP clients that strictly validate JSON-RPC envelopes ({ jsonrpc: "2.0", id, error: { code, message } }) reject the response before they can surface the real auth issue to the user, and the 401 status code itself makes some browser fetch wrappers throw before the JSON is even parsed.

Reproduction

  1. Deploy the recipe as-is to a Supabase project per recipes/work-operating-model-activation/README.md.
  2. In claude.ai -> Settings -> Connectors -> Add custom connector, paste the function URL with ?key=<MCP_ACCESS_KEY>.
  3. Claude.ai surfaces a generic connection error. Network panel shows the OPTIONS preflight returning no Access-Control-Allow-Origin header and the POST never firing.
  4. Even when bypassing CORS (e.g. server-side curl with an intentionally wrong key), the body is a bare { "error": "Unauthorized" } rather than a JSON-RPC error envelope.

Fix

Minimal, localized to the Hono app setup and the app.all("*") catch-all. No changes to the four MCP tools, schemas, or any business logic.

  • Add app.options("*", ...) returning standard CORS allow-list (origin, methods, and the headers MCP transports actually use: authorization, x-client-info, apikey, content-type, x-brain-key, accept, mcp-session-id, mcp-protocol-version, last-event-id).
  • Replace the bare 401 response with unauthorizedResponse(id), which returns a JSON-RPC 2.0 envelope ({ jsonrpc: "2.0", error: { code: -32001, message }, id }) at HTTP 200, with CORS headers attached. The inbound request id is best-effort echoed so the client can correlate.
  • Move the auth gate above the accept-header patching so the unauthorized path doesn't consume the request body twice and conflict with transport.handleRequest(c) ownership of the stream.

The patched version of this function has been running successfully against the live Claude.ai web UI for ~24h on the contributor's deployment, completing a full five-layer operating-model session end-to-end.

Notes

  • Branch named fix/work-operating-model-cors per the explicit task instruction. Happy to rename to contrib/drewyd/work-operating-model-cors-fix per CONTRIBUTING.md convention if maintainers prefer - just shout.
  • Marked as draft for maintainer review before flipping to ready.

Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com

…tion

Browser-based MCP clients (e.g. the Claude.ai web UI) cannot reach this
recipe's Edge Function because:

1. CORS preflight is not handled. OPTIONS requests get no
   Access-Control-Allow-* headers, so the browser blocks the POST.
2. The unauthorized path returns a bare { error: "Unauthorized" } at HTTP
   401, which is not a valid JSON-RPC 2.0 response. MCP clients that
   strictly validate the envelope reject the response before surfacing
   the real auth issue.

Fix:

- Add an OPTIONS handler returning the standard CORS allow-list (origin,
  methods, headers used by MCP transports including mcp-session-id,
  mcp-protocol-version, last-event-id).
- Wrap the unauthorized response in a JSON-RPC 2.0 envelope
  ({ jsonrpc, error: { code, message }, id }) at HTTP 200, with the
  CORS headers attached, and best-effort echo the inbound request id.
- Move the auth gate above the accept-header patching so we never consume
  the request body twice on the unauthorized path.

No behaviour change for authorized callers. No refactor of the four MCP
tools or any business logic.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added the recipe Contribution: step-by-step recipe label May 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

recipe Contribution: step-by-step recipe

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant