fix(mcplocal): wire-form tool names in gate and favourite-index prose #125
@@ -26,12 +26,21 @@ import type { ToolDefinition } from '../types.js';
|
|||||||
/** Per-session state key holding the presented-name → canonical-name map. */
|
/** Per-session state key holding the presented-name → canonical-name map. */
|
||||||
const RESOLVER_KEY = 'favourite-index:resolver';
|
const RESOLVER_KEY = 'favourite-index:resolver';
|
||||||
|
|
||||||
/** The load-bearing "prefer favourite/" instruction (see module doc). */
|
/**
|
||||||
|
* The load-bearing "prefer favourite" instruction (see module doc).
|
||||||
|
*
|
||||||
|
* Phrased in the WIRE form (underscore-joined): the HTTP boundary's
|
||||||
|
* WireNameCodec rewrites the presented `favourite/<tool>` and
|
||||||
|
* `all/<server>/<tool>` names to `favourite_<tool>` / `all_<server>_<tool>`
|
||||||
|
* before any client sees them, so prose naming the slash form would tell the
|
||||||
|
* model to call names that are not in its function list.
|
||||||
|
*/
|
||||||
export const FAVOURITE_INDEX_INSTRUCTION =
|
export const FAVOURITE_INDEX_INSTRUCTION =
|
||||||
'Tools are indexed in two namespaces. PREFER favourite/<tool> — a short ' +
|
'Tools are indexed in two namespaces. PREFER the favourite_-prefixed tools ' +
|
||||||
'curated list of the common tools that covers most tasks; reach for these ' +
|
'— a short curated list of the common tools that covers most tasks; reach ' +
|
||||||
'first. Use the full catalog under all/<server>/<tool> only if nothing in ' +
|
'for these first. Use the full catalog under the all_-prefixed names only ' +
|
||||||
"favourites fits. (read_prompts gives this project's own guidance.)";
|
"if nothing in favourites fits. (read_prompts gives this project's own " +
|
||||||
|
'guidance.)';
|
||||||
|
|
||||||
export interface FavouriteIndexConfig {
|
export interface FavouriteIndexConfig {
|
||||||
/** Canonical `server/tool` names to surface as favourites, in display order. */
|
/** Canonical `server/tool` names to surface as favourites, in display order. */
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import type { TagMatchResult } from '../../gate/tag-matcher.js';
|
|||||||
import { LlmPromptSelector, pickCompletionText, type ServerInfer } from '../../gate/llm-selector.js';
|
import { LlmPromptSelector, pickCompletionText, type ServerInfer } from '../../gate/llm-selector.js';
|
||||||
import type { ProviderRegistry } from '../../providers/registry.js';
|
import type { ProviderRegistry } from '../../providers/registry.js';
|
||||||
import { withTimeout, TimeoutError } from '../../util/with-timeout.js';
|
import { withTimeout, TimeoutError } from '../../util/with-timeout.js';
|
||||||
|
import { sanitizeWireName } from '../../util/wire-names.js';
|
||||||
|
|
||||||
/** Cap on the gate's LLM prompt-selection. A slow/thinking LLM must never block
|
/** Cap on the gate's LLM prompt-selection. A slow/thinking LLM must never block
|
||||||
* begin_session — on timeout we fall back to deterministic tag matching. */
|
* begin_session — on timeout we fall back to deterministic tag matching. */
|
||||||
@@ -115,13 +116,15 @@ export function createGatePlugin(config: GatePluginConfig = {}): ProxyModelPlugi
|
|||||||
);
|
);
|
||||||
parts.push(`\n${gateInstructions}`);
|
parts.push(`\n${gateInstructions}`);
|
||||||
|
|
||||||
// Append tool inventory (names only)
|
// Append tool inventory (names only). Sanitized to the wire charset:
|
||||||
|
// the boundary WireNameCodec serves `server_tool`, so prose listing the
|
||||||
|
// canonical `server/tool` would name functions the model cannot call.
|
||||||
try {
|
try {
|
||||||
const tools = await ctx.discoverTools();
|
const tools = await ctx.discoverTools();
|
||||||
if (tools.length > 0) {
|
if (tools.length > 0) {
|
||||||
parts.push('\nAvailable MCP server tools (accessible after begin_session):');
|
parts.push('\nAvailable MCP server tools (accessible after begin_session):');
|
||||||
for (const t of tools) {
|
for (const t of tools) {
|
||||||
parts.push(` ${t.name}`);
|
parts.push(` ${sanitizeWireName(t.name)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@@ -424,13 +427,14 @@ async function handleBeginSession(
|
|||||||
);
|
);
|
||||||
responseParts.push(encouragement);
|
responseParts.push(encouragement);
|
||||||
|
|
||||||
// Append tool inventory (names only)
|
// Append tool inventory (names only) — wire charset, see the initialize
|
||||||
|
// inventory note.
|
||||||
try {
|
try {
|
||||||
const tools = await ctx.discoverTools();
|
const tools = await ctx.discoverTools();
|
||||||
if (tools.length > 0) {
|
if (tools.length > 0) {
|
||||||
responseParts.push('\nAvailable MCP server tools:');
|
responseParts.push('\nAvailable MCP server tools:');
|
||||||
for (const t of tools) {
|
for (const t of tools) {
|
||||||
responseParts.push(` ${t.name}`);
|
responseParts.push(` ${sanitizeWireName(t.name)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -525,8 +525,11 @@ describe('McpRouter gating', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const result = res.result as { instructions: string };
|
const result = res.result as { instructions: string };
|
||||||
expect(result.instructions).toContain('ha/get_entities');
|
// Wire form: prose must name what the client can actually call — the
|
||||||
expect(result.instructions).toContain('node-red/get_flows');
|
// boundary WireNameCodec serves underscore-joined names.
|
||||||
|
expect(result.instructions).toContain('ha_get_entities');
|
||||||
|
expect(result.instructions).toContain('node-red_get_flows');
|
||||||
|
expect(result.instructions).not.toContain('ha/get_entities');
|
||||||
expect(result.instructions).toContain('after begin_session');
|
expect(result.instructions).toContain('after begin_session');
|
||||||
// Descriptions should NOT be in init instructions (names only)
|
// Descriptions should NOT be in init instructions (names only)
|
||||||
expect(result.instructions).not.toContain('Get all entities');
|
expect(result.instructions).not.toContain('Get all entities');
|
||||||
@@ -544,7 +547,9 @@ describe('McpRouter gating', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const text = (res.result as { content: Array<{ text: string }> }).content[0]!.text;
|
const text = (res.result as { content: Array<{ text: string }> }).content[0]!.text;
|
||||||
expect(text).toContain('ha/get_entities');
|
// Wire form in the inventory (see the initialize-instructions test).
|
||||||
|
expect(text).toContain('ha_get_entities');
|
||||||
|
expect(text).not.toContain('ha/get_entities');
|
||||||
expect(text).not.toContain('Get all entities');
|
expect(text).not.toContain('Get all entities');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user