Follow-on to #7645, found by re-testing it. #7645 is genuinely fixed and this is not a regression of it — the stdio transport is no longer deaf: initialize now answers immediately (it used to time out with zero bytes on stdout), and resources/list, resources/templates/list and resources/read all answer. The five fail-closed clauses were each re-proved from scratch rather than carried over, and they all hold. What the fix made visible is a second, independent gap that was previously masked by the deafness.
This is not an RLS bypass. The one principal-bound read path stdio does expose (the record resource template) was driven on both sides and the gate held: a member key reads its own row in full and gets not found for another principal's row; an admin key reads both — matching REST row-for-row.
Symptom
The stdio server's initialize result advertises capabilities.tools: {}, but the tool surface is empty:
tools/list → -32601 Method not found- every
tools/call → -32601 Method not found - across 4 fresh boots, and on retry at +3 s and +8 s after
notifications/initialized (ruling out a registration race)
Decisive isolation: in the same process, with the sameosk_ key, the HTTP transport (POST /api/v1/mcp) returns all 11 tools. So the bridge, the key, and the principal are all fine — the tools are simply never registered on the stdio server.
Boot log corroborates: [MCP] Bridged 4 resource endpoints, Agent prompts bridged, [MCP] Bridged 0 skill prompts — and no tool-bridging line at all.
Root cause
The object/action tool surface is built per HTTP request, on a throwaway server, and never on the long-lived stdio server.
packages/mcp/src/mcp-server-runtime.ts — handleHttpRequest() constructs a fresh McpServer per request and registers the tools onto that instance:
// mcp-server-runtime.ts:1157,1166 — inside handleHttpRequest()registerObjectTools(server,opts.bridge,opts.toolOptions);
...
registerActionTools(server,opts.bridgeasMcpActionBridge,opts.toolOptions);
The long-lived server used by stdio (this.mcpServer) only ever receives tools through bridgeTools(toolRegistry) (mcp-server-runtime.ts:663), which is a different surface — the AI service's function-calling ToolRegistry, wired in packages/mcp/src/plugin.ts:138:
constaiService=ctx.getService<IAIService&{toolRegistry?: ToolRegistry}>('ai');if(aiService?.toolRegistry)this.runtime.bridgeTools(aiService.toolRegistry);The showcase registers no ToolRegistry tools, so zero tools land on the stdio server. registerObjectTools/registerActionTools are imported only at line 9 and called only from inside handleHttpRequest — there is no path that reaches them for stdio.
The -32601 alongside an advertised tools capability is consistent with this: the MCP SDK only wires the tools/list handler once at least one tool is registered, while the capability object is declared statically at construction.
Reproduction
- Boot the showcase with stdio MCP enabled:
node packages/cli/bin/run.js serve -p <port> --dev with stdio: ['pipe','pipe','pipe'], OS_MCP_STDIO_ENABLED=true, and a valid osk_ key. - Write JSON-RPC
initialize, then notifications/initialized, then tools/list to the child's stdin. - Against the same process,
POST /api/v1/mcp with the same key and body {"jsonrpc":"2.0","id":1,"method":"tools/list"}.
Expected both transports expose the same tool surface (11 tools).
Actual HTTP returns 11 tools; stdio returns -32601 Method not found, while its own initialize result claims capabilities.tools: {}.
Blast radius
Every MCP client that speaks stdio (the transport most desktop MCP hosts default to) sees an ObjectStack server with resources and prompts but no tools — it cannot query or mutate a single object. The 17/17 unit pins stay green because they exercise handleHttpRequest and bridgeTools separately, never asserting the two transports agree on the tool surface.
Downstream in the checklist
ai.mcp-stdio-fail-closed clauses 4/5 are blocked by this (no aggregate to reconcile against on stdio). Clause 5 has a separate fixture gap worth recording: the factory showcase has no readable: false FLS-masked field anywhere (repo-wide grep is zero), runtime authoring of one is refused 403 NOT_OVERRIDABLE, and the scratch-artifact escape hatch fails because the connector provider factory comes from objectstack.config.ts.
Suggested shape (not prescriptive)
Register the object/action tools on the long-lived server too — either by hoisting the registerObjectTools/registerActionTools pair into a shared wireTools(server, bridge, opts) called from both start() (stdio branch) and handleHttpRequest(), or by having stdio route through the same per-request assembly. Whatever the shape, the gate that keeps this from coming back is a test that asserts the two transports expose the same tool names for the same bridge — the current pins can't see a divergence.
Source
Extracted from the checklist retest run (framework b602d536, console 6314e87f2d49), re-testing #7645.
Follow-on to #7645, found by re-testing it. #7645 is genuinely fixed and this is not a regression of it — the stdio transport is no longer deaf:
initializenow answers immediately (it used to time out with zero bytes on stdout), andresources/list,resources/templates/listandresources/readall answer. The five fail-closed clauses were each re-proved from scratch rather than carried over, and they all hold. What the fix made visible is a second, independent gap that was previously masked by the deafness.This is not an RLS bypass. The one principal-bound read path stdio does expose (the record resource template) was driven on both sides and the gate held: a member key reads its own row in full and gets
not foundfor another principal's row; an admin key reads both — matching REST row-for-row.Symptom
The stdio server's
initializeresult advertisescapabilities.tools: {}, but the tool surface is empty:tools/list→-32601 Method not foundtools/call→-32601 Method not foundnotifications/initialized(ruling out a registration race)Decisive isolation: in the same process, with the same
osk_key, the HTTP transport (POST /api/v1/mcp) returns all 11 tools. So the bridge, the key, and the principal are all fine — the tools are simply never registered on the stdio server.Boot log corroborates:
[MCP] Bridged 4 resource endpoints,Agent prompts bridged,[MCP] Bridged 0 skill prompts— and no tool-bridging line at all.Root cause
The object/action tool surface is built per HTTP request, on a throwaway server, and never on the long-lived stdio server.
packages/mcp/src/mcp-server-runtime.ts—handleHttpRequest()constructs a freshMcpServerper request and registers the tools onto that instance:The long-lived server used by stdio (
this.mcpServer) only ever receives tools throughbridgeTools(toolRegistry)(mcp-server-runtime.ts:663), which is a different surface — the AI service's function-callingToolRegistry, wired inpackages/mcp/src/plugin.ts:138:The showcase registers no
ToolRegistrytools, so zero tools land on the stdio server.registerObjectTools/registerActionToolsare imported only at line 9 and called only from insidehandleHttpRequest— there is no path that reaches them for stdio.The
-32601alongside an advertisedtoolscapability is consistent with this: the MCP SDK only wires thetools/listhandler once at least one tool is registered, while the capability object is declared statically at construction.Reproduction
node packages/cli/bin/run.js serve -p <port> --devwithstdio: ['pipe','pipe','pipe'],OS_MCP_STDIO_ENABLED=true, and a validosk_key.initialize, thennotifications/initialized, thentools/listto the child's stdin.POST /api/v1/mcpwith the same key and body{"jsonrpc":"2.0","id":1,"method":"tools/list"}.Expected both transports expose the same tool surface (11 tools).
Actual HTTP returns 11 tools; stdio returns
-32601 Method not found, while its owninitializeresult claimscapabilities.tools: {}.Blast radius
Every MCP client that speaks stdio (the transport most desktop MCP hosts default to) sees an ObjectStack server with resources and prompts but no tools — it cannot query or mutate a single object. The 17/17 unit pins stay green because they exercise
handleHttpRequestandbridgeToolsseparately, never asserting the two transports agree on the tool surface.Downstream in the checklist
ai.mcp-stdio-fail-closedclauses 4/5 areblockedby this (no aggregate to reconcile against on stdio). Clause 5 has a separate fixture gap worth recording: the factory showcase has noreadable: falseFLS-masked field anywhere (repo-wide grep is zero), runtime authoring of one is refused403 NOT_OVERRIDABLE, and the scratch-artifact escape hatch fails because the connector provider factory comes fromobjectstack.config.ts.Suggested shape (not prescriptive)
Register the object/action tools on the long-lived server too — either by hoisting the
registerObjectTools/registerActionToolspair into a sharedwireTools(server, bridge, opts)called from bothstart()(stdio branch) andhandleHttpRequest(), or by having stdio route through the same per-request assembly. Whatever the shape, the gate that keeps this from coming back is a test that asserts the two transports expose the same tool names for the same bridge — the current pins can't see a divergence.Source
Extracted from the checklist retest run (framework
b602d536, console6314e87f2d49), re-testing #7645.