Skip to content

feat(server): retrieve_tools filter diagnostics (spec 094) - #970

Merged
Dumbris merged 20 commits into
mainfrom
094-filter-diagnostics
Aug 10, 2026
Merged

feat(server): retrieve_tools filter diagnostics (spec 094)#970
Dumbris merged 20 commits into
mainfrom
094-filter-diagnostics

Conversation

@Dumbris

@Dumbris Dumbris commented Aug 10, 2026

Copy link
Copy Markdown
Member

Related #969

When annotation-based safety filters (read_only_only, exclude_destructive, exclude_open_world) remove query-matched tools from a retrieve_tools response, the response now carries a compact filter_diagnostics block — so a caller can distinguish "tool doesn't exist" from "tool exists but was withheld by a filter, and here's why". Directly addresses the field report in #969 (item 1 only; items 2–3 deferred per the reporter's own suggestion).

What's in the block (FR-003, normative)

"filter_diagnostics": {
  "matched_before_filters": 12,
  "omitted_total": 8,
  "omitted_by_filter": {
    "read_only_only": { "missing_annotation": 5, "explicit": 1 }
  },
  "suggestion": "Tools matched but lack upstream annotations required by read_only_only; publish annotations or retry without these filters."
}
  • Present only when an active filter omitted ≥1 candidate — otherwise the response is byte-identical to before (verified against frozen pre-feature goldens in both full and compact modes).
  • Counts only — never tool/server identities (leak-proof for the stale-quarantine-index edge case).
  • Missing-annotation vs explicitly-unsafe split per filter, first-failure attribution (read-only → destructive → open-world, preserving the readOnly-implies-non-destructive shortcut).
  • Identical block in full/compact modes and on the code-execution routing surface.
  • Also closes the discoverability gap that started the field report: the default retrieve_tools registration now exposes the three filter params (previously only the routing-mode registrations declared them), via a shared retrieveToolsAnnotationFilterOptions() helper so the three schemas can't drift.

Design provenance

Spec → plan → tasks each cross-model (Codex) reviewed to APPROVE before the next stage (spec: 4 rounds; plan: 3; tasks: 4). Implementation is strict TDD per specs/094-filter-diagnostics/tasks.md: frozen-oracle parity test (224 cases) → pre-feature response goldens → RED wall (every assertion observed failing) → implementation → registration helper. Note: the RED-wall commit 4b212bd16 intentionally does not compile in isolation (tests-before-implementation); the PR HEAD is green.

Testing

  • go test -race ./internal/server/... — ok (562s full suite)
  • go test -race ./internal/... — ok
  • golangci-lint run --config .github/.golangci.yml ./... — 0 issues
  • SC-002 byte-identity vs 6 frozen pre-feature goldens (full+compact × 3 conditions); SC-003 maximal-fixture ≤500-byte size test; menu-surface controlled-delta + cross-registration schema deep-compare; TOON isolation with a diagnostics-active case
  • Live end-to-end on an isolated instance (port 18972, tests/echo-rugpull-server unannotated fixture): filtered query returns the block with missing_annotation:1 + suggestion; unfiltered query returns the tool with no block; compact mode byte-identical block

Dumbris added 18 commits August 10, 2026 08:22
Related #969

Spec for a compact filter_diagnostics block explaining tools omitted by
annotation-based safety filters (read_only_only, exclude_destructive,
exclude_open_world): matched-before-filters count, per-filter omission
counts split by missing-vs-explicit annotations, bounded affected-server
list, one adaptive suggestion string. Absent on the happy path; identical
in full and compact modes. Items 2-3 of #969 explicitly out of scope.
…fer server names, window semantics

Related #969

All 8 required changes from the cross-model review applied: omitted_servers
deferred (stale-quarantine-index leak + unbounded size); FR-003 now carries
a normative filter_diagnostics schema with omission rules and reason
classification; candidate-window semantics replace the raw-limit claim
(index normalizes limit<=0 to 20, no backfill); cause-precedence suggestion
selection; FR-007 protects every existing field; all three retrieve_tools
registrations enumerated and the default schema gains the filter params;
FR-010 scoped to the /mcp code-execution routing response; SC-003 is an
exact 600-byte serialized bound.
…ortened suggestions, full test matrix

Related #969

Shortened FR-006 suggestion templates (163/149 chars worst case, filters
named once); parity test rebuilt around a frozen pre-refactor oracle + truth
table over the corrected 224-case domain; contract schema forbids zero-count
reason pairs; test matrix expanded (raw-byte presence/absence in both modes,
delete-key equivalence, menu-surface controlled-delta widening, cross-
registration schema deep-compare, diagnostics-active TOON case, locked+
filtered coexistence, scope non-contribution); quickstart uses a controlled
unannotated fixture; Go version and SC-003 byte figures corrected.
…003 split to characterization phase, dependency graph fixed

Related #969
…002)

shouldExclude now delegates to excludeReason, which additionally reports the
first filter responsible for an omission and whether the decisive annotation
hint was explicitly unsafe or simply missing. Filter semantics are unchanged:
the parity test embeds the pre-refactor shouldExclude body verbatim as an
independent oracle and exhausts all 224 cases (28 annotation states x 8 filter
combinations), asserting excluded/filterKey/explicit on every one.

Related #969
…c 094 T003)

