137 lines
4.0 KiB
TypeScript
137 lines
4.0 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import type { CallResult } from '../src/index.js';
|
|
import { createServerProxy } from '../src/index.js';
|
|
import type { Runtime, ServerToolInfo } from '../src/runtime.js';
|
|
|
|
type CallLogEntry = {
|
|
server: string;
|
|
tool: string;
|
|
options: unknown;
|
|
};
|
|
|
|
function createComposableRuntime() {
|
|
const listCalls: Array<{ server: string; options?: unknown }> = [];
|
|
const callLog: CallLogEntry[] = [];
|
|
|
|
const schemas: Record<string, ServerToolInfo[]> = {
|
|
docs: [
|
|
{
|
|
name: 'lookup',
|
|
description: 'Lookup documentation entries',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: { query: { type: 'string' } },
|
|
required: ['query'],
|
|
},
|
|
},
|
|
],
|
|
summarize: [
|
|
{
|
|
name: 'summarize',
|
|
description: 'Summarize content',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
text: { type: 'string' },
|
|
style: { type: 'string', default: 'concise' },
|
|
},
|
|
required: ['text'],
|
|
},
|
|
},
|
|
],
|
|
};
|
|
|
|
const runtime: Pick<Runtime, 'listTools' | 'callTool' | 'getDefinition'> & {
|
|
listCalls: typeof listCalls;
|
|
callLog: typeof callLog;
|
|
} = {
|
|
listCalls,
|
|
callLog,
|
|
async listTools(server, options) {
|
|
listCalls.push({ server, options });
|
|
return schemas[server] ?? [];
|
|
},
|
|
async callTool(server, toolName, options) {
|
|
callLog.push({ server, tool: toolName, options });
|
|
|
|
if (server === 'docs' && toolName === 'lookup') {
|
|
const query =
|
|
typeof options?.args === 'object' && options?.args !== null
|
|
? (options.args as { query?: string }).query
|
|
: undefined;
|
|
return {
|
|
content: [
|
|
{
|
|
type: 'text',
|
|
text: `Reference entry for ${query ?? 'unknown topic'}`,
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
if (server === 'summarize' && toolName === 'summarize') {
|
|
const args = (options?.args ?? {}) as { text?: string; style?: string };
|
|
return {
|
|
structuredContent: {
|
|
summary: `Summary: ${(args.text ?? '').slice(0, 24)}`,
|
|
style: args.style ?? 'concise',
|
|
},
|
|
};
|
|
}
|
|
|
|
throw new Error(`Unexpected call: ${server}.${toolName}`);
|
|
},
|
|
getDefinition(server) {
|
|
return {
|
|
name: server,
|
|
description: '',
|
|
command: {
|
|
kind: 'stdio' as const,
|
|
command: 'noop',
|
|
args: [],
|
|
cwd: process.cwd(),
|
|
},
|
|
};
|
|
},
|
|
};
|
|
|
|
return runtime;
|
|
}
|
|
|
|
describe('index exports integration', () => {
|
|
it('composes proxies across MCP servers using the TypeScript API', async () => {
|
|
const runtime = createComposableRuntime();
|
|
const docs = createServerProxy(runtime as unknown as Runtime, 'docs') as Record<string, unknown>;
|
|
const summarize = createServerProxy(runtime as unknown as Runtime, 'summarize') as Record<string, unknown>;
|
|
|
|
const lookup = docs.lookup as (query: string) => Promise<CallResult>;
|
|
const summarizeTool = summarize.summarize as (text: string) => Promise<CallResult>;
|
|
|
|
const lookupResult = await lookup('TypeScript decorators');
|
|
expect(lookupResult.text()).toBe('Reference entry for TypeScript decorators');
|
|
|
|
const summaryResult = await summarizeTool(lookupResult.text() ?? '');
|
|
const structured = summaryResult.structuredContent() as { summary?: string; style?: string };
|
|
expect(structured?.style).toBe('concise');
|
|
expect(structured?.summary).toMatch(/^Summary: Reference entry/);
|
|
|
|
expect(runtime.listCalls).toEqual([
|
|
{ server: 'docs', options: { includeSchema: true } },
|
|
{ server: 'summarize', options: { includeSchema: true } },
|
|
]);
|
|
|
|
expect(runtime.callLog).toEqual([
|
|
{
|
|
server: 'docs',
|
|
tool: 'lookup',
|
|
options: { args: { query: 'TypeScript decorators' } },
|
|
},
|
|
{
|
|
server: 'summarize',
|
|
tool: 'summarize',
|
|
options: { args: { text: 'Reference entry for TypeScript decorators', style: 'concise' } },
|
|
},
|
|
]);
|
|
});
|
|
});
|