Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions packages/mcp/src/server.test.ts
Original file line number Diff line number Diff line change
@@ -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()
}
})
})
19 changes: 19 additions & 0 deletions packages/mcp/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,28 @@ import { registerTools } from './tools'
import { registerVisionTools } from './tools/vision'
import { version } from './version'

export type PascalMcpToolExecutor = <Result>(input: {
name: string
execute: () => Promise<Result>
}) => Promise<Result>

export type CreatePascalMcpServerOptions = {
bridge: SceneBridge
operations?: SceneOperations
/** Required for persistence tools. Hosted apps and CLIs inject their own store. */
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 {
const server = new McpServer({
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)
Expand All @@ -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<typeof callback>) =>
executeTool({
name,
execute: () => Promise.resolve(Reflect.apply(callback, undefined, args)),
})) as typeof callback)
server.registerTool = wrappedRegisterTool
}
Loading