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

vercelBot commented Jun 6, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
specBuildingBuildingPreview, CommentJun 6, 2026 9:24pm

Request Review

@xuyushun441-sys
xuyushun441-sys merged commit bc0d85b into mainJun 6, 2026
7 of 8 checks passed
@xuyushun441-sys
xuyushun441-sys deleted the feat/mcp-transport branch June 6, 2026 21:24
@github-actionsgithub-actionsBot 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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@xuyushun441-sys@os-zhuang