feat(mcp): add MCP tool integration via dedicated subpath export
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
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();
|
||||
Reference in New Issue
Block a user