feat(proxy): favourite-index tool presentation #86
Reference in New Issue
Block a user
Delete Branch "feat/favourite-index-presentation"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Implements the measured-winner tool presentation (favindex) from the DGX-Spark bake-off: curated
favourite/<tool>shortlist + fullall/<server>/<tool>catalog + a load-bearing prefer-favourite instruction (nearly halved wander, 2.5x first-pick vs flat catalog).favourite-index.tscomposed after gate (no-ops while gated); rewrites presented names -> canonical in onToolCallBefore so routing + content-pipeline still run.Project.favouriteIndexconfig; surfaced via discovery; wired at the endpoint.mcpctl favourites suggest|list.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
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>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>