Skip to content

Commit 73adb0f

Browse files
authored
chore: steer towards mcp types a bit (#880)
1 parent 8572ab3 commit 73adb0f

File tree

10 files changed

+87
-141
lines changed

10 files changed

+87
-141
lines changed

src/browserServerBackend.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { Response } from './response.js';
2222
import { SessionLog } from './sessionLog.js';
2323
import { filteredTools } from './tools.js';
2424
import { packageJSON } from './utils/package.js';
25-
import { toToolDefinition } from './tools/tool.js';
25+
import { toMcpTool } from './tools/tool.js';
2626

2727
import type { Tool } from './tools/tool.js';
2828
import type { BrowserContextFactory } from './browserContextFactory.js';
@@ -64,12 +64,14 @@ export class BrowserServerBackend implements ServerBackend {
6464
});
6565
}
6666

67-
tools(): mcpServer.ToolDefinition[] {
68-
return this._tools.map(tool => toToolDefinition(tool.schema));
67+
async listTools(): Promise<mcpServer.Tool[]> {
68+
return this._tools.map(tool => toMcpTool(tool.schema));
6969
}
7070

71-
async callTool(name: string, rawArguments: any) {
71+
async callTool(name: string, rawArguments: mcpServer.CallToolRequest['params']['arguments']) {
7272
const tool = this._tools.find(tool => tool.schema.name === name)!;
73+
if (!tool)
74+
throw new Error(`Tool "${name}" not found`);
7375
const parsedArguments = tool.schema.inputSchema.parse(rawArguments || {});
7476
const context = this._context!;
7577
const response = new Response(context, name, parsedArguments);

src/inProcessMcpFactrory.ts

Lines changed: 0 additions & 45 deletions
This file was deleted.

src/loopTools/context.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export class Context {
5252
return new Context(config, client);
5353
}
5454

55-
async runTask(task: string, oneShot: boolean = false): Promise<mcpServer.ToolResponse> {
55+
async runTask(task: string, oneShot: boolean = false): Promise<mcpServer.CallToolResult> {
5656
const messages = await runTask(this._delegate, this._client!, task, oneShot);
5757
const lines: string[] = [];
5858

src/loopTools/main.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { packageJSON } from '../utils/package.js';
2222
import { Context } from './context.js';
2323
import { perform } from './perform.js';
2424
import { snapshot } from './snapshot.js';
25-
import { toToolDefinition } from '../tools/tool.js';
25+
import { toMcpTool } from '../tools/tool.js';
2626

2727
import type { FullConfig } from '../config.js';
2828
import type { ServerBackend } from '../mcp/server.js';
@@ -49,13 +49,13 @@ class LoopToolsServerBackend implements ServerBackend {
4949
this._context = await Context.create(this._config);
5050
}
5151

52-
tools(): mcpServer.ToolDefinition[] {
53-
return this._tools.map(tool => toToolDefinition(tool.schema));
52+
async listTools(): Promise<mcpServer.Tool[]> {
53+
return this._tools.map(tool => toMcpTool(tool.schema));
5454
}
5555

56-
async callTool(name: string, rawArguments: any): Promise<mcpServer.ToolResponse> {
56+
async callTool(name: string, args: mcpServer.CallToolRequest['params']['arguments']): Promise<mcpServer.CallToolResult> {
5757
const tool = this._tools.find(tool => tool.schema.name === name)!;
58-
const parsedArguments = tool.schema.inputSchema.parse(rawArguments || {});
58+
const parsedArguments = tool.schema.inputSchema.parse(args || {});
5959
return await tool.handle(this._context!, parsedArguments);
6060
}
6161

src/loopTools/tool.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import type { ToolSchema } from '../tools/tool.js';
2222

2323
export type Tool<Input extends z.Schema = z.Schema> = {
2424
schema: ToolSchema<Input>;
25-
handle: (context: Context, params: z.output<Input>) => Promise<mcpServer.ToolResponse>;
25+
handle: (context: Context, params: z.output<Input>) => Promise<mcpServer.CallToolResult>;
2626
};
2727

2828
export function defineTool<Input extends z.Schema>(tool: Tool<Input>): Tool<Input> {

src/mcp/proxyBackend.ts

Lines changed: 38 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -24,65 +24,67 @@ import { packageJSON } from '../utils/package.js';
2424

2525

2626
import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
27-
import type { ToolDefinition, ServerBackend, ToolResponse } from './server.js';
27+
import type { ServerBackend } from './server.js';
2828
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
29+
import type { Root, Tool, CallToolResult, CallToolRequest } from '@modelcontextprotocol/sdk/types.js';
2930

30-
type NonEmptyArray<T> = [T, ...T[]];
31-
32-
export type MCPFactory = {
31+
export type MCPProvider = {
3332
name: string;
3433
description: string;
35-
create(): Promise<Transport>;
34+
connect(): Promise<Transport>;
3635
};
3736

38-
export type MCPFactoryList = NonEmptyArray<MCPFactory>;
39-
4037
export class ProxyBackend implements ServerBackend {
4138
name = 'Playwright MCP Client Switcher';
4239
version = packageJSON.version;
4340

44-
private _mcpFactories: MCPFactoryList;
41+
private _mcpProviders: MCPProvider[];
4542
private _currentClient: Client | undefined;
46-
private _contextSwitchTool: ToolDefinition;
47-
private _tools: ToolDefinition[] = [];
48-
private _server: Server | undefined;
43+
private _contextSwitchTool: Tool;
44+
private _roots: Root[] = [];
4945

50-
constructor(clientFactories: MCPFactoryList) {
51-
this._mcpFactories = clientFactories;
46+
constructor(mcpProviders: MCPProvider[]) {
47+
this._mcpProviders = mcpProviders;
5248
this._contextSwitchTool = this._defineContextSwitchTool();
5349
}
5450

5551
async initialize(server: Server): Promise<void> {
56-
this._server = server;
57-
await this._setCurrentClient(this._mcpFactories[0]);
52+
const version = server.getClientVersion();
53+
const capabilities = server.getClientCapabilities();
54+
if (capabilities?.roots && version && clientsWithRoots.includes(version.name)) {
55+
const { roots } = await server.listRoots();
56+
this._roots = roots;
57+
}
58+
59+
await this._setCurrentClient(this._mcpProviders[0]);
5860
}
5961

60-
tools(): ToolDefinition[] {
61-
if (this._mcpFactories.length === 1)
62-
return this._tools;
62+
async listTools(): Promise<Tool[]> {
63+
const response = await this._currentClient!.listTools();
64+
if (this._mcpProviders.length === 1)
65+
return response.tools;
6366
return [
64-
...this._tools,
67+
...response.tools,
6568
this._contextSwitchTool,
6669
];
6770
}
6871

69-
async callTool(name: string, rawArguments: any): Promise<ToolResponse> {
72+
async callTool(name: string, args: CallToolRequest['params']['arguments']): Promise<CallToolResult> {
7073
if (name === this._contextSwitchTool.name)
71-
return this._callContextSwitchTool(rawArguments);
72-
const result = await this._currentClient!.callTool({
74+
return this._callContextSwitchTool(args);
75+
return await this._currentClient!.callTool({
7376
name,
74-
arguments: rawArguments,
75-
});
76-
return result as unknown as ToolResponse;
77+
arguments: args,
78+
}) as CallToolResult;
7779
}
7880

7981
serverClosed?(): void {
8082
void this._currentClient?.close().catch(logUnhandledError);
8183
}
8284

83-
private async _callContextSwitchTool(params: any): Promise<ToolResponse> {
85+
private async _callContextSwitchTool(params: any): Promise<CallToolResult> {
8486
try {
85-
const factory = this._mcpFactories.find(factory => factory.name === params.name);
87+
const factory = this._mcpProviders.find(factory => factory.name === params.name);
8688
if (!factory)
8789
throw new Error('Unknown connection method: ' + params.name);
8890

@@ -98,16 +100,16 @@ export class ProxyBackend implements ServerBackend {
98100
}
99101
}
100102

101-
private _defineContextSwitchTool(): ToolDefinition {
103+
private _defineContextSwitchTool(): Tool {
102104
return {
103105
name: 'browser_connect',
104106
description: [
105107
'Connect to a browser using one of the available methods:',
106-
...this._mcpFactories.map(factory => `- "${factory.name}": ${factory.description}`),
108+
...this._mcpProviders.map(factory => `- "${factory.name}": ${factory.description}`),
107109
].join('\n'),
108110
inputSchema: zodToJsonSchema(z.object({
109-
name: z.enum(this._mcpFactories.map(factory => factory.name) as [string, ...string[]]).default(this._mcpFactories[0].name).describe('The method to use to connect to the browser'),
110-
}), { strictUnions: true }) as ToolDefinition['inputSchema'],
111+
name: z.enum(this._mcpProviders.map(factory => factory.name) as [string, ...string[]]).default(this._mcpProviders[0].name).describe('The method to use to connect to the browser'),
112+
}), { strictUnions: true }) as Tool['inputSchema'],
111113
annotations: {
112114
title: 'Connect to a browser context',
113115
readOnlyHint: true,
@@ -116,7 +118,7 @@ export class ProxyBackend implements ServerBackend {
116118
};
117119
}
118120

119-
private async _setCurrentClient(factory: MCPFactory) {
121+
private async _setCurrentClient(factory: MCPProvider) {
120122
await this._currentClient?.close();
121123
this._currentClient = undefined;
122124

@@ -126,23 +128,13 @@ export class ProxyBackend implements ServerBackend {
126128
listRoots: true,
127129
},
128130
});
129-
client.setRequestHandler(ListRootsRequestSchema, async () => {
130-
const clientName = this._server!.getClientVersion()?.name;
131-
if (this._server!.getClientCapabilities()?.roots && (
132-
clientName === 'Visual Studio Code' ||
133-
clientName === 'Visual Studio Code - Insiders')) {
134-
const { roots } = await this._server!.listRoots();
135-
return { roots };
136-
}
137-
return { roots: [] };
138-
});
131+
client.setRequestHandler(ListRootsRequestSchema, () => ({ roots: this._roots }));
139132
client.setRequestHandler(PingRequestSchema, () => ({}));
140133

141-
const transport = await factory.create();
134+
const transport = await factory.connect();
142135
await client.connect(transport);
143-
144136
this._currentClient = client;
145-
const tools = await this._currentClient.listTools();
146-
this._tools = tools.tools;
147137
}
148138
}
139+
140+
const clientsWithRoots = ['Visual Studio Code', 'Visual Studio Code - Insiders'];

src/mcp/server.ts

Lines changed: 10 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -20,31 +20,19 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprot
2020
import { ManualPromise } from '../utils/manualPromise.js';
2121
import { logUnhandledError } from '../utils/log.js';
2222

23-
import type { ImageContent, TextContent, Tool } from '@modelcontextprotocol/sdk/types.js';
23+
import type { Tool, CallToolResult, CallToolRequest } from '@modelcontextprotocol/sdk/types.js';
2424
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
2525
export type { Server } from '@modelcontextprotocol/sdk/server/index.js';
26+
export type { Tool, CallToolResult, CallToolRequest } from '@modelcontextprotocol/sdk/types.js';
2627

2728
const serverDebug = debug('pw:mcp:server');
2829

29-
export type ClientCapabilities = {
30-
roots?: {
31-
listRoots?: boolean
32-
};
33-
};
34-
35-
export type ToolResponse = {
36-
content: (TextContent | ImageContent)[];
37-
isError?: boolean;
38-
};
39-
40-
export type ToolDefinition = Tool;
41-
4230
export interface ServerBackend {
4331
name: string;
4432
version: string;
4533
initialize?(server: Server): Promise<void>;
46-
tools(): ToolDefinition[];
47-
callTool(name: string, rawArguments: any): Promise<ToolResponse>;
34+
listTools(): Promise<Tool[]>;
35+
callTool(name: string, args: CallToolRequest['params']['arguments']): Promise<CallToolResult>;
4836
serverClosed?(): void;
4937
}
5038

@@ -66,7 +54,7 @@ export function createServer(backend: ServerBackend, runHeartbeat: boolean): Ser
6654

6755
server.setRequestHandler(ListToolsRequestSchema, async () => {
6856
serverDebug('listTools');
69-
const tools = backend.tools();
57+
const tools = await backend.listTools();
7058
return { tools };
7159
});
7260

@@ -80,19 +68,13 @@ export function createServer(backend: ServerBackend, runHeartbeat: boolean): Ser
8068
startHeartbeat(server);
8169
}
8270

83-
const errorResult = (...messages: string[]) => ({
84-
content: [{ type: 'text', text: '### Result\n' + messages.join('\n') }],
85-
isError: true,
86-
});
87-
const tools = backend.tools();
88-
const tool = tools.find(tool => tool.name === request.params.name);
89-
if (!tool)
90-
return errorResult(`Error: Tool "${request.params.name}" not found`);
91-
9271
try {
93-
return await backend.callTool(tool.name, request.params.arguments || {});
72+
return await backend.callTool(request.params.name, request.params.arguments || {});
9473
} catch (error) {
95-
return errorResult(String(error));
74+
return {
75+
content: [{ type: 'text', text: '### Result\n' + String(error) }],
76+
isError: true,
77+
};
9678
}
9779
});
9880
addServerListener(server, 'initialized', () => {

0 commit comments

Comments
 (0)