Skip to content

Commit e472bbe

Browse files
os-zhuangclaude
andauthored
fix(mcp): apply the ADR-0049 exposure gate on the stdio MCP bridge (#8083) (#8265)
Both MCP transports register the same tools from the same McpDataBridge, but the two hosts implement that bridge over different seams: HTTP runs every data verb through callData, which gates on the object's declared apiEnabled / apiMethods before dispatch, while the stdio bridge went straight to the engine and applied no gate. One declaration, two transports, two answers. This is a surface-area declaration leak, not an authorization bypass -- the gate is a surface-area control by api-exposure.ts's own ADR note, and every stdio call passed the engine's CRUD/FLS/RLS before this change and after it. What was leaking is the author's exposure declaration. The decision is not re-implemented here: it comes from the spec's single source of truth (resolveEffectiveApiMethods / isApiOperationAllowed), the same helpers checkApiExposure (runtime) and apiAccessDenialFromEnable (rest) delegate to, so each surface owns only its envelope. Refusals carry the same two machine codes REST answers with. Gated verbs are exactly the six buildMcpBridge routes through callData; list_objects / describe_object stay ungated because the HTTP bridge answers both off the metadata service, and gating them would be a fresh divergence pointing the other way. The isSystem bypass, the fail-open on unresolvable metadata and the flat-shape fallback are matched to the HTTP path deliberately. Claude-Session: https://claude.ai/code/session_016pY4Xb2iDecfDtT3CWoiTW Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6ca0b49 commit e472bbe

3 files changed

Lines changed: 555 additions & 5 deletions

File tree

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
---
2+
"@objectstack/mcp": patch
3+
---
4+
5+
fix(mcp): the stdio transport honours the ADR-0049 `apiEnabled` / `apiMethods` exposure declaration (#8083)
6+
7+
An object that declares `enable.apiEnabled: false` — or narrows `enable.apiMethods`
8+
is telling the platform which data operations it exposes over the API. That
9+
declaration was honoured on the MCP **HTTP** surface and ignored on the MCP
10+
**stdio** surface: same product, same tool names, same key, different answer.
11+
12+
**This is a surface-area declaration leak, not an authorization bypass.** The gate
13+
is a surface-area control by `api-exposure.ts`'s own ADR note, and every stdio call
14+
passed the ObjectQL security middleware (CRUD / FLS / RLS) before this change and
15+
after it. What was leaking is the author's *exposure declaration*, not the data
16+
guard.
17+
18+
The two MCP hosts implement the same `McpDataBridge` over different seams — HTTP
19+
through `callData`, which gates before dispatch; stdio straight onto the engine,
20+
which did not. The stdio bridge now applies the same gate, and takes its decision
21+
from the same single source of truth both existing enforcement points already
22+
delegate to (the spec's `resolveEffectiveApiMethods` / `isApiOperationAllowed`), so
23+
the three-state whitelist, the action-to-operation mapping and the derived verbs
24+
resolve identically on all three surfaces.
25+
26+
Gated verbs are exactly the six the HTTP bridge routes through `callData`:
27+
`query_records`, `get_record`, `create_record`, `update_record`, `delete_record`
28+
and `aggregate_records`. `list_objects` / `describe_object` stay ungated, because
29+
the HTTP bridge answers both straight off the metadata service — a schema read
30+
refused on stdio and served on HTTP would be the same divergence pointing the
31+
other way.
32+
33+
Refusals carry the same machine codes the REST surface answers with:
34+
`OBJECT_API_DISABLED` (the object is hidden) and `OBJECT_API_METHOD_NOT_ALLOWED`
35+
(the operation is outside the whitelist, with the effective operation set
36+
attached). Three behaviours are matched to the HTTP path deliberately: a system
37+
context bypasses the gate, unresolvable metadata **fails open** to the schema
38+
defaults, and the flat legacy definition shape is read when there is no nested
39+
`enable` block.
40+
41+
Unaffected: the remaining known divergences between the two MCP bridges (the
42+
protocol layer's ingress `readonly` strip, its existence probes, its spec-shaped
43+
receipts and `expand` / `select`) are unchanged and still filed as follow-up work.

0 commit comments

Comments
 (0)