@playwright/mcp 0.0.34 → 0.0.35-alpha-2025-08-28

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/lib/mcp/mdb.js ADDED
@@ -0,0 +1,198 @@
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 debug from 'debug';
17
+ import { z } from 'zod';
18
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
19
+ import { PingRequestSchema } from '@modelcontextprotocol/sdk/types.js';
20
+ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
21
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
22
+ import { defineToolSchema } from './tool.js';
23
+ import * as mcpServer from './server.js';
24
+ import * as mcpHttp from './http.js';
25
+ import { wrapInProcess } from './server.js';
26
+ import { ManualPromise } from './manualPromise.js';
27
+ const mdbDebug = debug('pw:mcp:mdb');
28
+ const errorsDebug = debug('pw:mcp:errors');
29
+ export class MDBBackend {
30
+ _stack = [];
31
+ _interruptPromise;
32
+ _topLevelBackend;
33
+ _initialized = false;
34
+ constructor(topLevelBackend) {
35
+ this._topLevelBackend = topLevelBackend;
36
+ }
37
+ async initialize(server) {
38
+ if (this._initialized)
39
+ return;
40
+ this._initialized = true;
41
+ const transport = await wrapInProcess(this._topLevelBackend);
42
+ await this._pushClient(transport);
43
+ }
44
+ async listTools() {
45
+ const response = await this._client().listTools();
46
+ return response.tools;
47
+ }
48
+ async callTool(name, args) {
49
+ if (name === pushToolsSchema.name)
50
+ return await this._pushTools(pushToolsSchema.inputSchema.parse(args || {}));
51
+ const interruptPromise = new ManualPromise();
52
+ this._interruptPromise = interruptPromise;
53
+ let [entry] = this._stack;
54
+ // Pop the client while the tool is not found.
55
+ while (entry && !entry.toolNames.includes(name)) {
56
+ mdbDebug('popping client from stack for ', name);
57
+ this._stack.shift();
58
+ await entry.client.close();
59
+ entry = this._stack[0];
60
+ }
61
+ if (!entry)
62
+ throw new Error(`Tool ${name} not found in the tool stack`);
63
+ const resultPromise = new ManualPromise();
64
+ entry.resultPromise = resultPromise;
65
+ this._client().callTool({
66
+ name,
67
+ arguments: args,
68
+ }).then(result => {
69
+ resultPromise.resolve(result);
70
+ }).catch(e => {
71
+ mdbDebug('error in client call', e);
72
+ if (this._stack.length < 2)
73
+ throw e;
74
+ this._stack.shift();
75
+ const prevEntry = this._stack[0];
76
+ void prevEntry.resultPromise.then(result => resultPromise.resolve(result));
77
+ });
78
+ const result = await Promise.race([interruptPromise, resultPromise]);
79
+ if (interruptPromise.isDone())
80
+ mdbDebug('client call intercepted', result);
81
+ else
82
+ mdbDebug('client call result', result);
83
+ return result;
84
+ }
85
+ _client() {
86
+ const [entry] = this._stack;
87
+ if (!entry)
88
+ throw new Error('No debugging backend available');
89
+ return entry.client;
90
+ }
91
+ async _pushTools(params) {
92
+ mdbDebug('pushing tools to the stack', params.mcpUrl);
93
+ const transport = new StreamableHTTPClientTransport(new URL(params.mcpUrl));
94
+ await this._pushClient(transport, params.introMessage);
95
+ return { content: [{ type: 'text', text: 'Tools pushed' }] };
96
+ }
97
+ async _pushClient(transport, introMessage) {
98
+ mdbDebug('pushing client to the stack');
99
+ const client = new Client({ name: 'Internal client', version: '0.0.0' });
100
+ client.setRequestHandler(PingRequestSchema, () => ({}));
101
+ await client.connect(transport);
102
+ mdbDebug('connected to the new client');
103
+ const { tools } = await client.listTools();
104
+ this._stack.unshift({ client, toolNames: tools.map(tool => tool.name), resultPromise: undefined });
105
+ mdbDebug('new tools added to the stack:', tools.map(tool => tool.name));
106
+ mdbDebug('interrupting current call:', !!this._interruptPromise);
107
+ this._interruptPromise?.resolve({
108
+ content: [{
109
+ type: 'text',
110
+ text: introMessage || '',
111
+ }],
112
+ });
113
+ this._interruptPromise = undefined;
114
+ return { content: [{ type: 'text', text: 'Tools pushed' }] };
115
+ }
116
+ }
117
+ const pushToolsSchema = defineToolSchema({
118
+ name: 'mdb_push_tools',
119
+ title: 'Push MCP tools to the tools stack',
120
+ description: 'Push MCP tools to the tools stack',
121
+ inputSchema: z.object({
122
+ mcpUrl: z.string(),
123
+ introMessage: z.string().optional(),
124
+ }),
125
+ type: 'readOnly',
126
+ });
127
+ export async function runMainBackend(backendFactory, options) {
128
+ const mdbBackend = new MDBBackend(backendFactory.create());
129
+ // Start HTTP unconditionally.
130
+ const factory = {
131
+ ...backendFactory,
132
+ create: () => mdbBackend
133
+ };
134
+ const url = await startAsHttp(factory, { port: options?.port || 0 });
135
+ process.env.PLAYWRIGHT_DEBUGGER_MCP = url;
136
+ if (options?.port !== undefined)
137
+ return url;
138
+ // Start stdio conditionally.
139
+ await mcpServer.connect(factory, new StdioServerTransport(), false);
140
+ }
141
+ export async function runOnPauseBackendLoop(mdbUrl, backend, introMessage) {
142
+ const wrappedBackend = new OnceTimeServerBackendWrapper(backend);
143
+ const factory = {
144
+ name: 'on-pause-backend',
145
+ nameInConfig: 'on-pause-backend',
146
+ version: '0.0.0',
147
+ create: () => wrappedBackend,
148
+ };
149
+ const httpServer = await mcpHttp.startHttpServer({ port: 0 });
150
+ await mcpHttp.installHttpTransport(httpServer, factory);
151
+ const url = mcpHttp.httpAddressToString(httpServer.address());
152
+ const client = new Client({ name: 'Internal client', version: '0.0.0' });
153
+ client.setRequestHandler(PingRequestSchema, () => ({}));
154
+ const transport = new StreamableHTTPClientTransport(new URL(mdbUrl));
155
+ await client.connect(transport);
156
+ const pushToolsResult = await client.callTool({
157
+ name: pushToolsSchema.name,
158
+ arguments: {
159
+ mcpUrl: url,
160
+ introMessage,
161
+ },
162
+ });
163
+ if (pushToolsResult.isError)
164
+ errorsDebug('Failed to push tools', pushToolsResult.content);
165
+ await transport.terminateSession();
166
+ await client.close();
167
+ await wrappedBackend.waitForClosed();
168
+ httpServer.close();
169
+ }
170
+ async function startAsHttp(backendFactory, options) {
171
+ const httpServer = await mcpHttp.startHttpServer(options);
172
+ await mcpHttp.installHttpTransport(httpServer, backendFactory);
173
+ return mcpHttp.httpAddressToString(httpServer.address());
174
+ }
175
+ class OnceTimeServerBackendWrapper {
176
+ _backend;
177
+ _selfDestructPromise = new ManualPromise();
178
+ constructor(backend) {
179
+ this._backend = backend;
180
+ this._backend.requestSelfDestruct = () => this._selfDestructPromise.resolve();
181
+ }
182
+ async initialize(server, clientVersion, roots) {
183
+ await this._backend.initialize?.(server, clientVersion, roots);
184
+ }
185
+ async listTools() {
186
+ return this._backend.listTools();
187
+ }
188
+ async callTool(name, args) {
189
+ return this._backend.callTool(name, args);
190
+ }
191
+ serverClosed(server) {
192
+ this._backend.serverClosed?.(server);
193
+ this._selfDestructPromise.resolve();
194
+ }
195
+ async waitForClosed() {
196
+ await this._selfDestructPromise;
197
+ }
198
+ }
@@ -13,15 +13,13 @@
13
13
  * See the License for the specific language governing permissions and
