Files

144 lines
4.0 KiB
TypeScript
Raw Permalink Normal View History

import { expect, tap } from '@git.zone/tstest/tapbundle';
import {
createMcpTools,
formatMcpToolResult,
sanitizeMcpToolName,
type IMcpClientLike,
} from '../ts_mcp/index.js';
interface IFakeMcpClientState {
calls: Array<{ name: string; arguments?: Record<string, unknown> }>;
closed: boolean;
}
const createFakeMcpClient = (
toolName: string,
responsePrefix: string,
state: IFakeMcpClientState,
): IMcpClientLike => ({
listTools: async () => ({
tools: [
{
name: toolName,
description: `Runs ${toolName}`,
inputSchema: {
type: 'object',
properties: {
value: { type: 'string' },
},
required: ['value'],
},
},
],
} as any),
callTool: async (params) => {
state.calls.push(params);
return {
content: [
{
type: 'text',
text: `${responsePrefix}: ${String(params.arguments?.value ?? '')}`,
},
],
structuredContent: {
ok: true,
},
} as any;
},
close: async () => {
state.closed = true;
},
});
tap.test('sanitizeMcpToolName should produce AI SDK-safe tool names', async () => {
expect(sanitizeMcpToolName('read file/path')).toEqual('read_file_path');
expect(sanitizeMcpToolName('123-start')).toEqual('_123-start');
expect(sanitizeMcpToolName('***', 'fallback')).toEqual('fallback');
});
tap.test('createMcpTools should expose prefixed tools for multiple MCP servers', async () => {
const firstState: IFakeMcpClientState = { calls: [], closed: false };
const secondState: IFakeMcpClientState = { calls: [], closed: false };
const mcpTools = await createMcpTools({
servers: {
'server one': {
type: 'client',
client: createFakeMcpClient('echo tool', 'first', firstState),
},
'server/two': {
type: 'client',
client: createFakeMcpClient('echo tool', 'second', secondState),
},
},
});
expect(Object.keys(mcpTools.tools).sort()).toEqual([
'server_one__echo_tool',
'server_two__echo_tool',
]);
expect(mcpTools.toolNameMap.server_one__echo_tool).toEqual({
serverName: 'server one',
toolName: 'echo tool',
});
const output = await (mcpTools.tools.server_one__echo_tool as any).execute({ value: 'hello' });
expect(output).toInclude('first: hello');
expect(output).toInclude('Structured content');
expect(firstState.calls).toEqual([
{
name: 'echo tool',
arguments: { value: 'hello' },
},
]);
await mcpTools.close();
expect(firstState.closed).toBeTrue();
expect(secondState.closed).toBeTrue();
});
tap.test('createMcpTools should support paginated tool discovery and custom formatting', async () => {
const calls: Array<{ name: string; arguments?: Record<string, unknown> }> = [];
const client: IMcpClientLike = {
listTools: async (params) => ({
tools: [
{
name: params?.cursor ? 'second tool' : 'first tool',
inputSchema: { type: 'object' },
},
],
nextCursor: params?.cursor ? undefined : 'next',
} as any),
callTool: async (params) => {
calls.push(params);
return { content: [{ type: 'text', text: 'ok' }] } as any;
},
};
const mcpTools = await createMcpTools({
servers: {
only: { type: 'client', client },
},
formatResult: (_result, context) => context.exposedToolName,
});
expect(Object.keys(mcpTools.tools).sort()).toEqual(['first_tool', 'second_tool']);
const output = await (mcpTools.tools.second_tool as any).execute('ignored');
expect(output).toEqual('second_tool');
expect(calls).toEqual([
{
name: 'second tool',
arguments: {},
},
]);
});
tap.test('formatMcpToolResult should handle MCP result shapes', async () => {
expect(formatMcpToolResult({ toolResult: { ok: true } } as any)).toEqual({ ok: true });
expect(formatMcpToolResult({ content: [{ type: 'text', text: 'failed' }], isError: true } as any)).toInclude(
'MCP tool returned an error',
);
});
export default tap.start();