feat(mcp): Streamable HTTP transport — apps as MCP servers (ADR-0036 Phase 2)#1626
Merged
Conversation
…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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
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>
This was referenced Jun 6, 2026
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_keyauth foundation (#1624).Changes
@objectstack/plugin-mcp-serverMCPServerRuntime.handleHttpRequest(request, { bridge, parsedBody })serves one MCP request over the SDK's Web-standardWebStandardStreamableHTTPServerTransport(runs on Node 18+, Workers, Deno, Bun). Stateless: a fresh, isolatedMcpServer+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).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/runtimeHttpDispatcherserves/mcp:OS_MCP_SERVER_ENABLED=true→ 404 when off (surface not advertised). Multi-tenant cloud overrides this gate per env.McpDataBridgethat runs every op through the existingcallDatapath bound to the request'sExecutionContext— external agents run under the key's permissions + RLS, never a parallel or escalated path./api/v1/discovery) advertisesmcponly when enabled.Why stateless-per-request is correct
In stateless mode the transport skips session validation and the SDK
Serverhas no init-gate, so a fresh per-request server handlestools/calldirectly — real clients sendinitializeto one ephemeral server andtools/callto another. Verified against the SDK source.Tests (local, all green)
sys_guard (fail-closed), bridge-error → tool error mapping, 406 on bad Accept, destructive/read-only annotations.tsc --noEmitclean; plugin DTS build clean.Security
Every external MCP entry runs as the scoped
sys_api_keyprincipal 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)
claude_desktop_config.json/ Cursor snippet (readsdiscovery.mcp), show-once key.OS_MCP_SERVER_ENABLEDtoggle in the multi-tenant runtime.🤖 Generated with Claude Code