feat(proxy): favourite-index tool presentation #86

Merged
michal merged 4 commits from feat/favourite-index-presentation into main 2026-07-23 21:28:23 +00:00
Owner

Implements the measured-winner tool presentation (favindex) from the DGX-Spark bake-off: curated favourite/<tool> shortlist + full all/<server>/<tool> catalog + a load-bearing prefer-favourite instruction (nearly halved wander, 2.5x first-pick vs flat catalog).

  • mcplocal plugin favourite-index.ts composed after gate (no-ops while gated); rewrites presented names -> canonical in onToolCallBefore so routing + content-pipeline still run.
  • compose onInitialize now concatenates instructions.
  • Per-project Project.favouriteIndex config; surfaced via discovery; wired at the endpoint.
  • Usage derivation: tool-usage ranking + mcpctl favourites suggest|list.
  • CLI create-project flags + apply/get round-trip; completions regenerated.
  • Unit + smoke tests; docs/tool-presentation.md.

Unit suites green: mcplocal 745, mcpd 954, cli 513.

NOT yet deployed — needs mcpd+mcplocal image rebuild + CLI RPM (schema adds a nullable Project.favouriteIndex column, auto-pushed on mcpd startup).

🤖 Generated with Claude Code

Implements the measured-winner tool presentation (favindex) from the DGX-Spark bake-off: curated `favourite/<tool>` shortlist + full `all/<server>/<tool>` catalog + a load-bearing prefer-favourite instruction (nearly halved wander, 2.5x first-pick vs flat catalog). - mcplocal plugin `favourite-index.ts` composed after gate (no-ops while gated); rewrites presented names -> canonical in onToolCallBefore so routing + content-pipeline still run. - compose onInitialize now concatenates instructions. - Per-project `Project.favouriteIndex` config; surfaced via discovery; wired at the endpoint. - Usage derivation: tool-usage ranking + `mcpctl favourites suggest|list`. - CLI create-project flags + apply/get round-trip; completions regenerated. - Unit + smoke tests; docs/tool-presentation.md. Unit suites green: mcplocal 745, mcpd 954, cli 513. NOT yet deployed — needs mcpd+mcplocal image rebuild + CLI RPM (schema adds a nullable Project.favouriteIndex column, auto-pushed on mcpd startup). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
michal added 1 commit 2026-07-23 00:21:36 +00:00
feat(proxy): favourite-index tool presentation (favourite/ + all/ + prefer instruction)
Some checks failed
CI/CD / lint (pull_request) Successful in 1m4s
CI/CD / test (pull_request) Successful in 1m23s
CI/CD / typecheck (pull_request) Successful in 2m36s
CI/CD / smoke (pull_request) Failing after 1m53s
CI/CD / build (pull_request) Successful in 4m16s
CI/CD / publish (pull_request) Has been skipped
f614e9bb98
Measured winner from the DGX-Spark bake-off (toolsim.py, 145-tool catalog): a
curated favourite/<tool> shortlist + the full all/<server>/<tool> catalog + a
load-bearing "prefer favourite/ first" instruction nearly halved wander (37→20)
and 2.5x'd first-pick (2→5/8) vs a flat catalog. The instruction is load-bearing;
enriching descriptions did not help.

- New mcplocal plugin `favourite-index.ts`: composes AFTER gate (no-ops while
  gated), reshapes the ungated upstream catalog into favourite/ + all/, injects
  the instruction (onInitialize), and rewrites presented names back to canonical
  server/tool in onToolCallBefore so normal routing + content-pipeline still run.
  Gate/agent virtual tools pass through untouched; favourites are upstream-only.
- compose.ts: onInitialize now concatenates plugin instructions (was first-non-null)
  so favindex can contribute its banner alongside the gate's.
- Per-project config `Project.favouriteIndex` {enabled, tools[], maxFavourites};
  surfaced to the proxy via discovery; wired at project-mcp-endpoint when enabled.
- Usage derivation: mcpd tool-usage ranking over tool_call_trace events
  (normalizing presented names → canonical), GET /api/v1/audit/tool-usage, and
  `mcpctl favourites suggest|list`.
- CLI: `create project` gains --favourite/--favourite-index/--max-favourites;
  favouriteIndex round-trips through get -o yaml | apply -f. Completions regenerated.
