chore: tool definition without zod (#873)

This commit is contained in:
Yury Semikhatsky
2025-08-12 13:19:25 -07:00
committed by GitHub
parent 53c6b6dcb1
commit 7c4d67b3ae
8 changed files with 77 additions and 77 deletions

View File

@@ -22,6 +22,7 @@ import { Response } from './response.js';
import { SessionLog } from './sessionLog.js'; import { SessionLog } from './sessionLog.js';
import { filteredTools } from './tools.js'; import { filteredTools } from './tools.js';
import { packageJSON } from './package.js'; import { packageJSON } from './package.js';
import { toToolDefinition } from './tools/tool.js';
import type { Tool } from './tools/tool.js'; import type { Tool } from './tools/tool.js';
import type { BrowserContextFactory } from './browserContextFactory.js'; import type { BrowserContextFactory } from './browserContextFactory.js';
@@ -65,15 +66,15 @@ export class BrowserServerBackend implements ServerBackend {
}); });
} }
tools(): mcpServer.ToolSchema<any>[] { tools(): mcpServer.ToolDefinition[] {
return this._tools.map(tool => tool.schema); return this._tools.map(tool => toToolDefinition(tool.schema));
} }
async callTool(schema: mcpServer.ToolSchema<any>, 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 context = this._context!;
const parsedArguments = schema.inputSchema.parse(rawArguments || {}); const response = new Response(context, name, parsedArguments);
const response = new Response(context, schema.name, parsedArguments);
const tool = this._tools.find(tool => tool.schema.name === schema.name)!;
context.setRunningTool(true); context.setRunningTool(true);
try { try {
await tool.handle(context, parsedArguments, response); await tool.handle(context, parsedArguments, response);

View File

@@ -2,3 +2,4 @@
../ ../
../loop/ ../loop/
../mcp/ ../mcp/
../tools/

View File

@@ -22,6 +22,7 @@ import { packageJSON } from '../package.js';
import { Context } from './context.js'; import { Context } from './context.js';
import { perform } from './perform.js'; import { perform } from './perform.js';
import { snapshot } from './snapshot.js'; import { snapshot } from './snapshot.js';
import { toToolDefinition } from '../tools/tool.js';
import type { FullConfig } from '../config.js'; import type { FullConfig } from '../config.js';
import type { ServerBackend } from '../mcp/server.js'; import type { ServerBackend } from '../mcp/server.js';
@@ -48,13 +49,13 @@ class LoopToolsServerBackend implements ServerBackend {
this._context = await Context.create(this._config); this._context = await Context.create(this._config);
} }
tools(): mcpServer.ToolSchema<any>[] { tools(): mcpServer.ToolDefinition[] {
return this._tools.map(tool => tool.schema); return this._tools.map(tool => toToolDefinition(tool.schema));
} }
async callTool(schema: mcpServer.ToolSchema<any>, rawArguments: any): Promise<mcpServer.ToolResponse> { async callTool(name: string, rawArguments: any): Promise<mcpServer.ToolResponse> {
const tool = this._tools.find(tool => tool.schema.name === schema.name)!; const tool = this._tools.find(tool => tool.schema.name === name)!;
const parsedArguments = schema.inputSchema.parse(rawArguments || {}); const parsedArguments = tool.schema.inputSchema.parse(rawArguments || {});
return await tool.handle(this._context!, parsedArguments); return await tool.handle(this._context!, parsedArguments);
} }

View File

@@ -17,10 +17,11 @@
import type { z } from 'zod'; import type { z } from 'zod';
import type * as mcpServer from '../mcp/server.js'; import type * as mcpServer from '../mcp/server.js';
import type { Context } from './context.js'; import type { Context } from './context.js';
import type { ToolSchema } from '../tools/tool.js';
export type Tool<Input extends z.Schema = z.Schema> = { export type Tool<Input extends z.Schema = z.Schema> = {
schema: mcpServer.ToolSchema<Input>; schema: ToolSchema<Input>;
handle: (context: Context, params: z.output<Input>) => Promise<mcpServer.ToolResponse>; handle: (context: Context, params: z.output<Input>) => Promise<mcpServer.ToolResponse>;
}; };

View File

@@ -2,3 +2,6 @@
../log.js ../log.js
../manualPromise.js ../manualPromise.js
../httpServer.js ../httpServer.js
[proxyBackend.ts]
../package.js

View File

@@ -15,12 +15,12 @@
*/ */
import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { z } from 'zod'; import { z } from 'zod';
import { ServerBackend, ToolResponse, ToolSchema } from './server.js'; import { zodToJsonSchema } from 'zod-to-json-schema';
import { defineTool, Tool } from '../tools/tool.js';
import { packageJSON } from '../package.js';
import { logUnhandledError } from '../log.js'; 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'; import type { Client } from '@modelcontextprotocol/sdk/client/index.js';
@@ -40,8 +40,8 @@ export class ProxyBackend implements ServerBackend {
private _clientFactories: ClientFactoryList; private _clientFactories: ClientFactoryList;
private _currentClient: Client | undefined; private _currentClient: Client | undefined;
private _contextSwitchTool: Tool<any>; private _contextSwitchTool: ToolDefinition;
private _tools: ToolSchema<any>[] = []; private _tools: ToolDefinition[] = [];
private _server: Server | undefined; private _server: Server | undefined;
constructor(clientFactories: ClientFactoryList) { constructor(clientFactories: ClientFactoryList) {
@@ -54,20 +54,20 @@ export class ProxyBackend implements ServerBackend {
await this._setCurrentClient(this._clientFactories[0]); await this._setCurrentClient(this._clientFactories[0]);
} }
tools(): ToolSchema<any>[] { tools(): ToolDefinition[] {
if (this._clientFactories.length === 1) if (this._clientFactories.length === 1)
return this._tools; return this._tools;
return [ return [
...this._tools, ...this._tools,
this._contextSwitchTool.schema, this._contextSwitchTool,
]; ];
} }
async callTool(schema: ToolSchema<any>, rawArguments: any): Promise<ToolResponse> { async callTool(name: string, rawArguments: any): Promise<ToolResponse> {
if (schema.name === this._contextSwitchTool.schema.name) if (name === this._contextSwitchTool.name)
return this._callContextSwitchTool(rawArguments); return this._callContextSwitchTool(rawArguments);
const result = await this._currentClient!.callTool({ const result = await this._currentClient!.callTool({
name: schema.name, name,
arguments: rawArguments, arguments: rawArguments,
}); });
return result as unknown as ToolResponse; return result as unknown as ToolResponse;
@@ -95,39 +95,28 @@ export class ProxyBackend implements ServerBackend {
} }
} }
private _defineContextSwitchTool(): Tool<any> { private _defineContextSwitchTool(): ToolDefinition {
return defineTool({ return {
capability: 'core', name: 'browser_connect',
description: [
schema: { 'Connect to a browser using one of the available methods:',
name: 'browser_connect', ...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', title: 'Connect to a browser context',
description: [ readOnlyHint: true,
'Connect to a browser using one of the available methods:', openWorldHint: false,
...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',
}, },
};
async handle() {
throw new Error('Unreachable');
}
});
} }
private async _setCurrentClient(factory: ClientFactory) { private async _setCurrentClient(factory: ClientFactory) {
await this._currentClient?.close(); await this._currentClient?.close();
this._currentClient = await factory.create(this._server!); this._currentClient = await factory.create(this._server!);
const tools = await this._currentClient.listTools(); const tools = await this._currentClient.listTools();
this._tools = tools.tools.map(tool => ({ this._tools = tools.tools;
name: tool.name,
title: tool.title ?? '',
description: tool.description ?? '',
inputSchema: tool.inputSchema ?? z.object({}),
type: tool.annotations?.readOnlyHint ? 'readOnly' as const : 'destructive' as const,
}));
} }
} }

View File

@@ -14,14 +14,12 @@
* limitations under the License. * limitations under the License.
*/ */
import { z } from 'zod';
import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'; import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
import { zodToJsonSchema } from 'zod-to-json-schema';
import { ManualPromise } from '../manualPromise.js'; import { ManualPromise } from '../manualPromise.js';
import { logUnhandledError } from '../log.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'; import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
export type { Server } from '@modelcontextprotocol/sdk/server/index.js'; export type { Server } from '@modelcontextprotocol/sdk/server/index.js';
@@ -36,22 +34,14 @@ export type ToolResponse = {
isError?: boolean; isError?: boolean;
}; };
export type ToolSchema<Input extends z.Schema> = { export type ToolDefinition = Tool;
name: string;
title: string;
description: string;
inputSchema: Input;
type: 'readOnly' | 'destructive';
};
export type ToolHandler = (toolName: string, params: any) => Promise<ToolResponse>;
export interface ServerBackend { export interface ServerBackend {
name: string; name: string;
version: string; version: string;
initialize?(server: Server): Promise<void>; initialize?(server: Server): Promise<void>;
tools(): ToolSchema<any>[]; tools(): ToolDefinition[];
callTool(schema: ToolSchema<any>, rawArguments: any): Promise<ToolResponse>; callTool(name: string, rawArguments: any): Promise<ToolResponse>;
serverClosed?(): void; serverClosed?(): void;
} }
@@ -73,17 +63,7 @@ export function createServer(backend: ServerBackend, runHeartbeat: boolean): Ser
server.setRequestHandler(ListToolsRequestSchema, async () => { server.setRequestHandler(ListToolsRequestSchema, async () => {
const tools = backend.tools(); const tools = backend.tools();
return { tools: tools.map(tool => ({ return { tools };
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,
},
})) };
}); });
let heartbeatRunning = false; let heartbeatRunning = false;
@@ -100,12 +80,12 @@ export function createServer(backend: ServerBackend, runHeartbeat: boolean): Ser
isError: true, isError: true,
}); });
const tools = backend.tools(); const tools = backend.tools();
const tool = tools.find(tool => tool.name === request.params.name) as ToolSchema<any>; const tool = tools.find(tool => tool.name === request.params.name);
if (!tool) if (!tool)
return errorResult(`Error: Tool "${request.params.name}" not found`); return errorResult(`Error: Tool "${request.params.name}" not found`);
try { try {
return await backend.callTool(tool, request.params.arguments || {}); return await backend.callTool(tool.name, request.params.arguments || {});
} catch (error) { } catch (error) {
return errorResult(String(error)); return errorResult(String(error));
} }