14
14
  * limitations under the License.
15
15
  */
16
+ import debug from 'debug';
16
17
  import { z } from 'zod';
17
18
  import { zodToJsonSchema } from 'zod-to-json-schema';
18
19
  import { Client } from '@modelcontextprotocol/sdk/client/index.js';
19
20
  import { ListRootsRequestSchema, PingRequestSchema } from '@modelcontextprotocol/sdk/types.js';
20
- import { logUnhandledError } from '../utils/log.js';
21
- import { packageJSON } from '../utils/package.js';
21
+ const errorsDebug = debug('pw:mcp:errors');
22
22
  export class ProxyBackend {
23
- name = 'Playwright MCP Client Switcher';
24
- version = packageJSON.version;
25
23
  _mcpProviders;
26
24
  _currentClient;
27
25
  _contextSwitchTool;
@@ -30,7 +28,7 @@ export class ProxyBackend {
30
28
  this._mcpProviders = mcpProviders;
31
29
  this._contextSwitchTool = this._defineContextSwitchTool();
32
30
  }
33
- async initialize(clientVersion, roots) {
31
+ async initialize(server, clientVersion, roots) {
34
32
  this._roots = roots;
35
33
  await this._setCurrentClient(this._mcpProviders[0]);
36
34
  }
@@ -52,7 +50,7 @@ export class ProxyBackend {
52
50
  });
53
51
  }
54
52
  serverClosed() {
55
- void this._currentClient?.close().catch(logUnhandledError);
53
+ void this._currentClient?.close().catch(errorsDebug);
56
54
  }
57
55
  async _callContextSwitchTool(params) {
58
56
  try {
@@ -91,7 +89,7 @@ export class ProxyBackend {
91
89
  async _setCurrentClient(factory) {
92
90
  await this._currentClient?.close();
93
91
  this._currentClient = undefined;
94
- const client = new Client({ name: 'Playwright MCP Proxy', version: packageJSON.version });
92
+ const client = new Client({ name: 'Playwright MCP Proxy', version: '0.0.0' });
95
93
  client.registerCapabilities({
96
94
  roots: {
97
95
  listRoots: true,
package/lib/mcp/server.js CHANGED
@@ -16,17 +16,23 @@
16
16
  import debug from 'debug';
17
17
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
18
18
  import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
19
- import { ManualPromise } from '../utils/manualPromise.js';
20
- import { logUnhandledError } from '../utils/log.js';
19
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
20
+ import { httpAddressToString, installHttpTransport, startHttpServer } from './http.js';
21
+ import { InProcessTransport } from './inProcessTransport.js';
21
22
  const serverDebug = debug('pw:mcp:server');
22
- export async function connect(serverBackendFactory, transport, runHeartbeat) {
23
- const backend = serverBackendFactory();
24
- const server = createServer(backend, runHeartbeat);
23
+ const errorsDebug = debug('pw:mcp:errors');
24
+ export async function connect(factory, transport, runHeartbeat) {
25
+ const server = createServer(factory.name, factory.version, factory.create(), runHeartbeat);
25
26
  await server.connect(transport);
26
27
  }
27
- export function createServer(backend, runHeartbeat) {
28
- const initializedPromise = new ManualPromise();
29
- const server = new Server({ name: backend.name, version: backend.version }, {
28
+ export async function wrapInProcess(backend) {
29
+ const server = createServer('Internal', '0.0.0', backend, false);
30
+ return new InProcessTransport(server);
31
+ }
32
+ export function createServer(name, version, backend, runHeartbeat) {
33
+ let initializedPromiseResolve = () => { };
34
+ const initializedPromise = new Promise(resolve => initializedPromiseResolve = resolve);
35
+ const server = new Server({ name, version }, {
30
36
  capabilities: {
31
37
  tools: {},
32
38
  }
@@ -64,14 +70,14 @@ export function createServer(backend, runHeartbeat) {
64
70
  clientRoots = roots;
65
71
  }
66
72
  const clientVersion = server.getClientVersion() ?? { name: 'unknown', version: 'unknown' };
67
- await backend.initialize?.(clientVersion, clientRoots);
68
- initializedPromise.resolve();
73
+ await backend.initialize?.(server, clientVersion, clientRoots);
74
+ initializedPromiseResolve();
69
75
  }
70
76
  catch (e) {
71
- logUnhandledError(e);
77
+ errorsDebug(e);
72
78
  }
73
79
  });
74
- addServerListener(server, 'close', () => backend.serverClosed?.());
80
+ addServerListener(server, 'close', () => backend.serverClosed?.(server));
75
81
  return server;
76
82
  }
77
83
  const startHeartbeat = (server) => {
@@ -94,3 +100,24 @@ function addServerListener(server, event, listener) {
94
100
  listener();
95
101
  };
96
102
  }
103
+ export async function start(serverBackendFactory, options) {
104
+ if (options.port === undefined) {
105
+ await connect(serverBackendFactory, new StdioServerTransport(), false);
106
+ return;
107
+ }
108
+ const httpServer = await startHttpServer(options);
109
+ await installHttpTransport(httpServer, serverBackendFactory);
110
+ const url = httpAddressToString(httpServer.address());
111
+ const mcpConfig = { mcpServers: {} };
112
+ mcpConfig.mcpServers[serverBackendFactory.nameInConfig] = {
113
+ url: `${url}/mcp`
114
+ };
115
+ const message = [
116
+ `Listening on ${url}`,
117
+ 'Put this in your client config:',
118
+ JSON.stringify(mcpConfig, undefined, 2),
119
+ 'For legacy SSE transport support, you can use the /sse endpoint instead.',
120
+ ].join('\n');
121
+ // eslint-disable-next-line no-console
122
+ console.error(message);
123
+ }
package/lib/mcp/tool.js CHANGED
@@ -27,3 +27,6 @@ export function toMcpTool(tool) {
27
27
  },
28
28
  };
29
29
  }
30
+ export function defineToolSchema(tool) {
31
+ return tool;
32
+ }
package/lib/program.js CHANGED
@@ -15,7 +15,6 @@
15
15
  */
16
16
  import { program, Option } from 'commander';
17
17
  import * as mcpServer from './mcp/server.js';
18
- import * as mcpTransport from './mcp/transport.js';
19
18
  import { commaSeparatedList, resolveCLIConfig, semicolonSeparatedList } from './config.js';
20
19
  import { packageJSON } from './utils/package.js';
21
20
  import { Context } from './context.js';
@@ -24,7 +23,7 @@ import { runLoopTools } from './loopTools/main.js';
24
23
  import { ProxyBackend } from './mcp/proxyBackend.js';
25
24
  import { BrowserServerBackend } from './browserServerBackend.js';
26
25
  import { ExtensionContextFactory } from './extension/extensionContextFactory.js';
27
- import { InProcessTransport } from './mcp/inProcessTransport.js';
26
+ import { runVSCodeTools } from './vscode/host.js';
28
27
  program
29
28
  .version('Version ' + packageJSON.version)
30
29
  .name(packageJSON.name)
@@ -55,6 +54,7 @@ program
55
54
  .option('--user-data-dir <path>', 'path to the user data directory. If not specified, a temporary directory will be created.')
56
55
  .option('--viewport-size <size>', 'specify browser viewport size in pixels, for example "1280, 720"')
57
56
  .addOption(new Option('--connect-tool', 'Allow to switch between different browser connection methods.').hideHelp())
57
+ .addOption(new Option('--vscode', 'VS Code tools.').hideHelp())
58
58
  .addOption(new Option('--loop-tools', 'Run loop tools').hideHelp())
59
59
  .addOption(new Option('--vision', 'Legacy option, use --caps=vision instead').hideHelp())
60
60
  .action(async (options) => {
@@ -65,21 +65,55 @@ program
65
65
  options.caps = 'vision';
66
66
  }
67
67
  const config = await resolveCLIConfig(options);
68
+ const browserContextFactory = contextFactory(config);
69
+ const extensionContextFactory = new ExtensionContextFactory(config.browser.launchOptions.channel || 'chrome', config.browser.userDataDir, config.browser.launchOptions.executablePath);
68
70
  if (options.extension) {
69
- const contextFactory = createExtensionContextFactory(config);
70
- const serverBackendFactory = () => new BrowserServerBackend(config, contextFactory);
71
- await mcpTransport.start(serverBackendFactory, config.server);
71
+ const serverBackendFactory = {
72
+ name: 'Playwright w/ extension',
73
+ nameInConfig: 'playwright-extension',
74
+ version: packageJSON.version,
75
+ create: () => new BrowserServerBackend(config, extensionContextFactory)
76
+ };
77
+ await mcpServer.start(serverBackendFactory, config.server);
78
+ return;
79
+ }
80
+ if (options.vscode) {
81
+ await runVSCodeTools(config);
72
82
  return;
73
83
  }
74
84
  if (options.loopTools) {
75
85
  await runLoopTools(config);
76
86
  return;
77
87
  }
78
- const browserContextFactory = contextFactory(config);
79
- const providers = [mcpProviderForBrowserContextFactory(config, browserContextFactory)];
80
- if (options.connectTool)
81
- providers.push(mcpProviderForBrowserContextFactory(config, createExtensionContextFactory(config)));
82
- await mcpTransport.start(() => new ProxyBackend(providers), config.server);
88
+ if (options.connectTool) {
89
+ const providers = [
90
+ {
91
+ name: 'default',
92
+ description: 'Starts standalone browser',
93
+ connect: () => mcpServer.wrapInProcess(new BrowserServerBackend(config, browserContextFactory)),
94
+ },
95
+ {
96
+ name: 'extension',
97
+ description: 'Connect to a browser using the Playwright MCP extension',
98
+ connect: () => mcpServer.wrapInProcess(new BrowserServerBackend(config, extensionContextFactory)),
99
+ },
100
+ ];
101
+ const factory = {
102
+ name: 'Playwright w/ switch',
103
+ nameInConfig: 'playwright-switch',
104
+ version: packageJSON.version,
105
+ create: () => new ProxyBackend(providers),
106
+ };
107
+ await mcpServer.start(factory, config.server);
108
+ return;
109
+ }
110
+ const factory = {
111
+ name: 'Playwright',
112
+ nameInConfig: 'playwright',
113
+ version: packageJSON.version,
114
+ create: () => new BrowserServerBackend(config, browserContextFactory)
115
+ };
116
+ await mcpServer.start(factory, config.server);
83
117
  });
84
118
  function setupExitWatchdog() {
85
119
  let isExiting = false;
@@ -95,17 +129,4 @@ function setupExitWatchdog() {
95
129
  process.on('SIGINT', handleExit);
96
130
  process.on('SIGTERM', handleExit);
97
131
  }
98
- function createExtensionContextFactory(config) {
99
- return new ExtensionContextFactory(config.browser.launchOptions.channel || 'chrome', config.browser.userDataDir);
100
- }
101
- function mcpProviderForBrowserContextFactory(config, browserContextFactory) {
102
- return {
103
- name: browserContextFactory.name,
104
- description: browserContextFactory.description,
105
- connect: async () => {
106
- const server = mcpServer.createServer(new BrowserServerBackend(config, browserContextFactory), false);
107
- return new InProcessTransport(server);
108
- },
109
- };
110
- }
111
132
  void program.parseAsync(process.argv);
package/lib/tab.js CHANGED
@@ -16,7 +16,7 @@
16
16
  import { EventEmitter } from 'events';
17
17
  import { callOnPageNoTrace, waitForCompletion } from './tools/utils.js';
18
18
  import { logUnhandledError } from './utils/log.js';
19
- import { ManualPromise } from './utils/manualPromise.js';
19
+ import { ManualPromise } from './mcp/manualPromise.js';
20
20
  export const TabEvents = {
21
21
  modalState: 'modalState'
22
22
  };
@@ -0,0 +1,57 @@
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 { defineTabTool } from './tool.js';
18
+ import { generateLocator } from './utils.js';
19
+ import * as javascript from '../utils/codegen.js';
20
+ const fillForm = defineTabTool({
21
+ capability: 'core',
22
+ schema: {
23
+ name: 'browser_fill_form',
24
+ title: 'Fill form',
25
+ description: 'Fill multiple form fields',
26
+ inputSchema: z.object({
27
+ fields: z.array(z.object({
28
+ name: z.string().describe('Human-readable field name'),
29
+ type: z.enum(['textbox', 'checkbox', 'radio', 'combobox', 'slider']).describe('Type of the field'),
30
+ ref: z.string().describe('Exact target field reference from the page snapshot'),
31
+ value: z.string().describe('Value to fill in the field. If the field is a checkbox, the value should be `true` or `false`. If the field is a combobox, the value should be the text of the option.'),
32
+ })).describe('Fields to fill in'),
33
+ }),
34
+ type: 'destructive',
35
+ },
36
+ handle: async (tab, params, response) => {
37
+ for (const field of params.fields) {
38
+ const locator = await tab.refLocator({ element: field.name, ref: field.ref });
39
+ const locatorSource = `await page.${await generateLocator(locator)}`;
40
+ if (field.type === 'textbox' || field.type === 'slider') {
41
+ await locator.fill(field.value);
42
+ response.addCode(`${locatorSource}.fill(${javascript.quote(field.value)});`);
43
+ }
44
+ else if (field.type === 'checkbox' || field.type === 'radio') {
45
+ await locator.setChecked(field.value === 'true');
46
+ response.addCode(`${locatorSource}.setChecked(${javascript.quote(field.value)});`);
47
+ }
48
+ else if (field.type === 'combobox') {
49
+ await locator.selectOption({ label: field.value });
50
+ response.addCode(`${locatorSource}.selectOption(${javascript.quote(field.value)});`);
51
+ }
52
+ }
53
+ },
54
+ });
55
+ export default [
56
+ fillForm,
57
+ ];
@@ -48,23 +48,7 @@ const goBack = defineTabTool({
48
48
  response.addCode(`await page.goBack();`);
49
49
  },
50
50
  });
51
- const goForward = defineTabTool({
52
- capability: 'core',
53
- schema: {
54
- name: 'browser_navigate_forward',
55
- title: 'Go forward',
56
- description: 'Go forward to the next page',
57
- inputSchema: z.object({}),
58
- type: 'readOnly',
59
- },
60
- handle: async (tab, params, response) => {
61
- await tab.page.goForward();
62
- response.setIncludeSnapshot();
63
- response.addCode(`await page.goForward();`);
64
- },
65
- });
66
51
  export default [
67
52
  navigate,
68
53
  goBack,
69
- goForward,
70
54
  ];
package/lib/tools/tabs.js CHANGED
@@ -15,73 +15,45 @@
15
15
  */
16
16
  import { z } from 'zod';
17
17
  import { defineTool } from './tool.js';
18
- const listTabs = defineTool({
18
+ const browserTabs = defineTool({
19
19
  capability: 'core-tabs',
20
20
  schema: {
21
- name: 'browser_tab_list',
22
- title: 'List tabs',
23
- description: 'List browser tabs',
24
- inputSchema: z.object({}),
25
- type: 'readOnly',
26
- },
27
- handle: async (context, params, response) => {
28
- await context.ensureTab();
29
- response.setIncludeTabs();
30
- },
31
- });
32
- const selectTab = defineTool({
33
- capability: 'core-tabs',
34
- schema: {
35
- name: 'browser_tab_select',
36
- title: 'Select a tab',
37
- description: 'Select a tab by index',
38
- inputSchema: z.object({
39
- index: z.number().describe('The index of the tab to select'),
40
- }),
41
- type: 'readOnly',
42
- },
43
- handle: async (context, params, response) => {
44
- await context.selectTab(params.index);
45
- response.setIncludeSnapshot();
46
- },
47
- });
48
- const newTab = defineTool({
49
- capability: 'core-tabs',
50
- schema: {
51
- name: 'browser_tab_new',
52
- title: 'Open a new tab',
53
- description: 'Open a new tab',
54
- inputSchema: z.object({
55
- url: z.string().optional().describe('The URL to navigate to in the new tab. If not provided, the new tab will be blank.'),
56
- }),
57
- type: 'readOnly',
58
- },
59
- handle: async (context, params, response) => {
60
- const tab = await context.newTab();
61
- if (params.url)
62
- await tab.navigate(params.url);
63
- response.setIncludeSnapshot();
64
- },
65
- });
66
- const closeTab = defineTool({
67
- capability: 'core-tabs',
68
- schema: {
69
- name: 'browser_tab_close',
70
- title: 'Close a tab',
71
- description: 'Close a tab',
21
+ name: 'browser_tabs',
22
+ title: 'Manage tabs',
23
+ description: 'List, create, close, or select a browser tab.',
72
24
  inputSchema: z.object({
73
- index: z.number().optional().describe('The index of the tab to close. Closes current tab if not provided.'),
25
+ action: z.enum(['list', 'new', 'close', 'select']).describe('Operation to perform'),
26
+ index: z.number().optional().describe('Tab index, used for close/select. If omitted for close, current tab is closed.'),
74
27
  }),
75
28
  type: 'destructive',
76
29
  },
77
30
  handle: async (context, params, response) => {
78
- await context.closeTab(params.index);
79
- response.setIncludeSnapshot();
31
+ switch (params.action) {
32
+ case 'list': {
33
+ await context.ensureTab();
34
+ response.setIncludeTabs();
35
+ return;
36
+ }
37
+ case 'new': {
38
+ await context.newTab();
39
+ response.setIncludeTabs();
40
+ return;
41
+ }
42
+ case 'close': {
43
+ await context.closeTab(params.index);
44
+ response.setIncludeSnapshot();
45
+ return;
46
+ }
47
+ case 'select': {
48
+ if (!params.index)
49
+ throw new Error('Tab index is required');
50
+ await context.selectTab(params.index);
51
+ response.setIncludeSnapshot();
52
+ return;
53
+ }
54
+ }
80
55
  },
81
56
  });
82
57
  export default [
83
- listTabs,
84
- newTab,
85
- selectTab,
86
- closeTab,
58
+ browserTabs,
87
59
  ];