Six goldens captured while the branch still has pre-feature behavior: the
no-filters, filters-with-zero-omissions and filters-with-omissions conditions,
each in full and compact serialization. They turn SC-002 into a byte-level
guarantee instead of a key-absence check — for the omissions condition the
comparison splices the filter_diagnostics member out of the raw bytes rather
than round-tripping through a decoder, so the remaining bytes are the
originals.

Regeneration is deliberate only: UPDATE_SPEC094_GOLDENS=1.

Related #969
…4 T004-T009)

Every behavior test for the feature, written against pre-feature code and
observed failing: presence/absence in both serialization modes, the normative
FR-003 byte shape, the candidate-window semantics (a non-positive caller limit
is normalized to 20 by the search layer, and visibility drops shrink the window
without backfill), the missing-vs-explicit reason split including the read-only
shortcut, suggestion selection over all seven filter subsets, zero-result and
locked-tool coexistence, the 500-byte bound, cross-surface parity, and TOON
isolation.

The package deliberately does not compile at this commit: the production types
land in T010.

Related #969
When an annotation filter withholds a query match, the response now carries a
compact block saying how many tools entered the candidate window, how many each
filter withheld, and whether the cause was a missing upstream annotation
(fixable) or an explicitly unsafe one (the filter working as intended), plus one
actionable suggestion. Counts and one string only — never tool or server
identities — which keeps the block leak-proof for a quarantined tool lingering
in a stale index and bounds it at 463 bytes in the worst reachable case.

The block is self-suppressing: with no filter active, or with filters that
omitted nothing, the response is byte-identical to before, so no configuration
knob is needed. All three retrieve_tools surfaces share the handler, so the
code-execution surface gets it for free.

Related #969
…rieve_tools registrations (spec 094 T011-T012)

The default registration never exposed read_only_only, exclude_destructive or
exclude_open_world, even though the handler has always honored them — agents on
that surface simply could not discover the filters, which is where the field
report's confusion began. All three registrations now build those parameters
from one helper, the same anti-drift pattern retrieveToolsDetailOption already
uses, and all three descriptions gain the diagnostics mention plus a fixed
caveat that the counts describe one call's candidate window rather than the
catalog.

The menu-surface golden stays frozen; its controlled delta is widened to permit
exactly these changes, and a new deep-compare test pins the three filter schemas
to each other so they cannot drift apart again.

Related #969
Ran the quickstart end-to-end on an isolated instance (port 18972, scratch
dirs, stopped by PID): unannotated fixture tools are withheld by
read_only_only with a missing_annotation count and the upstream-annotations
suggestion, the key is absent once the filter is dropped, and the block is
identical under detail=compact. No docs page enumerates the retrieve_tools
annotation-filter parameters, so none needed the filter_diagnostics mention.

Related #969
go test -race ./internal/server/... and ./internal/... are green, and
golangci-lint v2 reports no issues. Two cleanups on the way there: the
attribution oracle spells out "explicitly read-only" instead of negating a
conjunction (staticcheck QF1001), and the serialization-mode table drops a
redundant identity map.

Related #969
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 10, 2026

Copy link
Copy Markdown

Deploying mcpproxy-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: 6d1e758
Status: ✅  Deploy successful!
Preview URL: https://e562118e.mcpproxy-docs.pages.dev
Branch Preview URL: https://094-filter-diagnostics.mcpproxy-docs.pages.dev

View logs

@codecov-commenter

Copy link
Copy Markdown

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

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown

📦 Build Artifacts

Workflow Run: View Run
Branch: 094-filter-diagnostics

Available Artifacts

  • archive-darwin-amd64 (28 MB)
  • archive-darwin-arm64 (26 MB)
  • archive-linux-amd64 (17 MB)
  • archive-linux-arm64 (15 MB)
  • archive-windows-amd64 (28 MB)
  • archive-windows-arm64 (25 MB)
  • frontend-dist-pr (0 MB)
  • installer-dmg-darwin-amd64 (22 MB)
  • installer-dmg-darwin-arm64 (20 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 31365681290 --repo smart-mcp-proxy/mcpproxy-go

Note: Artifacts expire in 14 days.

…ts (Codex round 1)

The no-backfill test asked for a window of 20 against six matching hits, so the
whole match set fit and an implementation that refilled a visibility-dropped
slot from a lower-ranked hit would have passed unchanged. It now indexes six
tools of strictly increasing length (six distinct BM25 scores, unambiguous
ranking), takes a window of three, locks the top-ranked hit, and asserts both
that no rank-4+ tool takes the freed slot and that matched_before_filters
reports 2 rather than the search cap or the match set. Widening the window back
to six makes every one of those assertions fire.

The shape test only marshaled a hand-built struct, and every decoder here
ignores unknown fields, so a handler attaching tool names or servers would have
gone unnoticed. Added an assertion over a real handler response: the exact
member set of the block and of each per-filter entry, plus a byte comparison of
the whole block.

Related #969
Related #969

The six TestSpec094Goldens comparisons failed on windows-latest: autocrlf
checked the fixtures out with CRLF, appending \r to the expected bytes. The
existing testdata/*.golden.json rule does not cross into spec094/.
@Dumbris
Dumbris merged commit 9ba4442 into main Aug 10, 2026
51 checks passed
@Dumbris
Dumbris deleted the 094-filter-diagnostics branch August 10, 2026 17:10
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