From e0fb748cccf3fcaa741cfe33b2e69a82acdbab36 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 24 Jul 2025 15:25:32 -0700 Subject: [PATCH] chore: wire one tool in-process (#753) --- package-lock.json | 3 +- package.json | 2 +- src/loop/onetool.ts | 49 ++++++++++--------- src/mcp/inProcessTransport.ts | 92 +++++++++++++++++++++++++++++++++++ 4 files changed, 119 insertions(+), 27 deletions(-) create mode 100644 src/mcp/inProcessTransport.ts diff --git a/package-lock.json b/package-lock.json index 78dd9a7..2d5c524 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@modelcontextprotocol/sdk": "^1.16.0", "commander": "^13.1.0", "debug": "^4.4.1", + "dotenv": "^17.2.0", "mime": "^4.0.7", "playwright": "1.55.0-alpha-1752701791000", "playwright-core": "1.55.0-alpha-1752701791000", @@ -34,7 +35,6 @@ "@typescript-eslint/eslint-plugin": "^8.26.1", "@typescript-eslint/parser": "^8.26.1", "@typescript-eslint/utils": "^8.26.1", - "dotenv": "^17.2.0", "eslint": "^9.19.0", "eslint-plugin-import": "^2.31.0", "eslint-plugin-notice": "^1.0.0", @@ -1289,7 +1289,6 @@ "version": "17.2.0", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.0.tgz", "integrity": "sha512-Q4sgBT60gzd0BB0lSyYD3xM4YxrXA9y4uBDof1JNYGzOXrQdQ6yX+7XIAqoFOGQFOTK1D3Hts5OllpxMDZFONQ==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=12" diff --git a/package.json b/package.json index 5b47a4a..f6c3ba8 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "@modelcontextprotocol/sdk": "^1.16.0", "commander": "^13.1.0", "debug": "^4.4.1", + "dotenv": "^17.2.0", "mime": "^4.0.7", "playwright": "1.55.0-alpha-1752701791000", "playwright-core": "1.55.0-alpha-1752701791000", @@ -61,7 +62,6 @@ "@typescript-eslint/eslint-plugin": "^8.26.1", "@typescript-eslint/parser": "^8.26.1", "@typescript-eslint/utils": "^8.26.1", - "dotenv": "^17.2.0", "eslint": "^9.19.0", "eslint-plugin-import": "^2.31.0", "eslint-plugin-notice": "^1.0.0", diff --git a/src/loop/onetool.ts b/src/loop/onetool.ts index 9128e39..748de21 100644 --- a/src/loop/onetool.ts +++ b/src/loop/onetool.ts @@ -14,25 +14,23 @@ * limitations under the License. */ -import path from 'path'; -import url from 'url'; +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import dotenv from 'dotenv'; import { z } from 'zod'; -import { Client } from '@modelcontextprotocol/sdk/client/index.js'; -import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; -import { OpenAIDelegate } from './loopOpenAI.js'; -import { runTask } from './loop.js'; -import { packageJSON } from '../package.js'; +import { contextFactory } from '../browserContextFactory.js'; +import { BrowserServerBackend } from '../browserServerBackend.js'; +import { Context } from '../context.js'; +import { logUnhandledError } from '../log.js'; +import { InProcessTransport } from '../mcp/inProcessTransport.js'; +import * as mcpServer from '../mcp/server.js'; import * as mcpTransport from '../mcp/transport.js'; +import { packageJSON } from '../package.js'; +import { runTask } from './loop.js'; +import { OpenAIDelegate } from './loopOpenAI.js'; import type { FullConfig } from '../config.js'; import type { ServerBackend } from '../mcp/server.js'; -import type * as mcpServer from '../mcp/server.js'; - -const __filename = url.fileURLToPath(import.meta.url); - -const delegate = new OpenAIDelegate(); const oneToolSchema: mcpServer.ToolSchema = { name: 'browser', @@ -46,7 +44,7 @@ const oneToolSchema: mcpServer.ToolSchema = { export async function runOneTool(config: FullConfig) { dotenv.config(); - const serverBackendFactory = () => new OneToolServerBackend(); + const serverBackendFactory = () => new OneToolServerBackend(config); await mcpTransport.start(serverBackendFactory, config.server); } @@ -54,19 +52,17 @@ class OneToolServerBackend implements ServerBackend { readonly name = 'Playwright'; readonly version = packageJSON.version; private _innerClient: Client | undefined; + private _config: FullConfig; + + constructor(config: FullConfig) { + this._config = config; + } async initialize() { - const transport = new StdioClientTransport({ - command: 'node', - args: [ - path.resolve(__filename, '../../../cli.js'), - ], - stderr: 'inherit', - env: process.env as Record, - }); - const client = new Client({ name: 'Playwright Proxy', version: '1.0.0' }); - await client.connect(transport); + const browserContextFactory = contextFactory(this._config.browser); + const server = mcpServer.createServer(new BrowserServerBackend(this._config, browserContextFactory)); + await client.connect(new InProcessTransport(server)); await client.ping(); this._innerClient = client; } @@ -76,9 +72,14 @@ class OneToolServerBackend implements ServerBackend { } async callTool(schema: mcpServer.ToolSchema, parsedArguments: any): Promise { - const result = await runTask(delegate!, this._innerClient!, parsedArguments.task as string); + const delegate = new OpenAIDelegate(); + const result = await runTask(delegate, this._innerClient!, parsedArguments.task as string); return { content: [{ type: 'text', text: result }], }; } + + serverClosed() { + void Context.disposeAll().catch(logUnhandledError); + } } diff --git a/src/mcp/inProcessTransport.ts b/src/mcp/inProcessTransport.ts new file mode 100644 index 0000000..317cff1 --- /dev/null +++ b/src/mcp/inProcessTransport.ts @@ -0,0 +1,92 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import type { Transport, TransportSendOptions } from '@modelcontextprotocol/sdk/shared/transport.js'; +import type { JSONRPCMessage, MessageExtraInfo } from '@modelcontextprotocol/sdk/types.js'; + +export class InProcessTransport implements Transport { + private _server: Server; + private _serverTransport: InProcessServerTransport; + private _connected: boolean = false; + + constructor(server: Server) { + this._server = server; + this._serverTransport = new InProcessServerTransport(this); + } + + async start(): Promise { + if (this._connected) + throw new Error('InprocessTransport already started!'); + + await this._server.connect(this._serverTransport); + this._connected = true; + } + + async send(message: JSONRPCMessage, options?: TransportSendOptions): Promise { + if (!this._connected) + throw new Error('Transport not connected'); + + + this._serverTransport._receiveFromClient(message); + } + + async close(): Promise { + if (this._connected) { + this._connected = false; + this.onclose?.(); + this._serverTransport.onclose?.(); + } + } + + onclose?: (() => void) | undefined; + onerror?: ((error: Error) => void) | undefined; + onmessage?: ((message: JSONRPCMessage, extra?: MessageExtraInfo) => void) | undefined; + sessionId?: string | undefined; + setProtocolVersion?: ((version: string) => void) | undefined; + + _receiveFromServer(message: JSONRPCMessage, extra?: MessageExtraInfo): void { + this.onmessage?.(message, extra); + } +} + +class InProcessServerTransport implements Transport { + private _clientTransport: InProcessTransport; + + constructor(clientTransport: InProcessTransport) { + this._clientTransport = clientTransport; + } + + async start(): Promise { + } + + async send(message: JSONRPCMessage, options?: TransportSendOptions): Promise { + this._clientTransport._receiveFromServer(message); + } + + async close(): Promise { + this.onclose?.(); + } + + onclose?: (() => void) | undefined; + onerror?: ((error: Error) => void) | undefined; + onmessage?: ((message: JSONRPCMessage, extra?: MessageExtraInfo) => void) | undefined; + sessionId?: string | undefined; + setProtocolVersion?: ((version: string) => void) | undefined; + _receiveFromClient(message: JSONRPCMessage): void { + this.onmessage?.(message); + } +}