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 }>; 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 }> = []; 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();