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
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:
30
src/mcpd/tests/tool-usage.test.ts
Normal file
30
src/mcpd/tests/tool-usage.test.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { canonicalizeToolName } from '../src/repositories/audit-event.repository.js';
|
||||
|
||||
describe('canonicalizeToolName — normalize presented tool names for usage ranking', () => {
|
||||
it('strips all/ back to canonical server/tool and derives the server', () => {
|
||||
expect(canonicalizeToolName('all/k8s/get_pods', null)).toEqual({ key: 'k8s/get_pods', server: 'k8s' });
|
||||
});
|
||||
|
||||
it('keeps favourite/ distinct (already a pin), using the trace server if any', () => {
|
||||
expect(canonicalizeToolName('favourite/get_pods', null)).toEqual({ key: 'favourite/get_pods', server: null });
|
||||
});
|
||||
|
||||
it('passes a canonical server/tool through, deriving the server from the name', () => {
|
||||
expect(canonicalizeToolName('vault/read_secret', 'vault')).toEqual({ key: 'vault/read_secret', server: 'vault' });
|
||||
});
|
||||
|
||||
it('handles a bare (non-namespaced) tool name', () => {
|
||||
expect(canonicalizeToolName('read_prompts', null)).toEqual({ key: 'read_prompts', server: null });
|
||||
});
|
||||
|
||||
it('handles all/ where the tool name itself contains slashes', () => {
|
||||
expect(canonicalizeToolName('all/srv/a/b', null)).toEqual({ key: 'srv/a/b', server: 'srv' });
|
||||
});
|
||||
|
||||
it('all/<server>/<tool> and <server>/<tool> collapse to the same key', () => {
|
||||
const a = canonicalizeToolName('all/gitea/create_pull_request', null);
|
||||
const b = canonicalizeToolName('gitea/create_pull_request', 'gitea');
|
||||
expect(a.key).toBe(b.key);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user