- Tests: plugin unit (presentation, rewrite routing, gated no-op, collisions),
  compose merge, canonicalizeToolName, buildFavouriteIndex, + a live smoke test.
- Docs: docs/tool-presentation.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
michal added 1 commit 2026-07-23 21:10:14 +00:00
fix(gate): stop masking backend errors + rescue thinking-model prompt selection
Some checks failed
CI/CD / lint (pull_request) Successful in 1m6s
CI/CD / typecheck (pull_request) Successful in 2m14s
CI/CD / test (pull_request) Successful in 1m22s
CI/CD / smoke (pull_request) Failing after 1m54s
CI/CD / build (pull_request) Successful in 4m17s
CI/CD / publish (pull_request) Has been skipped
9dbd2f175d
Two independent, live-reproduced causes of "Smart prompt-selection unavailable
(LLM response did not contain valid selection JSON)" in the gate:

- Cause B (openai.ts request): the response handler JSON.parsed the body with no
  res.statusCode check, so a LiteLLM/vLLM HTTP 500 ("Connection error") parsed as
  an empty completion and surfaced as generic "bad JSON", masking backend-down.
  Now reject on status >= 400 with "OpenAI HTTP <status>: <body>" so the selector
  reports the true degradedReason.
- Cause A (parseResponse + selector budget): a reasoning model (qwen3-thinking)
  spends its whole 1024-token budget in reasoning_content and emits content=null,
  so the {selectedNames} JSON never appears. parseResponse now falls back to
  reasoning_content (incl. provider_specific_fields) when content is empty, and
  the selector budget is raised 1024 -> 4000 (env MCPCTL_GATE_SELECT_MAX_TOKENS)
  so thinking + the JSON answer both fit.

Note: mcpd's chat path got a reasoning_content fallback in PR #67; this is the
separate mcplocal gate-selection provider, which never did. Tests: openai-provider
(500 rejects, reasoning_content capture/preference). mcplocal suite green (749).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
michal added 1 commit 2026-07-23 21:20:52 +00:00
fix(favourite-index): route favourite/ calls even before tools/list (lazy resolver)
Some checks failed
CI/CD / typecheck (pull_request) Successful in 1m4s
CI/CD / lint (pull_request) Successful in 2m12s
CI/CD / test (pull_request) Successful in 1m20s
CI/CD / smoke (pull_request) Failing after 1m54s
CI/CD / build (pull_request) Successful in 4m31s
CI/CD / publish (pull_request) Has been skipped
c9364a2f2a
The presented→canonical resolver was only built during tools/list, so a
favourite/<tool> call arriving before any list this session fell through to the
namespaced router and 404'd (all/ was fine — it prefix-strips). Real MCP clients
list first, but the smoke test's initialize→call flow exposed it. Extract
buildPresentation() and lazily rebuild the resolver in onToolCallBefore when a
favourite//all/ name isn't resolved yet. + unit test (favourite/ call with no
prior list) and realistic listTools() in the smoke routing case.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
michal added 1 commit 2026-07-23 21:23:41 +00:00
fix(providers): anthropic surfaces HTTP errors too (gate parity with openai)
Some checks failed
CI/CD / lint (pull_request) Successful in 1m6s
CI/CD / typecheck (pull_request) Successful in 1m5s
CI/CD / test (pull_request) Successful in 1m19s
CI/CD / build (pull_request) Successful in 2m14s
CI/CD / smoke (pull_request) Failing after 3m0s
CI/CD / publish (pull_request) Has been skipped
366b505c00
The local mcplocal gate resolves heavy=[anthropic]; its OAuth-gated key 404s the
configured opus-4 model, and anthropic.ts (like openai.ts before this series)
JSON.parsed the error body as a completion → empty content → the gate's
misleading "did not contain valid selection JSON". Reject on status >= 400 with
"Anthropic HTTP <status>: <body>" so the true reason (model-not-found / auth)
surfaces as the degradedReason. + parity test.

Note: this makes the error honest; it does not make the local gate SUCCEED — that
needs a working heavy provider (the anthropic OAuth token is fundamentally gated).
The qwen3-thinking gate path (k8s mcplocal) is addressed by the budget+reasoning
fix in the previous commit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
michal merged commit e75d2ba296 into main 2026-07-23 21:28:23 +00:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: michal/mcpctl#86