Skip to content

feat(mcp): Streamable HTTP transport — apps as MCP servers (ADR-0036 Phase 2)#1626

Merged
xuyushun441-sys merged 1 commit into
mainfrom
feat/mcp-transport
Jun 6, 2026
Merged

feat(mcp): Streamable HTTP transport — apps as MCP servers (ADR-0036 Phase 2)#1626
xuyushun441-sys merged 1 commit into
mainfrom
feat/mcp-transport

Conversation

@xuyushun441-sys
Copy link
Copy Markdown
Contributor

What

Phase 2 of ADR-0036 — every ObjectStack app becomes a network-reachable MCP server. The MCP plugin previously spoke stdio only, so a remote agent (Claude Desktop / Cursor) couldn't connect to a hosted env. This adds the Streamable HTTP transport and wires it into the runtime on top of the Phase 1a sys_api_key auth foundation (#1624).

Changes

@objectstack/plugin-mcp-server

  • MCPServerRuntime.handleHttpRequest(request, { bridge, parsedBody }) serves one MCP request over the SDK's Web-standard WebStandardStreamableHTTPServerTransport (runs on Node 18+, Workers, Deno, Bun). Stateless: a fresh, isolated McpServer+transport per request (the SDK-recommended pattern — avoids cross-request id/session collisions), JSON-response mode so the body is fully buffered (no streaming pass-through concerns over the Worker→container hop).
  • New registerObjectTools + McpDataBridge (mcp-http-tools.ts): the object-CRUD tool set — list_objects, describe_object, query_records, get_record, create_record, update_record, delete_record. All execution is delegated to an injected, principal-bound bridge; the tool layer never touches the data engine. sys_* objects are not exposed by default (fail-closed guard on every object-scoped tool). The internal AI/authoring toolRegistry is deliberately not bridged onto the external surface.

