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

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>
This commit is contained in:
Michal
2026-07-23 01:20:30 +01:00
parent 574fc63bb1
commit f614e9bb98
25 changed files with 919 additions and 14 deletions

View File

@@ -0,0 +1,31 @@
import { describe, it, expect } from 'vitest';
import { buildFavouriteIndex } from '../src/commands/create.js';
describe('buildFavouriteIndex — CLI flags → project favouriteIndex', () => {
it('returns undefined when no favourite flag is given', () => {
expect(buildFavouriteIndex({})).toBeUndefined();
});
it('enables and sets the pins when --favourite is given', () => {
expect(buildFavouriteIndex({ favourite: ['k8s/get_pods', 'vault/read_secret'] })).toEqual({
enabled: true,
tools: ['k8s/get_pods', 'vault/read_secret'],
});
});
it('--no-favourite-index disables', () => {
expect(buildFavouriteIndex({ favouriteIndex: false })).toEqual({ enabled: false });
});
it('parses --max-favourites into an integer', () => {
expect(buildFavouriteIndex({ favouriteIndex: true, maxFavourites: '15' })).toEqual({
enabled: true,
maxFavourites: 15,
});
});
it('throws on a non-positive-integer --max-favourites', () => {
expect(() => buildFavouriteIndex({ maxFavourites: 'abc' })).toThrow(/max-favourites/);
expect(() => buildFavouriteIndex({ maxFavourites: '0' })).toThrow(/max-favourites/);
});
});