diff --git a/packages/mcp/src/server.test.ts b/packages/mcp/src/server.test.ts new file mode 100644 index 000000000..cadb9263b --- /dev/null +++ b/packages/mcp/src/server.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, test } from 'bun:test' +import { Client } from '@modelcontextprotocol/sdk/client/index.js' +import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js' +import { SceneBridge } from './bridge/scene-bridge' +import { createPascalMcpServer } from './server' + +describe('Pascal MCP tool execution', () => { + test('runs registered tools through the configured executor', async () => { + const bridge = new SceneBridge() + bridge.loadDefault() + const events: string[] = [] + const server = createPascalMcpServer({ + bridge, + executeTool: async ({ name, execute }) => { + events.push(`before:${name}`) + const result = await execute() + events.push(`after:${name}`) + return result + }, + }) + const [serverTransport, clientTransport] = InMemoryTransport.createLinkedPair() + const client = new Client({ name: 'tool-executor-test', version: '0.0.0' }) + await Promise.all([server.connect(serverTransport), client.connect(clientTransport)]) + + try { + const result = await client.callTool({ name: 'get_scene', arguments: {} }) + expect(result.isError).toBeFalsy() + expect(events).toEqual(['before:get_scene', 'after:get_scene']) + } finally { + await client.close() + await server.close() + } + }) +}) diff --git a/packages/mcp/src/server.ts b/packages/mcp/src/server.ts index 3cbda9ad8..4424dec82 100644 --- a/packages/mcp/src/server.ts +++ b/packages/mcp/src/server.ts @@ -8,6 +8,11 @@ import { registerTools } from './tools' import { registerVisionTools } from './tools/vision' import { version } from './version' +export type PascalMcpToolExecutor = (input: { + name: string + execute: () => Promise +}) => Promise + export type CreatePascalMcpServerOptions = { bridge: SceneBridge operations?: SceneOperations @@ -15,6 +20,8 @@ export type CreatePascalMcpServerOptions = { store?: SceneStore name?: string version?: string + /** Wrap every tool handler, for example to serialize access to a stateful bridge. */ + executeTool?: PascalMcpToolExecutor } export function createPascalMcpServer(opts: CreatePascalMcpServerOptions): McpServer { @@ -22,6 +29,7 @@ export function createPascalMcpServer(opts: CreatePascalMcpServerOptions): McpSe name: opts.name ?? 'pascal-mcp-server', version: opts.version ?? version, }) + if (opts.executeTool) installToolExecutor(server, opts.executeTool) const operations = opts.operations ?? createSceneOperations({ bridge: opts.bridge, store: opts.store }) registerTools(server, operations) @@ -30,3 +38,14 @@ export function createPascalMcpServer(opts: CreatePascalMcpServerOptions): McpSe registerPrompts(server, operations) return server } + +function installToolExecutor(server: McpServer, executeTool: PascalMcpToolExecutor): void { + const registerTool = server.registerTool.bind(server) + const wrappedRegisterTool: McpServer['registerTool'] = (name, config, callback) => + registerTool(name, config, ((...args: Parameters) => + executeTool({ + name, + execute: () => Promise.resolve(Reflect.apply(callback, undefined, args)), + })) as typeof callback) + server.registerTool = wrappedRegisterTool +}