View File

@@ -14,13 +14,15 @@
* limitations under the License. * limitations under the License.
*/ */
import { zodToJsonSchema } from 'zod-to-json-schema';
import type { z } from 'zod'; import type { z } from 'zod';
import type { Context } from '../context.js'; import type { Context } from '../context.js';
import type * as playwright from 'playwright'; import type * as playwright from 'playwright';
import type { ToolCapability } from '../../config.js'; import type { ToolCapability } from '../../config.js';
import type { Tab } from '../tab.js'; import type { Tab } from '../tab.js';
import type { Response } from '../response.js'; import type { Response } from '../response.js';
import type { ToolSchema } from '../mcp/server.js'; import type { ToolDefinition } from '../mcp/server.js';
export type FileUploadModalState = { export type FileUploadModalState = {
type: 'fileChooser'; type: 'fileChooser';
@@ -36,6 +38,28 @@ export type DialogModalState = {
export type ModalState = FileUploadModalState | DialogModalState; export type ModalState = FileUploadModalState | DialogModalState;
export type ToolSchema<Input extends z.Schema> = {
name: string;
title: string;
description: string;
inputSchema: Input;
type: 'readOnly' | 'destructive';
};
export function toToolDefinition(tool: ToolSchema<any>): 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<Input extends z.Schema = z.Schema> = { export type Tool<Input extends z.Schema = z.Schema> = {
capability: ToolCapability; capability: ToolCapability;
schema: ToolSchema<Input>; schema: ToolSchema<Input>;