@objectstack/runtime

  • HttpDispatcher serves /mcp:
    • opt-in via OS_MCP_SERVER_ENABLED=true → 404 when off (surface not advertised). Multi-tenant cloud overrides this gate per env.
    • fail-closed auth → anonymous = 401 (requires the principal resolved by Phase 1a's API-key path or a session).
    • builds an McpDataBridge that runs every op through the existing callData path bound to the request's ExecutionContext — external agents run under the key's permissions + RLS, never a parallel or escalated path.
    • discovery (/api/v1/discovery) advertises mcp only when enabled.

Why stateless-per-request is correct

In stateless mode the transport skips session validation and the SDK Server has no init-gate, so a fresh per-request server handles tools/call directly — real clients send initialize to one ephemeral server and tools/call to another. Verified against the SDK source.

Tests (local, all green)

  • plugin 36 (11 new): initialize handshake, tools/list, tools/call delegation, sys_ guard (fail-closed), bridge-error → tool error mapping, 406 on bad Accept, destructive/read-only annotations.
  • runtime 365 (5 new): opt-in gate (404), missing service (501), anonymous (401), delegation with bridge+parsedBody, principal binding (bridge → callData → engine receives the caller's ExecutionContext, not system).
  • tsc --noEmit clean; plugin DTS build clean.

Security

Every external MCP entry runs as the scoped sys_api_key principal under existing object permissions + RLS; MCP is opt-in per env; no raw keys/secrets cross the wire; fail-closed on anything ambiguous.

Follow-ups (out of scope, per ADR §4/§5)

  • objectui: Integrations page "Connect an agent" — generate claude_desktop_config.json / Cursor snippet (reads discovery.mcp), show-once key.
  • cloud: per-env OS_MCP_SERVER_ENABLED toggle in the multi-tenant runtime.

🤖 Generated with Claude Code

…servers (ADR-0036 Phase 2)

The MCP server plugin spoke stdio only, so remote agents (Claude Desktop /
Cursor) could not reach a hosted env. This adds the Streamable HTTP transport
and wires it into the runtime request path on top of the Phase 1a sys_api_key
auth foundation.

plugin-mcp-server:
- MCPServerRuntime.handleHttpRequest(request, { bridge, parsedBody }) serves one
  MCP request over the Web-standard WebStandardStreamableHTTPServerTransport
  (Node 18+/Workers/Deno/Bun). Stateless: a fresh isolated McpServer + transport
  per request (SDK-recommended), JSON-response mode → fully buffered, no
  streaming pass-through concerns over the Worker→container hop.
- New registerObjectTools + McpDataBridge (mcp-http-tools.ts): object-CRUD tool
  set (list_objects, describe_object, query_records, get_record, create_record,
  update_record, delete_record). Execution delegated to an injected,
  principal-bound bridge — the tool layer never touches the data engine. System
  (sys_*) objects are not exposed by default (fail-closed guard per tool). The
  internal AI/authoring toolRegistry is deliberately NOT bridged externally.

runtime:
- HttpDispatcher serves /mcp: opt-in via OS_MCP_SERVER_ENABLED=true (404 when
  off), fail-closed auth (anonymous → 401). Builds an McpDataBridge that runs
  every op through the existing callData path bound to the request's
  ExecutionContext — external agents run under the key's permissions + RLS,
  never a parallel/escalated path. Discovery advertises mcp only when enabled.

Tests: plugin 36 (11 new: handshake/tools/list/call, sys_ guard, error mapping,
406), runtime 365 (5 new: gate/auth/principal-binding). tsc clean.

Security: every external MCP entry runs as the scoped sys_api_key principal
under existing permissions + RLS; opt-in per env; no raw keys/secrets on the wire.

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

vercel Bot commented Jun 6, 2026

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

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

Request Review

@xuyushun441-sys xuyushun441-sys merged commit bc0d85b into main Jun 6, 2026
7 of 8 checks passed
@xuyushun441-sys xuyushun441-sys deleted the feat/mcp-transport branch June 6, 2026 21:24
@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/l labels Jun 6, 2026
xuyushun441-sys added a commit that referenced this pull request Jun 6, 2026
…mcp + ADR-0036 amendment (#1627)

Drops the legacy `plugin-` prefix and moves the outbound MCP-server package to
the top level (`packages/mcp`), parallel to `@objectstack/rest` — both are "your
app exposed over a protocol". Inbound MCP stays `@objectstack/connector-mcp`.

- packages/plugins/plugin-mcp-server → packages/mcp; name → @objectstack/mcp;
  internal plugin id → com.objectstack.mcp; build/tsconfig relative paths fixed
  for the new depth. Exported API unchanged (MCPServerPlugin, MCPServerRuntime,
  registerObjectTools, McpDataBridge, …).
- @objectstack/cli: dependency + dynamic-loader pkg id updated.
- Inbound refs (runtime test, spec comment, changeset config, docs) updated.
  Pre-launch clean break — no compat shim (only cli depended on it internally).
- ADR-0036 amendment (2026-06-07): records (A) the rename, (B) granularity =
  per-environment not per-app (one MCP server per env covers all apps; dynamic
  apps via live discovery; key-scope for narrowing), (C) distribution = skills +
  MCP (one generic portable Skill + live MCP, not hand-maintained vendor config
  snippets) — verified Agent Skills is now an open cross-platform standard.
- Status updated: Phase 1a (#1624) + Phase 2 (#1626) shipped; Phase 2b next.

Build + typecheck + tests green (mcp 36, runtime mcp 5); lockfile regenerated.

Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
xuyushun441-sys added a commit that referenced this pull request Jun 6, 2026
…R-0036) (#1631)

The dispatcher mounts routes explicitly (no catch-all). #1626 (MCP transport)
and #1630 (key-gen) added dispatch() branches but never registered the HTTP
routes, so /api/v1/mcp and /api/v1/keys 404'd at the HTTP layer before reaching
the dispatcher. Unit tests called handlers directly, hiding it; caught in live
staging e2e.

- Register /mcp (GET/POST/DELETE) + /keys (POST) via dispatch() in the
  dispatcher plugin (transport reads the method from the request).
- dispatcher-plugin.routes.test.ts asserts the registrations (the missing
  regression). Full runtime suite 379 green.

Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants