Skip to content

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

Closed

Description

@objectstack-fleet

repo:cloud#3 · os-warren · session_01JQwxbZqfEd7pcckjz3pGqn

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 直接回答"工具不存在"。这一步发生在拦截器之前,所以没有任何地方留下记录。
  • 影响:安全负责人看不到"某个 AI 试图越权写数据"。这正是审计最该看到的一类事件。
  • 要做:未注册工具的调用也经过 mcp: a host can observe and refuse every MCP tool call through one neutral interceptor (cloud#3820 O2) #31605 的拦截入口(或单独上报一次),让宿主能记一行。
  • 需要你做的:无,分诊定车道。

Reproduction (pin 9e1f1f9)

Work

  1. 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.
  2. 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.

Related: cloud#4049, cloud#4057, objectstack#31605, objectstack#31796.

Activity

  1. objectstack-fleet commented on Nov 9, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: domain:server · p2 · pm:queue · 2026-11-09T06:56Z

    ⛔ Not a claim.


    Generated by Claude Code

  2. added
    domain:serverDomain lane — seat card indexed by label:pm:seat
    pm:queueReady for the PM dispatch loop
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 9, 2026
  3. objectstack-fleet commented on Nov 9, 2026

    @objectstack-fleet
    ContributorAuthor

    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_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).

  4. objectstack-fleet commented on Nov 9, 2026

    @objectstack-fleet
    ContributorAuthor

    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_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).

  5. objectstack-fleet commented on Nov 9, 2026

    @objectstack-fleet
    ContributorAuthor

    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.

  6. added a commit that references this issue on Nov 10, 2026
    551b68e
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingdomain:serverDomain lane — seat card indexed by label:pm:seatpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions