diff --git a/src/browserServerBackend.ts b/src/browserServerBackend.ts index 0c20a37..b840d84 100644 --- a/src/browserServerBackend.ts +++ b/src/browserServerBackend.ts @@ -22,6 +22,7 @@ import { Response } from './response.js'; import { SessionLog } from './sessionLog.js'; import { filteredTools } from './tools.js'; import { packageJSON } from './package.js'; +import { toToolDefinition } from './tools/tool.js'; import type { Tool } from './tools/tool.js'; import type { BrowserContextFactory } from './browserContextFactory.js'; @@ -65,15 +66,15 @@ export class BrowserServerBackend implements ServerBackend { }); } - tools(): mcpServer.ToolSchema[] { - return this._tools.map(tool => tool.schema); + tools(): mcpServer.ToolDefinition[] { + return this._tools.map(tool => toToolDefinition(tool.schema)); } - async callTool(schema: mcpServer.ToolSchema, rawArguments: any) { + async callTool(name: string, rawArguments: any) { + const tool = this._tools.find(tool => tool.schema.name === name)!; + const parsedArguments = tool.schema.inputSchema.parse(rawArguments || {}); const context = this._context!; - const parsedArguments = schema.inputSchema.parse(rawArguments || {}); - const response = new Response(context, schema.name, parsedArguments); - const tool = this._tools.find(tool => tool.schema.name === schema.name)!; + const response = new Response(context, name, parsedArguments); context.setRunningTool(true); try { await tool.handle(context, parsedArguments, response); diff --git a/src/loopTools/DEPS.list b/src/loopTools/DEPS.list index 2da9aac..07786f2 100644 --- a/src/loopTools/DEPS.list +++ b/src/loopTools/DEPS.list @@ -2,3 +2,4 @@ ../ ../loop/ ../mcp/ +../tools/ diff --git a/src/loopTools/main.ts b/src/loopTools/main.ts index fc788aa..3d9f346 100644 --- a/src/loopTools/main.ts +++ b/src/loopTools/main.ts @@ -22,6 +22,7 @@ import { packageJSON } from '../package.js'; import { Context } from './context.js'; import { perform } from './perform.js'; import { snapshot } from './snapshot.js'; +import { toToolDefinition } from '../tools/tool.js'; import type { FullConfig } from '../config.js'; import type { ServerBackend } from '../mcp/server.js'; @@ -48,13 +49,13 @@ class LoopToolsServerBackend implements ServerBackend { this._context = await Context.create(this._config); } - tools(): mcpServer.ToolSchema[] { - return this._tools.map(tool => tool.schema); + tools(): mcpServer.ToolDefinition[] { + return this._tools.map(tool => toToolDefinition(tool.schema)); } - async callTool(schema: mcpServer.ToolSchema, rawArguments: any): Promise { - const tool = this._tools.find(tool => tool.schema.name === schema.name)!; - const parsedArguments = schema.inputSchema.parse(rawArguments || {}); + async callTool(name: string, rawArguments: any): Promise { + const tool = this._tools.find(tool => tool.schema.name === name)!; + const parsedArguments = tool.schema.inputSchema.parse(rawArguments || {}); return await tool.handle(this._context!, parsedArguments); } diff --git a/src/loopTools/tool.ts b/src/loopTools/tool.ts index 5399b08..27f1862 100644 --- a/src/loopTools/tool.ts +++ b/src/loopTools/tool.ts @@ -17,10 +17,11 @@ import type { z } from 'zod'; import type * as mcpServer from '../mcp/server.js'; import type { Context } from './context.js'; +import type { ToolSchema } from '../tools/tool.js'; export type Tool = { - schema: mcpServer.ToolSchema; + schema: ToolSchema; handle: (context: Context, params: z.output) => Promise; }; diff --git a/src/mcp/DEPS.list b/src/mcp/DEPS.list index 0ba24e6..9f8ba35 100644 --- a/src/mcp/DEPS.list +++ b/src/mcp/DEPS.list @@ -2,3 +2,6 @@ ../log.js ../manualPromise.js ../httpServer.js + +[proxyBackend.ts] +../package.js \ No newline at end of file diff --git a/src/mcp/proxyBackend.ts b/src/mcp/proxyBackend.ts index d90d1e2..983c833 100644 --- a/src/mcp/proxyBackend.ts +++ b/src/mcp/proxyBackend.ts @@ -15,12 +15,12 @@ */ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; - import { z } from 'zod'; -import { ServerBackend, ToolResponse, ToolSchema } from './server.js'; -import { defineTool, Tool } from '../tools/tool.js'; -import { packageJSON } from '../package.js'; +import { zodToJsonSchema } from 'zod-to-json-schema'; + import { logUnhandledError } from '../log.js'; +import { packageJSON } from '../package.js'; +import { ToolDefinition, ServerBackend, ToolResponse } from './server.js'; import type { Client } from '@modelcontextprotocol/sdk/client/index.js'; @@ -40,8 +40,8 @@ export class ProxyBackend implements ServerBackend { private _clientFactories: ClientFactoryList; private _currentClient: Client | undefined; - private _contextSwitchTool: Tool; - private _tools: ToolSchema[] = []; + private _contextSwitchTool: ToolDefinition; + private _tools: ToolDefinition[] = []; private _server: Server | undefined; constructor(clientFactories: ClientFactoryList) { @@ -54,20 +54,20 @@ export class ProxyBackend implements ServerBackend { await this._setCurrentClient(this._clientFactories[0]); } - tools(): ToolSchema[] { + tools(): ToolDefinition[] { if (this._clientFactories.length === 1) return this._tools; return [ ...this._tools, - this._contextSwitchTool.schema, + this._contextSwitchTool, ]; } - async callTool(schema: ToolSchema, rawArguments: any): Promise { - if (schema.name === this._contextSwitchTool.schema.name) + async callTool(name: string, rawArguments: any): Promise { + if (name === this._contextSwitchTool.name) return this._callContextSwitchTool(rawArguments); const result = await this._currentClient!.callTool({ - name: schema.name, + name, arguments: rawArguments, }); return result as unknown as ToolResponse; @@ -95,39 +95,28 @@ export class ProxyBackend implements ServerBackend { } } - private _defineContextSwitchTool(): Tool { - return defineTool({ - capability: 'core', - - schema: { - name: 'browser_connect', + private _defineContextSwitchTool(): ToolDefinition { + return { + name: 'browser_connect', + description: [ + 'Connect to a browser using one of the available methods:', + ...this._clientFactories.map(factory => `- "${factory.name}": ${factory.description}`), + ].join('\n'), + inputSchema: zodToJsonSchema(z.object({ + name: z.enum(this._clientFactories.map(factory => factory.name) as [string, ...string[]]).default(this._clientFactories[0].name).describe('The method to use to connect to the browser'), + }), { strictUnions: true }) as ToolDefinition['inputSchema'], + annotations: { title: 'Connect to a browser context', - description: [ - 'Connect to a browser using one of the available methods:', - ...this._clientFactories.map(factory => `- "${factory.name}": ${factory.description}`), - ].join('\n'), - inputSchema: z.object({ - name: z.enum(this._clientFactories.map(factory => factory.name) as [string, ...string[]]).default(this._clientFactories[0].name).describe('The method to use to connect to the browser'), - }), - type: 'readOnly', + readOnlyHint: true, + openWorldHint: false, }, - - async handle() { - throw new Error('Unreachable'); - } - }); + }; } private async _setCurrentClient(factory: ClientFactory) { await this._currentClient?.close(); this._currentClient = await factory.create(this._server!); const tools = await this._currentClient.listTools(); - this._tools = tools.tools.map(tool => ({ - name: tool.name, - title: tool.title ?? '', - description: tool.description ?? '', - inputSchema: tool.inputSchema ?? z.object({}), - type: tool.annotations?.readOnlyHint ? 'readOnly' as const : 'destructive' as const, - })); + this._tools = tools.tools; } } diff --git a/src/mcp/server.ts b/src/mcp/server.ts index f726f58..065298e 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -14,14 +14,12 @@ * limitations under the License. */ -import { z } from 'zod'; import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'; -import { zodToJsonSchema } from 'zod-to-json-schema'; import { ManualPromise } from '../manualPromise.js'; import { logUnhandledError } from '../log.js'; -import type { ImageContent, TextContent } from '@modelcontextprotocol/sdk/types.js'; +import type { ImageContent, TextContent, Tool } from '@modelcontextprotocol/sdk/types.js'; import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'; export type { Server } from '@modelcontextprotocol/sdk/server/index.js'; @@ -36,22 +34,14 @@ export type ToolResponse = { isError?: boolean; }; -export type ToolSchema = { - name: string; - title: string; - description: string; - inputSchema: Input; - type: 'readOnly' | 'destructive'; -}; - -export type ToolHandler = (toolName: string, params: any) => Promise; +export type ToolDefinition = Tool; export interface ServerBackend { name: string; version: string; initialize?(server: Server): Promise; - tools(): ToolSchema[]; - callTool(schema: ToolSchema, rawArguments: any): Promise; + tools(): ToolDefinition[]; + callTool(name: string, rawArguments: any): Promise; serverClosed?(): void; } @@ -73,17 +63,7 @@ export function createServer(backend: ServerBackend, runHeartbeat: boolean): Ser server.setRequestHandler(ListToolsRequestSchema, async () => { const tools = backend.tools(); - return { tools: tools.map(tool => ({ - name: tool.name, - description: tool.description, - inputSchema: tool.inputSchema instanceof z.ZodType ? zodToJsonSchema(tool.inputSchema) : tool.inputSchema, - annotations: { - title: tool.title, - readOnlyHint: tool.type === 'readOnly', - destructiveHint: tool.type === 'destructive', - openWorldHint: true, - }, - })) }; + return { tools }; }); let heartbeatRunning = false; @@ -100,12 +80,12 @@ export function createServer(backend: ServerBackend, runHeartbeat: boolean): Ser isError: true, }); const tools = backend.tools(); - const tool = tools.find(tool => tool.name === request.params.name) as ToolSchema; + const tool = tools.find(tool => tool.name === request.params.name); if (!tool) return errorResult(`Error: Tool "${request.params.name}" not found`); try { - return await backend.callTool(tool, request.params.arguments || {}); + return await backend.callTool(tool.name, request.params.arguments || {}); } catch (error) { return errorResult(String(error)); } diff --git a/src/tools/tool.ts b/src/tools/tool.ts index f0b7795..383aae6 100644 --- a/src/tools/tool.ts +++ b/src/tools/tool.ts @@ -14,13 +14,15 @@ * limitations under the License. */ +import { zodToJsonSchema } from 'zod-to-json-schema'; + import type { z } from 'zod'; import type { Context } from '../context.js'; import type * as playwright from 'playwright'; import type { ToolCapability } from '../../config.js'; import type { Tab } from '../tab.js'; import type { Response } from '../response.js'; -import type { ToolSchema } from '../mcp/server.js'; +import type { ToolDefinition } from '../mcp/server.js'; export type FileUploadModalState = { type: 'fileChooser'; @@ -36,6 +38,28 @@ export type DialogModalState = { export type ModalState = FileUploadModalState | DialogModalState; +export type ToolSchema = { + name: string; + title: string; + description: string; + inputSchema: Input; + type: 'readOnly' | 'destructive'; +}; + +export function toToolDefinition(tool: ToolSchema): ToolDefinition { + return { + name: tool.name, + description: tool.description, + inputSchema: zodToJsonSchema(tool.inputSchema, { strictUnions: true }) as ToolDefinition['inputSchema'], + annotations: { + title: tool.title, + readOnlyHint: tool.type === 'readOnly', + destructiveHint: tool.type === 'destructive', + openWorldHint: true, + }, + }; +} + export type Tool = { capability: ToolCapability; schema: ToolSchema;