Skip to content

fix(server): close the tool_call record when output_sanitisation blocks a response - #1687

Merged
Dumbris merged 3 commits into
mainfrom
fix/sanitisation-block-completes-tool-call
Oct 20, 2026
Merged

Dumbris merged 3 commits into
mainfrom
fix/sanitisation-block-completes-tool-call

Conversation

@Dumbris

@Dumbris Dumbris commented Oct 20, 2026 •

Copy link
Copy Markdown
Member

Summary

When output_sanitisation blocks a response (response_action: block), the call_tool_* handlers returned before recording the call's completion. The upstream call did run (side effects may have happened), yet only a policy_decision was persisted: no tool_call activity row (started events are not persisted), an SSE started event with no completion, and no tool-call history entry.

handleCallToolVariant, the legacy handleCallTool and direct routing mode (makeDirectModeHandler, added by #1685) now close the call on a block:

  • Tool-call history: the attempt is stored with Error set to the block message and Response cleared, so the blocked payload is never stored.
  • Activity: a tool_call completion is emitted with status error, error_class: proxy_policy and fault_domain: proxy. It carries the fixed block message, never the response. On call_tool_* paths the internal_tool_call record is emitted as well.
  • The new helper recordOutputBlocked follows the convention of the code_execution sub-call block path, which already closed its record this way.

Counting / audit check

  • Spec 107 audit: the block's policy_decision already writes tool_call outcome:blocked reason:output_sanitisation (isPostDispatchBlockKey). The completion's audit write is the existing deduped no-op (toolCallWritten), so the audit log still has one line per attempt.
  • Telemetry: nothing is double-counted. The availability block counter comes only from policy_decision, the upstream-attempt bucket was counted at dispatch, and activation is not recorded.
  • Activity/Usage call totals (storage.CountsAsCall): the policy_decision (blocked) and the new tool_call (error) both count as failed calls. One blocked attempt therefore counts as two failed calls in those totals. The code_execution sanitisation block and the generation-changed refusal pair already counted this way. Separating them needs the post-dispatch block key on the policy_decision record, so that is left for a follow-up.

Direct mode

After #1685 merged, origin/main was merged into this branch and the direct-mode block now emits the same error tool_call completion. Direct mode keeps no tool-call history.

Tests

  • TestCallToolVariant_SanitisationBlockRecordsToolCallCompletion and TestLegacyCallTool_SanitisationBlockRecordsToolCallCompletion drive a real in-process upstream that returns an AWS key with block mode on. Each asserts one persisted tool_call with status error and error_class proxy_policy, the policy_decision beside it, and the secret absent from every activity record. TestDirectMode_SanitisationBlockRecordsToolCallCompletion asserts the same for direct mode. All three failed before the fix (no tool_call record).
  • TestRecordOutputBlocked_HistoryCarriesBlockMessageNotResponse asserts the history entry carries the block message and no response. The direct-routing fixture's incarnation guard drops all history writes, so this is tested on the helper.
  • go test -race ./internal/server/ with the unit-tests.yml -skip regex: pass.
  • golangci-lint v2 (.github/.golangci.yml), bare and --build-tags server, over ./internal/server/...: no issues in changed files. Six existing staticcheck findings remain in untouched test files.

🤖 Generated with Claude Code

…ks a response

A response_action: block refusal returned before the dispatch recorded its
completion, so a call whose upstream DID run left only a policy_decision: no
persisted tool_call row, an SSE started event with no completion, and no
tool-call history entry.

handleCallToolVariant and the legacy handleCallTool now close the call on a
block: the history entry and the tool_call completion (status error,
error_class proxy_policy) carry the fixed block message, never the response.
The call_tool_* path also emits its internal_tool_call record. The Spec 107
audit tool_call line was already written by the block's policy_decision; the
completion's audit write is the existing deduped no-op.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Deploying mcpproxy-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: a64da04
Status: ✅  Deploy successful!
Preview URL: https://19f38208.mcpproxy-docs.pages.dev
Branch Preview URL: https://fix-sanitisation-block-compl.mcpproxy-docs.pages.dev

View logs

@github-actions

github-actions Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

📦 Build Artifacts

Workflow Run: View Run
Branch: fix/sanitisation-block-completes-tool-call

Available Artifacts

  • archive-darwin-amd64 (32 MB)
  • archive-darwin-arm64 (28 MB)
  • archive-linux-amd64 (19 MB)
  • archive-linux-arm64 (17 MB)
  • archive-windows-amd64 (31 MB)
  • archive-windows-arm64 (28 MB)
  • frontend-dist-pr (0 MB)
  • installer-dmg-darwin-amd64 (27 MB)
  • installer-dmg-darwin-arm64 (25 MB)
  • smart-mcp-proxymcpproxy-goYR97SU.dockerbuild (0 MB)

How to Download

Option 1: GitHub Web UI (easiest)

  1. Go to the workflow run page linked above
  2. Scroll to the bottom "Artifacts" section
  3. Click on the artifact you want to download

Option 2: GitHub CLI

gh run download 39398597766 --repo smart-mcp-proxy/mcpproxy-go

Note: Artifacts expire in 14 days.

@codecov-commenter

codecov-commenter commented Oct 20, 2026 •

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 90.00000% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/server/mcp.go 88.23% 1 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

Dumbris and others added 2 commits October 20, 2026 11:52
… block

Direct routing (#1685) returned applyOutputSanitisation's block result before
emitting the call's completion. It now emits the same error tool_call
(error_class proxy_policy, block message only) as the call_tool_* paths.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Dumbris
Dumbris enabled auto-merge (squash) October 20, 2026 09:17
@Dumbris
Dumbris merged commit e779def into main Oct 20, 2026
49 checks passed
Dumbris added a commit that referenced this pull request Oct 20, 2026
Follow-up to #1687: reattach the doc comment that the sanitiser-block change left detached from recordToolCallAt.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants