@roxybrowser/playwright-mcp 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +742 -0
- package/cli.js +18 -0
- package/config.d.ts +119 -0
- package/index.d.ts +23 -0
- package/index.js +19 -0
- package/lib/browserContextFactory.js +264 -0
- package/lib/browserServerBackend.js +77 -0
- package/lib/config.js +246 -0
- package/lib/context.js +242 -0
- package/lib/extension/cdpRelay.js +355 -0
- package/lib/extension/extensionContextFactory.js +54 -0
- package/lib/index.js +40 -0
- package/lib/loop/loop.js +69 -0
- package/lib/loop/loopClaude.js +152 -0
- package/lib/loop/loopOpenAI.js +141 -0
- package/lib/loop/main.js +60 -0
- package/lib/loopTools/context.js +67 -0
- package/lib/loopTools/main.js +54 -0
- package/lib/loopTools/perform.js +32 -0
- package/lib/loopTools/snapshot.js +29 -0
- package/lib/loopTools/tool.js +18 -0
- package/lib/mcp/http.js +120 -0
- package/lib/mcp/inProcessTransport.js +72 -0
- package/lib/mcp/proxyBackend.js +104 -0
- package/lib/mcp/server.js +123 -0
- package/lib/mcp/tool.js +29 -0
- package/lib/program.js +145 -0
- package/lib/response.js +165 -0
- package/lib/sessionLog.js +121 -0
- package/lib/tab.js +249 -0
- package/lib/tools/common.js +55 -0
- package/lib/tools/console.js +33 -0
- package/lib/tools/dialogs.js +47 -0
- package/lib/tools/evaluate.js +53 -0
- package/lib/tools/files.js +44 -0
- package/lib/tools/install.js +53 -0
- package/lib/tools/keyboard.js +78 -0
- package/lib/tools/mouse.js +99 -0
- package/lib/tools/navigate.js +70 -0
- package/lib/tools/network.js +41 -0
- package/lib/tools/pdf.js +40 -0
- package/lib/tools/roxy.js +50 -0
- package/lib/tools/screenshot.js +79 -0
- package/lib/tools/snapshot.js +139 -0
- package/lib/tools/tabs.js +87 -0
- package/lib/tools/tool.js +33 -0
- package/lib/tools/utils.js +74 -0
- package/lib/tools/wait.js +55 -0
- package/lib/tools.js +52 -0
- package/lib/utils/codegen.js +49 -0
- package/lib/utils/fileUtils.js +36 -0
- package/lib/utils/guid.js +22 -0
- package/lib/utils/log.js +21 -0
- package/lib/utils/manualPromise.js +111 -0
- package/lib/utils/package.js +20 -0
- package/lib/vscode/host.js +128 -0
- package/lib/vscode/main.js +62 -0
- package/package.json +79 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Microsoft Corporation.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
const model = 'gpt-4.1';
|
|
17
|
+
export class OpenAIDelegate {
|
|
18
|
+
_openai;
|
|
19
|
+
async openai() {
|
|
20
|
+
if (!this._openai) {
|
|
21
|
+
const oai = await import('openai');
|
|
22
|
+
this._openai = new oai.OpenAI();
|
|
23
|
+
}
|
|
24
|
+
return this._openai;
|
|
25
|
+
}
|
|
26
|
+
createConversation(task, tools, oneShot) {
|
|
27
|
+
const genericTools = tools.map(tool => ({
|
|
28
|
+
name: tool.name,
|
|
29
|
+
description: tool.description || '',
|
|
30
|
+
inputSchema: tool.inputSchema,
|
|
31
|
+
}));
|
|
32
|
+
if (!oneShot) {
|
|
33
|
+
genericTools.push({
|
|
34
|
+
name: 'done',
|
|
35
|
+
description: 'Call this tool when the task is complete.',
|
|
36
|
+
inputSchema: {
|
|
37
|
+
type: 'object',
|
|
38
|
+
properties: {},
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
messages: [{
|
|
44
|
+
role: 'user',
|
|
45
|
+
content: task
|
|
46
|
+
}],
|
|
47
|
+
tools: genericTools,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
async makeApiCall(conversation) {
|
|
51
|
+
// Convert generic messages to OpenAI format
|
|
52
|
+
const openaiMessages = [];
|
|
53
|
+
for (const message of conversation.messages) {
|
|
54
|
+
if (message.role === 'user') {
|
|
55
|
+
openaiMessages.push({
|
|
56
|
+
role: 'user',
|
|
57
|
+
content: message.content
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
else if (message.role === 'assistant') {
|
|
61
|
+
const toolCalls = [];
|
|
62
|
+
if (message.toolCalls) {
|
|
63
|
+
for (const toolCall of message.toolCalls) {
|
|
64
|
+
toolCalls.push({
|
|
65
|
+
id: toolCall.id,
|
|
66
|
+
type: 'function',
|
|
67
|
+
function: {
|
|
68
|
+
name: toolCall.name,
|
|
69
|
+
arguments: JSON.stringify(toolCall.arguments)
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const assistantMessage = {
|
|
75
|
+
role: 'assistant'
|
|
76
|
+
};
|
|
77
|
+
if (message.content)
|
|
78
|
+
assistantMessage.content = message.content;
|
|
79
|
+
if (toolCalls.length > 0)
|
|
80
|
+
assistantMessage.tool_calls = toolCalls;
|
|
81
|
+
openaiMessages.push(assistantMessage);
|
|
82
|
+
}
|
|
83
|
+
else if (message.role === 'tool') {
|
|
84
|
+
openaiMessages.push({
|
|
85
|
+
role: 'tool',
|
|
86
|
+
tool_call_id: message.toolCallId,
|
|
87
|
+
content: message.content,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Convert generic tools to OpenAI format
|
|
92
|
+
const openaiTools = conversation.tools.map(tool => ({
|
|
93
|
+
type: 'function',
|
|
94
|
+
function: {
|
|
95
|
+
name: tool.name,
|
|
96
|
+
description: tool.description,
|
|
97
|
+
parameters: tool.inputSchema,
|
|
98
|
+
},
|
|
99
|
+
}));
|
|
100
|
+
const openai = await this.openai();
|
|
101
|
+
const response = await openai.chat.completions.create({
|
|
102
|
+
model,
|
|
103
|
+
messages: openaiMessages,
|
|
104
|
+
tools: openaiTools,
|
|
105
|
+
tool_choice: 'auto'
|
|
106
|
+
});
|
|
107
|
+
const message = response.choices[0].message;
|
|
108
|
+
// Extract tool calls and add assistant message to generic conversation
|
|
109
|
+
const toolCalls = message.tool_calls || [];
|
|
110
|
+
const genericToolCalls = toolCalls.map(toolCall => {
|
|
111
|
+
const functionCall = toolCall.function;
|
|
112
|
+
return {
|
|
113
|
+
name: functionCall.name,
|
|
114
|
+
arguments: JSON.parse(functionCall.arguments),
|
|
115
|
+
id: toolCall.id,
|
|
116
|
+
};
|
|
117
|
+
});
|
|
118
|
+
// Add assistant message to generic conversation
|
|
119
|
+
conversation.messages.push({
|
|
120
|
+
role: 'assistant',
|
|
121
|
+
content: message.content || '',
|
|
122
|
+
toolCalls: genericToolCalls.length > 0 ? genericToolCalls : undefined
|
|
123
|
+
});
|
|
124
|
+
return genericToolCalls;
|
|
125
|
+
}
|
|
126
|
+
addToolResults(conversation, results) {
|
|
127
|
+
for (const result of results) {
|
|
128
|
+
conversation.messages.push({
|
|
129
|
+
role: 'tool',
|
|
130
|
+
toolCallId: result.toolCallId,
|
|
131
|
+
content: result.content,
|
|
132
|
+
isError: result.isError,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
checkDoneToolCall(toolCall) {
|
|
137
|
+
if (toolCall.name === 'done')
|
|
138
|
+
return toolCall.arguments.result;
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
package/lib/loop/main.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Microsoft Corporation.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
/* eslint-disable no-console */
|
|
17
|
+
import path from 'path';
|
|
18
|
+
import url from 'url';
|
|
19
|
+
import dotenv from 'dotenv';
|
|
20
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
21
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
22
|
+
import { program } from 'commander';
|
|
23
|
+
import { OpenAIDelegate } from './loopOpenAI.js';
|
|
24
|
+
import { ClaudeDelegate } from './loopClaude.js';
|
|
25
|
+
import { runTask } from './loop.js';
|
|
26
|
+
dotenv.config();
|
|
27
|
+
const __filename = url.fileURLToPath(import.meta.url);
|
|
28
|
+
async function run(delegate) {
|
|
29
|
+
const transport = new StdioClientTransport({
|
|
30
|
+
command: 'node',
|
|
31
|
+
args: [
|
|
32
|
+
path.resolve(__filename, '../../../cli.js'),
|
|
33
|
+
'--save-session',
|
|
34
|
+
'--output-dir', path.resolve(__filename, '../../../sessions')
|
|
35
|
+
],
|
|
36
|
+
stderr: 'inherit',
|
|
37
|
+
env: process.env,
|
|
38
|
+
});
|
|
39
|
+
const client = new Client({ name: 'test', version: '1.0.0' });
|
|
40
|
+
await client.connect(transport);
|
|
41
|
+
await client.ping();
|
|
42
|
+
for (const task of tasks) {
|
|
43
|
+
const messages = await runTask(delegate, client, task);
|
|
44
|
+
for (const message of messages)
|
|
45
|
+
console.log(`${message.role}: ${message.content}`);
|
|
46
|
+
}
|
|
47
|
+
await client.close();
|
|
48
|
+
}
|
|
49
|
+
const tasks = [
|
|
50
|
+
'Open https://playwright.dev/',
|
|
51
|
+
];
|
|
52
|
+
program
|
|
53
|
+
.option('--model <model>', 'model to use')
|
|
54
|
+
.action(async (options) => {
|
|
55
|
+
if (options.model === 'claude')
|
|
56
|
+
await run(new ClaudeDelegate());
|
|
57
|
+
else
|
|
58
|
+
await run(new OpenAIDelegate());
|
|
59
|
+
});
|
|
60
|
+
void program.parseAsync(process.argv);
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Microsoft Corporation.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
17
|
+
import { contextFactory } from '../browserContextFactory.js';
|
|
18
|
+
import { BrowserServerBackend } from '../browserServerBackend.js';
|
|
19
|
+
import { Context as BrowserContext } from '../context.js';
|
|
20
|
+
import { runTask } from '../loop/loop.js';
|
|
21
|
+
import { OpenAIDelegate } from '../loop/loopOpenAI.js';
|
|
22
|
+
import { ClaudeDelegate } from '../loop/loopClaude.js';
|
|
23
|
+
import { InProcessTransport } from '../mcp/inProcessTransport.js';
|
|
24
|
+
import * as mcpServer from '../mcp/server.js';
|
|
25
|
+
import { packageJSON } from '../utils/package.js';
|
|
26
|
+
export class Context {
|
|
27
|
+
config;
|
|
28
|
+
_client;
|
|
29
|
+
_delegate;
|
|
30
|
+
constructor(config, client) {
|
|
31
|
+
this.config = config;
|
|
32
|
+
this._client = client;
|
|
33
|
+
if (process.env.OPENAI_API_KEY)
|
|
34
|
+
this._delegate = new OpenAIDelegate();
|
|
35
|
+
else if (process.env.ANTHROPIC_API_KEY)
|
|
36
|
+
this._delegate = new ClaudeDelegate();
|
|
37
|
+
else
|
|
38
|
+
throw new Error('No LLM API key found. Please set OPENAI_API_KEY or ANTHROPIC_API_KEY environment variable.');
|
|
39
|
+
}
|
|
40
|
+
static async create(config) {
|
|
41
|
+
const client = new Client({ name: 'Playwright Proxy', version: packageJSON.version });
|
|
42
|
+
const browserContextFactory = contextFactory(config);
|
|
43
|
+
const server = mcpServer.createServer('Playwright Subagent', packageJSON.version, new BrowserServerBackend(config, browserContextFactory), false);
|
|
44
|
+
await client.connect(new InProcessTransport(server));
|
|
45
|
+
await client.ping();
|
|
46
|
+
return new Context(config, client);
|
|
47
|
+
}
|
|
48
|
+
async runTask(task, oneShot = false) {
|
|
49
|
+
const messages = await runTask(this._delegate, this._client, task, oneShot);
|
|
50
|
+
const lines = [];
|
|
51
|
+
// Skip the first message, which is the user's task.
|
|
52
|
+
for (const message of messages.slice(1)) {
|
|
53
|
+
// Trim out all page snapshots.
|
|
54
|
+
if (!message.content.trim())
|
|
55
|
+
continue;
|
|
56
|
+
const index = oneShot ? -1 : message.content.indexOf('### Page state');
|
|
57
|
+
const trimmedContent = index === -1 ? message.content : message.content.substring(0, index);
|
|
58
|
+
lines.push(`[${message.role}]:`, trimmedContent);
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
content: [{ type: 'text', text: lines.join('\n') }],
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
async close() {
|
|
65
|
+
await BrowserContext.disposeAll();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Microsoft Corporation.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
import dotenv from 'dotenv';
|
|
17
|
+
import * as mcpServer from '../mcp/server.js';
|
|
18
|
+
import { packageJSON } from '../utils/package.js';
|
|
19
|
+
import { Context } from './context.js';
|
|
20
|
+
import { perform } from './perform.js';
|
|
21
|
+
import { snapshot } from './snapshot.js';
|
|
22
|
+
import { toMcpTool } from '../mcp/tool.js';
|
|
23
|
+
export async function runLoopTools(config) {
|
|
24
|
+
dotenv.config();
|
|
25
|
+
const serverBackendFactory = {
|
|
26
|
+
name: 'Playwright',
|
|
27
|
+
nameInConfig: 'playwright-loop',
|
|
28
|
+
version: packageJSON.version,
|
|
29
|
+
create: () => new LoopToolsServerBackend(config)
|
|
30
|
+
};
|
|
31
|
+
await mcpServer.start(serverBackendFactory, config.server);
|
|
32
|
+
}
|
|
33
|
+
class LoopToolsServerBackend {
|
|
34
|
+
_config;
|
|
35
|
+
_context;
|
|
36
|
+
_tools = [perform, snapshot];
|
|
37
|
+
constructor(config) {
|
|
38
|
+
this._config = config;
|
|
39
|
+
}
|
|
40
|
+
async initialize() {
|
|
41
|
+
this._context = await Context.create(this._config);
|
|
42
|
+
}
|
|
43
|
+
async listTools() {
|
|
44
|
+
return this._tools.map(tool => toMcpTool(tool.schema));
|
|
45
|
+
}
|
|
46
|
+
async callTool(name, args) {
|
|
47
|
+
const tool = this._tools.find(tool => tool.schema.name === name);
|
|
48
|
+
const parsedArguments = tool.schema.inputSchema.parse(args || {});
|
|
49
|
+
return await tool.handle(this._context, parsedArguments);
|
|
50
|
+
}
|
|
51
|
+
serverClosed() {
|
|
52
|
+
void this._context.close();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Microsoft Corporation.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
import { z } from 'zod';
|
|
17
|
+
import { defineTool } from './tool.js';
|
|
18
|
+
const performSchema = z.object({
|
|
19
|
+
task: z.string().describe('The task to perform with the browser'),
|
|
20
|
+
});
|
|
21
|
+
export const perform = defineTool({
|
|
22
|
+
schema: {
|
|
23
|
+
name: 'browser_perform',
|
|
24
|
+
title: 'Perform a task with the browser',
|
|
25
|
+
description: 'Perform a task with the browser. It can click, type, export, capture screenshot, drag, hover, select options, etc.',
|
|
26
|
+
inputSchema: performSchema,
|
|
27
|
+
type: 'destructive',
|
|
28
|
+
},
|
|
29
|
+
handle: async (context, params) => {
|
|
30
|
+
return await context.runTask(params.task);
|
|
31
|
+
},
|
|
32
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Microsoft Corporation.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
import { z } from 'zod';
|
|
17
|
+
import { defineTool } from './tool.js';
|
|
18
|
+
export const snapshot = defineTool({
|
|
19
|
+
schema: {
|
|
20
|
+
name: 'browser_snapshot',
|
|
21
|
+
title: 'Take a snapshot of the browser',
|
|
22
|
+
description: 'Take a snapshot of the browser to read what is on the page.',
|
|
23
|
+
inputSchema: z.object({}),
|
|
24
|
+
type: 'readOnly',
|
|
25
|
+
},
|
|
26
|
+
handle: async (context, params) => {
|
|
27
|
+
return await context.runTask('Capture browser snapshot', true);
|
|
28
|
+
},
|
|
29
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Microsoft Corporation.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
export function defineTool(tool) {
|
|
17
|
+
return tool;
|
|
18
|
+
}
|
package/lib/mcp/http.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Microsoft Corporation.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
import assert from 'assert';
|
|
17
|
+
import http from 'http';
|
|
18
|
+
import crypto from 'crypto';
|
|
19
|
+
import debug from 'debug';
|
|
20
|
+
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
|
21
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
22
|
+
import * as mcpServer from './server.js';
|
|
23
|
+
const testDebug = debug('pw:mcp:test');
|
|
24
|
+
export async function startHttpServer(config, abortSignal) {
|
|
25
|
+
const { host, port } = config;
|
|
26
|
+
const httpServer = http.createServer();
|
|
27
|
+
await new Promise((resolve, reject) => {
|
|
28
|
+
httpServer.on('error', reject);
|
|
29
|
+
abortSignal?.addEventListener('abort', () => {
|
|
30
|
+
httpServer.close();
|
|
31
|
+
reject(new Error('Aborted'));
|
|
32
|
+
});
|
|
33
|
+
httpServer.listen(port, host, () => {
|
|
34
|
+
resolve();
|
|
35
|
+
httpServer.removeListener('error', reject);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
return httpServer;
|
|
39
|
+
}
|
|
40
|
+
export function httpAddressToString(address) {
|
|
41
|
+
assert(address, 'Could not bind server socket');
|
|
42
|
+
if (typeof address === 'string')
|
|
43
|
+
return address;
|
|
44
|
+
const resolvedPort = address.port;
|
|
45
|
+
let resolvedHost = address.family === 'IPv4' ? address.address : `[${address.address}]`;
|
|
46
|
+
if (resolvedHost === '0.0.0.0' || resolvedHost === '[::]')
|
|
47
|
+
resolvedHost = 'localhost';
|
|
48
|
+
return `http://${resolvedHost}:${resolvedPort}`;
|
|
49
|
+
}
|
|
50
|
+
export async function installHttpTransport(httpServer, serverBackendFactory) {
|
|
51
|
+
const sseSessions = new Map();
|
|
52
|
+
const streamableSessions = new Map();
|
|
53
|
+
httpServer.on('request', async (req, res) => {
|
|
54
|
+
const url = new URL(`http://localhost${req.url}`);
|
|
55
|
+
if (url.pathname.startsWith('/sse'))
|
|
56
|
+
await handleSSE(serverBackendFactory, req, res, url, sseSessions);
|
|
57
|
+
else
|
|
58
|
+
await handleStreamable(serverBackendFactory, req, res, streamableSessions);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
async function handleSSE(serverBackendFactory, req, res, url, sessions) {
|
|
62
|
+
if (req.method === 'POST') {
|
|
63
|
+
const sessionId = url.searchParams.get('sessionId');
|
|
64
|
+
if (!sessionId) {
|
|
65
|
+
res.statusCode = 400;
|
|
66
|
+
return res.end('Missing sessionId');
|
|
67
|
+
}
|
|
68
|
+
const transport = sessions.get(sessionId);
|
|
69
|
+
if (!transport) {
|
|
70
|
+
res.statusCode = 404;
|
|
71
|
+
return res.end('Session not found');
|
|
72
|
+
}
|
|
73
|
+
return await transport.handlePostMessage(req, res);
|
|
74
|
+
}
|
|
75
|
+
else if (req.method === 'GET') {
|
|
76
|
+
const transport = new SSEServerTransport('/sse', res);
|
|
77
|
+
sessions.set(transport.sessionId, transport);
|
|
78
|
+
testDebug(`create SSE session: ${transport.sessionId}`);
|
|
79
|
+
await mcpServer.connect(serverBackendFactory, transport, false);
|
|
80
|
+
res.on('close', () => {
|
|
81
|
+
testDebug(`delete SSE session: ${transport.sessionId}`);
|
|
82
|
+
sessions.delete(transport.sessionId);
|
|
83
|
+
});
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
res.statusCode = 405;
|
|
87
|
+
res.end('Method not allowed');
|
|
88
|
+
}
|
|
89
|
+
async function handleStreamable(serverBackendFactory, req, res, sessions) {
|
|
90
|
+
const sessionId = req.headers['mcp-session-id'];
|
|
91
|
+
if (sessionId) {
|
|
92
|
+
const transport = sessions.get(sessionId);
|
|
93
|
+
if (!transport) {
|
|
94
|
+
res.statusCode = 404;
|
|
95
|
+
res.end('Session not found');
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
return await transport.handleRequest(req, res);
|
|
99
|
+
}
|
|
100
|
+
if (req.method === 'POST') {
|
|
101
|
+
const transport = new StreamableHTTPServerTransport({
|
|
102
|
+
sessionIdGenerator: () => crypto.randomUUID(),
|
|
103
|
+
onsessioninitialized: async (sessionId) => {
|
|
104
|
+
testDebug(`create http session: ${transport.sessionId}`);
|
|
105
|
+
await mcpServer.connect(serverBackendFactory, transport, true);
|
|
106
|
+
sessions.set(sessionId, transport);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
transport.onclose = () => {
|
|
110
|
+
if (!transport.sessionId)
|
|
111
|
+
return;
|
|
112
|
+
sessions.delete(transport.sessionId);
|
|
113
|
+
testDebug(`delete http session: ${transport.sessionId}`);
|
|
114
|
+
};
|
|
115
|
+
await transport.handleRequest(req, res);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
res.statusCode = 400;
|
|
119
|
+
res.end('Invalid request');
|
|
120
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Microsoft Corporation.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
export class InProcessTransport {
|
|
17
|
+
_server;
|
|
18
|
+
_serverTransport;
|
|
19
|
+
_connected = false;
|
|
20
|
+
constructor(server) {
|
|
21
|
+
this._server = server;
|
|
22
|
+
this._serverTransport = new InProcessServerTransport(this);
|
|
23
|
+
}
|
|
24
|
+
async start() {
|
|
25
|
+
if (this._connected)
|
|
26
|
+
throw new Error('InprocessTransport already started!');
|
|
27
|
+
await this._server.connect(this._serverTransport);
|
|
28
|
+
this._connected = true;
|
|
29
|
+
}
|
|
30
|
+
async send(message, options) {
|
|
31
|
+
if (!this._connected)
|
|
32
|
+
throw new Error('Transport not connected');
|
|
33
|
+
this._serverTransport._receiveFromClient(message);
|
|
34
|
+
}
|
|
35
|
+
async close() {
|
|
36
|
+
if (this._connected) {
|
|
37
|
+
this._connected = false;
|
|
38
|
+
this.onclose?.();
|
|
39
|
+
this._serverTransport.onclose?.();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
onclose;
|
|
43
|
+
onerror;
|
|
44
|
+
onmessage;
|
|
45
|
+
sessionId;
|
|
46
|
+
setProtocolVersion;
|
|
47
|
+
_receiveFromServer(message, extra) {
|
|
48
|
+
this.onmessage?.(message, extra);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
class InProcessServerTransport {
|
|
52
|
+
_clientTransport;
|
|
53
|
+
constructor(clientTransport) {
|
|
54
|
+
this._clientTransport = clientTransport;
|
|
55
|
+
}
|
|
56
|
+
async start() {
|
|
57
|
+
}
|
|
58
|
+
async send(message, options) {
|
|
59
|
+
this._clientTransport._receiveFromServer(message);
|
|
60
|
+
}
|
|
61
|
+
async close() {
|
|
62
|
+
this.onclose?.();
|
|
63
|
+
}
|
|
64
|
+
onclose;
|
|
65
|
+
onerror;
|
|
66
|
+
onmessage;
|
|
67
|
+
sessionId;
|
|
68
|
+
setProtocolVersion;
|
|
69
|
+
_receiveFromClient(message) {
|
|
70
|
+
this.onmessage?.(message);
|
|
71
|
+
}
|
|
72
|
+
}
|