Skip to content

fix(mcp): make remote /api/v1/mcp data plane work for external agents#2309

Merged
os-zhuang merged 2 commits into
mainfrom
fix/mcp-dataplane-auth
Jun 24, 2026
Merged

fix(mcp): make remote /api/v1/mcp data plane work for external agents#2309
os-zhuang merged 2 commits into
mainfrom
fix/mcp-dataplane-auth

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Summary

The remote MCP HTTP surface (ADR-0036, /api/v1/mcp) was non-functional for external agents — found via local end-to-end testing. Three bugs, all the same family: the MCP route resolves data services differently (and more buggily) than REST, so a key that authenticates on REST was rejected on MCP, standard MCP clients couldn't connect at all, and writes crashed.

# Symptom Root cause
1 (P0) Valid api-key → 401 on /api/v1/mcp, same key works on REST resolveExecutionContext's getQl used scoped resolveService('objectql', envId), which in the multi-env runtime returns an instance that can't see the env's sys_api_key rows → key never resolves. REST resolves identity via kernel.getServiceAsync('objectql') directly (the same gotcha handleActions works around).
2 Authorization: Bearer <key> rejected extractApiKey only accepted X-API-Key / ApiKey. Remote MCP clients (Claude Desktop / Cursor / Claude Code) send the key as a Bearer per the MCP spec.
3 create/update/delete_recordql.insert is not a function buildMcpBridge passes context.dataDriver (a raw db driver in the multi-env runtime) as ql; callData writes called ql.insert/update/delete directly. Reads worked only because query/get prefer protocol.findData/getData.

Changes (4 files, +74/-15)

  • runtime/http-dispatcher.tsresolveExecutionContext getQl: prefer this.kernel.getServiceAsync('objectql'), fall back to the scoped path. Aligns MCP identity resolution with REST.
  • core/security/api-key.tsextractApiKey: also accept Authorization: Bearer osk_<key> (prefix-gated). A better-auth session Bearer never carries the osk_ prefix, so the session path is unaffected.
  • runtime/http-dispatcher.tscallData create/update/delete: route through protocol.createData/updateData/deleteData (mirroring the read paths and REST) before falling back to ql.
  • Tests updated/added for the new Bearer behaviour.

Validation

Live, on a local isolated stack (cloud + multi-tenant objectos runtime, real env, real sys_api_key):

  • tools/list 401 → 200 with both X-API-Key and Authorization: Bearer
  • create_record succeeds (was ql.insert is not a function); query_records / get_record work
  • sys_* objects stay fail-closed; no-auth and bad-key stay 401
  • REST data/meta unaffected (no regression)

Unit: @objectstack/mcp 44/44 · core api-key 10/10 · runtime (mcp / exec-context / keys / kernel-resolver) 41/41.

Follow-ups (not in this PR)

  • HTTP MCP surface exposes no aggregate tool (stdio-only) — agents must page+count client-side.
  • query_records passes where through to protocol.findData/objectql, but the tool description only documents equality — agents won't discover operator/range filters. Worth enriching the tool schema.

🤖 Generated with Claude Code

The remote MCP HTTP surface (ADR-0036) was non-functional for external
agents: every call 401'd with a valid key, standard MCP clients couldn't
connect, and writes threw "ql.insert is not a function". All three are the
same family — the MCP route resolves data services differently from REST.

- resolveExecutionContext getQl: resolve objectql from the per-request kernel
  directly (kernel.getServiceAsync) instead of the scoped resolveService, which
  in the multi-env runtime hands back an instance blind to the env's
  sys_api_key rows so the api-key never resolved (→ 401). REST already resolves
  identity this way (rest-server), so REST + MCP no longer drift.
- extractApiKey: accept `Authorization: Bearer osk_<key>` (prefix-gated). Remote
  MCP clients (Claude Desktop / Cursor / Claude Code) send the key as a Bearer
  per the MCP spec; a better-auth session Bearer never carries the osk_ prefix,
  so the session path is unaffected.
