fix: disambiguate bridged tool names

This commit is contained in:
zm2231 2026-05-13 21:01:25 -04:00
parent bfe727150c
commit 89f5053c15
2 changed files with 40 additions and 11 deletions

View File

@ -163,26 +163,36 @@ export function selectServedServers(
}
export function encodeToolName(server: string, tool: string): string {
return `${server}${TOOL_SEPARATOR}${tool}`;
return `${encodeToolNamePart(server)}${TOOL_SEPARATOR}${encodeToolNamePart(tool)}`;
}
export function decodeToolName(
name: string,
servedServers: readonly Pick<ServedServer, 'name'>[]
): { server: string; tool: string } | undefined {
const sorted = [...servedServers].toSorted((a, b) => b.name.length - a.name.length);
for (const server of sorted) {
const prefix = `${server.name}${TOOL_SEPARATOR}`;
if (name.startsWith(prefix)) {
const tool = name.slice(prefix.length);
if (tool.length > 0) {
return { server: server.name, tool };
}
}
const separatorIndex = name.indexOf(TOOL_SEPARATOR);
if (separatorIndex === -1) {
return undefined;
}
const server = decodeToolNamePart(name.slice(0, separatorIndex));
const tool = decodeToolNamePart(name.slice(separatorIndex + TOOL_SEPARATOR.length));
if (tool.length === 0) {
return undefined;
}
if (servedServers.some((served) => served.name === server)) {
return { server, tool };
}
return undefined;
}
function encodeToolNamePart(value: string): string {
return value.replaceAll('%', '%25').replaceAll('_', '%5F');
}
function decodeToolNamePart(value: string): string {
return value.replaceAll('%5F', '_').replaceAll('%25', '%');
}
function describeTool(server: string, description: string | undefined): string | undefined {
if (!description) {
return `Tool from MCPorter server '${server}'.`;

View File

@ -39,12 +39,31 @@ describe('mcporter serve bridge', () => {
it('encodes and decodes namespaced tool names with longest-prefix matching', () => {
expect(encodeToolName('alpha', 'ping')).toBe('alpha__ping');
expect(decodeToolName('alpha-long__tool__with__separator', [{ name: 'alpha' }, { name: 'alpha-long' }])).toEqual({
expect(
decodeToolName('alpha-long__tool%5F%5Fwith%5F%5Fseparator', [{ name: 'alpha' }, { name: 'alpha-long' }])
).toEqual({
server: 'alpha-long',
tool: 'tool__with__separator',
});
});
it('escapes namespaced tool parts to avoid server/tool collisions', () => {
const first = encodeToolName('alpha', 'beta__ping');
const second = encodeToolName('alpha__beta', 'ping');
expect(first).toBe('alpha__beta%5F%5Fping');
expect(second).toBe('alpha%5F%5Fbeta__ping');
expect(first).not.toBe(second);
expect(decodeToolName(first, [{ name: 'alpha' }, { name: 'alpha__beta' }])).toEqual({
server: 'alpha',
tool: 'beta__ping',
});
expect(decodeToolName(second, [{ name: 'alpha' }, { name: 'alpha__beta' }])).toEqual({
server: 'alpha__beta',
tool: 'ping',
});
});
it('exposes daemon tools through a single MCP server', async () => {
const runtime = {
listTools: vi.fn().mockImplementation(async (server: string) => [