You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
mcp: a tools/call for a tool the grant did not register is answered "not found" before any interceptor, so a scope-narrowed write leaves no trace #31838
Filing gate: ② a finding from a dispatched card. cloud#4049 installed the governance interceptor on the #31605 seam and measured the gap below (PR cloud#4057, report cloud#4049 comment 6733693119). The cloud lane does not dispatch this card; triage routes it.
维护者速读
问题:外部 AI 用只读令牌(data:read)调用写入工具(例如 create_record)时,MCP 直接回答"工具不存在"。这一步发生在拦截器之前,所以没有任何地方留下记录。
Route a tools/call for a name the session did not register through the interceptor with a marker (e.g. registered: false), or call it once for observation only. The answer stays "not found", so nothing about the hidden surface leaks.
Keep it cheap: no handler runs.
Done when
A tools/call for an unregistered name reaches the installed interceptor once, with the principal and the name. The answer is unchanged.
A test pins both.
Dedupe: repo:objectstack-ai/objectstack is:issue: "MCP unregistered tool interceptor" 1 hit (#31605, the seam, closed), "tools/call not found audit" 55 hits, none on this.
Claim: PM loop round 1 · #31838 · 2026-11-09T08:00Z · bug (p2; cloud#4049 / cloud#4057 consumer)
Session: session_01ADHkmPF1skfu2kx89BEXZi
Account: os-steve
Branch: claude/project-thread-2jr1fz (the card thread's harness-pinned branch, restarted from main after #31851 merged; platform exception to claude/issue-N-slug)
Worktree: objectstack-issue-31838
Domain: domain:server
Seat: domain:server#1
File surface: packages/mcp/src/mcp-server-runtime.ts, packages/mcp/src/tool-call-interceptor.ts, their tests, a changeset (stop on breach; explain in the report)
Container & model: M, mode:thread, model: default tier, effort: high
Clause-②: yes (widening: the tool-call interceptor also observes a call for an unregistered name; the answer is unchanged)
Metadata-protocol: none
Ruling-ref: none (#31605's seam contract, "one seam every MCP tool call passes through")
Responsibility: a tools/call for a name the session did not register is answered by the SDK before the #31605 interceptor, so no host can record the attempt · reached by every MCP client calling a tool its grant does not register
Thread-read: none
Premise: holds (read at 2e9121ed: the interceptor wraps registered tools only)
Serial constraints cleared: none known; overlap with seat 2's MCP cards (#31826, #31836) is re-checked in step 0, before the first push.
Order: standard order + server-seat1/31838-delta.md (project files).
Claim: PM loop round 1 (restated: surface measured in step 0, no edit to mcp-server-runtime.ts) · #31838 · 2026-11-09T08:03Z · bug (p2; cloud#4049 / cloud#4057 consumer)
Session: session_01ADHkmPF1skfu2kx89BEXZi
Account: os-steve
Branch: claude/project-thread-2jr1fz (the card thread's harness-pinned branch, restarted from main after #31851 merged; platform exception to claude/issue-N-slug)
Worktree: objectstack-issue-31838
Domain: domain:server
Seat: domain:server#1
File surface: packages/mcp/src/tool-call-interceptor.ts (routeToolCallsThrough also observes an unregistered tools/call), packages/mcp/src/types.ts (McpToolCallContext.registered?: false), their tests, a changeset; not packages/mcp/src/mcp-server-runtime.ts, which seat 2's #31836 branch edits (stop on breach; explain in the report)
Container & model: M, mode:thread, model: default tier, effort: high
Clause-②: yes (widening: the tool-call interceptor also observes a call for an unregistered name; the answer is unchanged)
Metadata-protocol: none
Ruling-ref: none (#31605's seam contract, "one seam every MCP tool call passes through")
Responsibility: a tools/call for a name the session did not register is answered by the SDK before the #31605 interceptor, so no host can record the attempt · reached by every MCP client calling a tool its grant does not register
Thread-read: none
Premise: holds (read at 2e9121ed: the interceptor wraps registered tools only)
Serial constraints cleared: step 0 measured seat 2's #31836 branch touching mcp-server-runtime.ts only; this card no longer edits that file, and nothing of seat 2 touches tool-call-interceptor.ts or types.ts.
Order: standard order + server-seat1/31838-delta.md (project files).
{
"issue": 31838,
"status": "done",
"branch": "claude/project-thread-2jr1fz",
"pr": "https://github.com/objectstack-ai/objectstack/pull/31875",
"session": "session_01ADHkmQ8n76QVvYSyAqeFeV",
"premise_still_valid": true,
"summary": "Step 0 on main 2e9121ed: reproduced in a test that a tools/call for a name the session did not register is answered by the SDK (McpServer's own tools/call handler: 'MCP error -32602: Tool X not found', isError) and the installed #31605 interceptor is never called. Overlap: seat 2's #31836 branch edits mcp-server-runtime.ts; stopped before the first push and reported; seat ruled (6735186378) seat 1 keeps the card with surface tool-call-interceptor.ts + types.ts, mcp-server-runtime.ts untouched. Seam: routeToolCallsThrough (already called for stdio and per-request HTTP servers) also wraps the server's CallToolRequestSchema handler as the SDK installs it. Choice (a): the same interceptor, context gains registered: false. Observation only: the SDK answers first and its answer is returned byte-identical whatever the interceptor does; a throw/reject/verdict is caught and logged once at warn with the tool name and the error's code or class, never the arguments or message (seat condition 1). Registered is read from the SDK's live _registeredTools at each call, so a tool removed via remove() reads as unregistered (condition 2). Arguments are a frozen {} (condition 3, named in the PR body). No interceptor installed: the SDK handler runs untouched, no principal resolved.",
"tests": "head 0af23406. tool-call-interceptor.test.ts gains one describe, 6 cases: HTTP data:read grant calling create_record (raw response identical with/without the interceptor; context exactly {door, transport, toolName, arguments: {}, principal, registered: false}, frozen; bridge create never ran); five interceptors (allow via next(), deny, replace, coded throw quoting the arguments, rejection) each answer identical, next() = frozen SDK answer, 2 warn lines with neither arguments nor message; stdio arbitrary name told once; live registry after remove(); CONTROL registered call has no registered key; CONTROL no interceptor, zero principal resolutions. No existing test edited. File 49/49. Ablations via ablation-replace.mjs (src import, no dist leg): handler not wrapped (= base behaviour) 4 failed/45 passed; snapshot registry 1 failed/48; catch rethrows 1 failed/48; log carries message 1 failed/48; each restored blob == HEAD, git diff HEAD empty.",
"gates": "head 0af23406. mcp typecheck exit 0; mcp suite 62 files / 994 tests passed (lock VERDICT command-exit 0); root eslint --no-inline-config packages/mcp exit 0. dispatch-gates --commands: 61 families, each run with < /dev/null: 61/61 exit 0; --ran reconcile: 61 run, 0 NOT-MEASURED (DERIVED zero), 0 UNRUN. Prereqs: full turbo build --filter='./packages/**' 81/81, teardown fixture fetched. PR check-runs: not awaited at report time.",
"dogfood_battery": "0af23406 · os-verify-lock VERDICT command-exit 0 (held 26m57s) · Test Files 441 passed, 3 skipped (444) · Tests 3662 passed, 42 skipped (3704) · covers the three MCP 'not found' pin files the delta asks for (mcp-internal-field-evaluate-positions.dogfood.test.ts, mcp-stdio-runtime-object-exposure.test.ts, mcp-stdio-package-object-discovery.test.ts)",
"mcp_calls": "4 — mcp__github__issue_read (#31838 get, #31838 get_comments, #31826 get_comments, #31836 get_comments); no write tool",
"api_writes": "2 — relay pr_create (objectstack-ai/objectstack#31875, read-back 9448 bytes sent = stored); this os-dev-report comment via post-stamped.mjs. git push is not a REST write.",
"files_changed": [
".changeset/31838-mcp-unregistered-tool-call-interceptor.md",
"packages/mcp/src/tool-call-interceptor.ts",
"packages/mcp/src/tool-call-interceptor.test.ts",
"packages/mcp/src/types.ts"
],
"line_budget": "304 changed lines (+303 / -1) in 4 files, under the 3000 human-merge threshold; all inside the ruled surface",
"deviations": [
"PR body: the harness attribution block (two lines) precedes 'Fixes #31838', as on #31851.",
"First ablation attempt (wrap off) was refused by ablation-replace.mjs before any run (replacement contained the anchor); re-run as 'schema === undefined', recorded in the PR body.",
"main moved 8 commits (2e9121ed -> origin/main) before pr_create, none touching the 3 source files; not merged, per standard order rule 7.",
"PR assignee not set (MCP write forbidden); relay read-back lists none."
],
"open_questions": [
{ "question": "Contract member name and shape: McpToolCallContext.registered?: false, present only on the unregistered case (the seat's call).", "options": ["A keep: absent on registered calls so every existing toEqual pin and installer sees the same context", "B registered: boolean on every call"], "recommendation": "A, because B would change every registered call's context and widen the change past this card" }
],
"security_findings": "none",
"out_of_scope_findings": [
"A registered but disabled tool ('Tool X disabled') is still answered by the SDK without reaching the interceptor; no platform site disables a tool today, so not filed.",
"_registeredTools is a private SDK member; pinned by the new tests and guarded by a warn-once when unreadable, so an SDK upgrade that renames it goes red in CI."
]
}
Activity
objectstack-fleet commentedon Nov 9, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage:
domain:server· p2 ·pm:queue· 2026-11-09T06:56Z⛔ Not a claim.
domain:server, the MCP tools/call path and the feat(mcp): a host can observe and refuse every MCP tool call through one neutral interceptor #31633 interceptor. Per [Decision]repo:cloudis used in two senses: 6 open cards are the cloud PM's whole (withdomain:cloud), 20 are objectstack-lane work that cloud asked for. Which one does the label mean? #31154 A it carries an objectstack lane; norepo:cloud. Grade p2 as filed: an unregistered tool's refusal should pass the interceptor.Generated by Claude Code
objectstack-fleet commentedon Nov 9, 2026
domain:server#1 · os-steve · session_01ADHkmPF1skfu2kx89BEXZi
Claim: PM loop round 1 · #31838 · 2026-11-09T08:00Z · bug (p2; cloud#4049 / cloud#4057 consumer)
Session:
session_01ADHkmPF1skfu2kx89BEXZiAccount:
os-steveBranch:
claude/project-thread-2jr1fz(the card thread's harness-pinned branch, restarted frommainafter #31851 merged; platform exception toclaude/issue-N-slug)Worktree:
objectstack-issue-31838Domain:
domain:serverSeat:
domain:server#1File surface:
packages/mcp/src/mcp-server-runtime.ts,packages/mcp/src/tool-call-interceptor.ts, their tests, a changeset (stop on breach; explain in the report)Container & model:
M,mode:thread,model: default tier,effort: highClause-②: yes (widening: the tool-call interceptor also observes a call for an unregistered name; the answer is unchanged)
Metadata-protocol: none
Ruling-ref: none (#31605's seam contract, "one seam every MCP tool call passes through")
Responsibility: a
tools/callfor a name the session did not register is answered by the SDK before the #31605 interceptor, so no host can record the attempt · reached by every MCP client calling a tool its grant does not registerThread-read: none
Premise: holds (read at
2e9121ed: the interceptor wraps registered tools only)Serial constraints cleared: none known; overlap with seat 2's MCP cards (#31826, #31836) is re-checked in step 0, before the first push.
Order: standard order +
server-seat1/31838-delta.md(project files).objectstack-fleet commentedon Nov 9, 2026
domain:server#1 · os-steve · session_01ADHkmPF1skfu2kx89BEXZi
Claim: PM loop round 1 (restated: surface measured in step 0, no edit to mcp-server-runtime.ts) · #31838 · 2026-11-09T08:03Z · bug (p2; cloud#4049 / cloud#4057 consumer)
Session:
session_01ADHkmPF1skfu2kx89BEXZiAccount:
os-steveBranch:
claude/project-thread-2jr1fz(the card thread's harness-pinned branch, restarted frommainafter #31851 merged; platform exception toclaude/issue-N-slug)Worktree:
objectstack-issue-31838Domain:
domain:serverSeat:
domain:server#1File surface:
packages/mcp/src/tool-call-interceptor.ts(routeToolCallsThroughalso observes an unregisteredtools/call),packages/mcp/src/types.ts(McpToolCallContext.registered?: false), their tests, a changeset; notpackages/mcp/src/mcp-server-runtime.ts, which seat 2's #31836 branch edits (stop on breach; explain in the report)Container & model:
M,mode:thread,model: default tier,effort: highClause-②: yes (widening: the tool-call interceptor also observes a call for an unregistered name; the answer is unchanged)
Metadata-protocol: none
Ruling-ref: none (#31605's seam contract, "one seam every MCP tool call passes through")
Responsibility: a
tools/callfor a name the session did not register is answered by the SDK before the #31605 interceptor, so no host can record the attempt · reached by every MCP client calling a tool its grant does not registerThread-read: none
Premise: holds (read at
2e9121ed: the interceptor wraps registered tools only)Serial constraints cleared: step 0 measured seat 2's #31836 branch touching
mcp-server-runtime.tsonly; this card no longer edits that file, and nothing of seat 2 touchestool-call-interceptor.tsortypes.ts.Order: standard order +
server-seat1/31838-delta.md(project files).objectstack-fleet commentedon Nov 9, 2026
domain:server#1 · os-steve · session_01ADHkmQ8n76QVvYSyAqeFeV
os-dev-report
{ "issue": 31838, "status": "done", "branch": "claude/project-thread-2jr1fz", "pr": "https://github.com/objectstack-ai/objectstack/pull/31875", "session": "session_01ADHkmQ8n76QVvYSyAqeFeV", "premise_still_valid": true, "summary": "Step 0 on main 2e9121ed: reproduced in a test that a tools/call for a name the session did not register is answered by the SDK (McpServer's own tools/call handler: 'MCP error -32602: Tool X not found', isError) and the installed #31605 interceptor is never called. Overlap: seat 2's #31836 branch edits mcp-server-runtime.ts; stopped before the first push and reported; seat ruled (6735186378) seat 1 keeps the card with surface tool-call-interceptor.ts + types.ts, mcp-server-runtime.ts untouched. Seam: routeToolCallsThrough (already called for stdio and per-request HTTP servers) also wraps the server's CallToolRequestSchema handler as the SDK installs it. Choice (a): the same interceptor, context gains registered: false. Observation only: the SDK answers first and its answer is returned byte-identical whatever the interceptor does; a throw/reject/verdict is caught and logged once at warn with the tool name and the error's code or class, never the arguments or message (seat condition 1). Registered is read from the SDK's live _registeredTools at each call, so a tool removed via remove() reads as unregistered (condition 2). Arguments are a frozen {} (condition 3, named in the PR body). No interceptor installed: the SDK handler runs untouched, no principal resolved.", "tests": "head 0af23406. tool-call-interceptor.test.ts gains one describe, 6 cases: HTTP data:read grant calling create_record (raw response identical with/without the interceptor; context exactly {door, transport, toolName, arguments: {}, principal, registered: false}, frozen; bridge create never ran); five interceptors (allow via next(), deny, replace, coded throw quoting the arguments, rejection) each answer identical, next() = frozen SDK answer, 2 warn lines with neither arguments nor message; stdio arbitrary name told once; live registry after remove(); CONTROL registered call has no registered key; CONTROL no interceptor, zero principal resolutions. No existing test edited. File 49/49. Ablations via ablation-replace.mjs (src import, no dist leg): handler not wrapped (= base behaviour) 4 failed/45 passed; snapshot registry 1 failed/48; catch rethrows 1 failed/48; log carries message 1 failed/48; each restored blob == HEAD, git diff HEAD empty.", "gates": "head 0af23406. mcp typecheck exit 0; mcp suite 62 files / 994 tests passed (lock VERDICT command-exit 0); root eslint --no-inline-config packages/mcp exit 0. dispatch-gates --commands: 61 families, each run with < /dev/null: 61/61 exit 0; --ran reconcile: 61 run, 0 NOT-MEASURED (DERIVED zero), 0 UNRUN. Prereqs: full turbo build --filter='./packages/**' 81/81, teardown fixture fetched. PR check-runs: not awaited at report time.", "dogfood_battery": "0af23406 · os-verify-lock VERDICT command-exit 0 (held 26m57s) · Test Files 441 passed, 3 skipped (444) · Tests 3662 passed, 42 skipped (3704) · covers the three MCP 'not found' pin files the delta asks for (mcp-internal-field-evaluate-positions.dogfood.test.ts, mcp-stdio-runtime-object-exposure.test.ts, mcp-stdio-package-object-discovery.test.ts)", "mcp_calls": "4 — mcp__github__issue_read (#31838 get, #31838 get_comments, #31826 get_comments, #31836 get_comments); no write tool", "api_writes": "2 — relay pr_create (objectstack-ai/objectstack#31875, read-back 9448 bytes sent = stored); this os-dev-report comment via post-stamped.mjs. git push is not a REST write.", "files_changed": [ ".changeset/31838-mcp-unregistered-tool-call-interceptor.md", "packages/mcp/src/tool-call-interceptor.ts", "packages/mcp/src/tool-call-interceptor.test.ts", "packages/mcp/src/types.ts" ], "line_budget": "304 changed lines (+303 / -1) in 4 files, under the 3000 human-merge threshold; all inside the ruled surface", "deviations": [ "PR body: the harness attribution block (two lines) precedes 'Fixes #31838', as on #31851.", "First ablation attempt (wrap off) was refused by ablation-replace.mjs before any run (replacement contained the anchor); re-run as 'schema === undefined', recorded in the PR body.", "main moved 8 commits (2e9121ed -> origin/main) before pr_create, none touching the 3 source files; not merged, per standard order rule 7.", "PR assignee not set (MCP write forbidden); relay read-back lists none." ], "open_questions": [ { "question": "Contract member name and shape: McpToolCallContext.registered?: false, present only on the unregistered case (the seat's call).", "options": ["A keep: absent on registered calls so every existing toEqual pin and installer sees the same context", "B registered: boolean on every call"], "recommendation": "A, because B would change every registered call's context and widen the change past this card" } ], "security_findings": "none", "out_of_scope_findings": [ "A registered but disabled tool ('Tool X disabled') is still answered by the SDK without reaching the interceptor; no platform site disables a tool today, so not filed.", "_registeredTools is a private SDK member; pinned by the new tests and guarded by a warn-once when unreadable, so an SDK upgrade that renames it goes red in CI." ] }Stamped 2026-11-09T08:42Z.
fix(mcp): a tools/call for a name no tool is registered under reaches…