- callData create/update/delete: route through protocol.createData/updateData/
  deleteData (mirroring the read paths and REST) instead of calling ql.insert on
  context.dataDriver, which in the multi-env runtime is a raw db driver with no
  ORM methods.

Validated on a local isolated stack: tools/list 401->200 (X-API-Key AND Bearer),
create_record succeeds, sys_* and no-auth stay fail-closed, REST unaffected.
Unit: mcp 44/44, core api-key 10/10, runtime mcp/exec-ctx/keys 41/41.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 24, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
spec Ready Ready Preview, Comment Jun 24, 2026 6:12pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/core, @objectstack/runtime.

30 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/concepts/cloud-artifact-api.mdx (via packages/runtime)
  • content/docs/concepts/core/index.mdx (via @objectstack/core)
  • content/docs/concepts/core/plugins.mdx (via @objectstack/core)
  • content/docs/concepts/core/services.mdx (via @objectstack/core)
  • content/docs/concepts/implementation-status.mdx (via @objectstack/core, @objectstack/runtime)
  • content/docs/concepts/north-star.mdx (via packages/core, packages/runtime)
  • content/docs/concepts/packages.mdx (via @objectstack/core, @objectstack/runtime)
  • content/docs/concepts/webhook-delivery.mdx (via @objectstack/core)
  • content/docs/guides/ai-capabilities.mdx (via @objectstack/core)
  • content/docs/guides/api-reference.mdx (via @objectstack/runtime)
  • content/docs/guides/authentication.mdx (via @objectstack/core, @objectstack/runtime)
  • content/docs/guides/cloud-deployment.mdx (via @objectstack/runtime)
  • content/docs/guides/contracts/index.mdx (via @objectstack/core)
  • content/docs/guides/deployment-vercel.mdx (via @objectstack/runtime)
  • content/docs/guides/driver-configuration.mdx (via @objectstack/runtime)
  • content/docs/guides/hook-bodies.mdx (via @objectstack/runtime)
  • content/docs/guides/kernel-services.mdx (via @objectstack/core)
  • content/docs/guides/objectql-migration.mdx (via @objectstack/core)
  • content/docs/guides/packages.mdx (via @objectstack/core, @objectstack/runtime)
  • content/docs/guides/plugin-chatbot-integration.mdx (via @objectstack/runtime)
  • content/docs/guides/plugin-development.mdx (via @objectstack/core)
  • content/docs/guides/plugins.mdx (via @objectstack/core)
  • content/docs/guides/production-readiness.mdx (via @objectstack/runtime)
  • content/docs/guides/runtime-services/examples.mdx (via @objectstack/core)
  • content/docs/guides/single-project-mode.mdx (via @objectstack/runtime)
  • content/docs/protocol/objectos/config-resolution.mdx (via @objectstack/core)
  • content/docs/protocol/objectos/http-protocol.mdx (via @objectstack/runtime)
  • content/docs/protocol/objectos/index.mdx (via @objectstack/core, @objectstack/runtime)
  • content/docs/protocol/objectos/lifecycle.mdx (via @objectstack/core, @objectstack/runtime)
  • content/docs/protocol/objectos/plugin-spec.mdx (via @objectstack/core)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

Comment thread packages/core/src/security/api-key.ts Fixed
Comment thread packages/core/src/security/api-key.ts Fixed
CodeQL flagged `^(ApiKey|Bearer)\s+(.+)$` as polynomial-ReDoS: `\s+` and
`.+` both match whitespace, so an all-whitespace tail backtracks O(n²).
Anchor the capture to a non-whitespace first char (`\s+(\S.*)`) — linear,
same behaviour for every valid header. Clears the pre-existing ApiKey alert
too.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@os-zhuang
os-zhuang merged commit ffdd88d into main Jun 24, 2026
14 of 15 checks passed
@os-zhuang
os-zhuang deleted the fix/mcp-dataplane-auth branch June 24, 2026 18:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants