diff --git a/examples/todomvc/.claude/agents/playwright-test-planner.md b/examples/todomvc/.claude/agents/playwright-test-planner.md
index da10326eb98b8..78af43b534500 100644
--- a/examples/todomvc/.claude/agents/playwright-test-planner.md
+++ b/examples/todomvc/.claude/agents/playwright-test-planner.md
@@ -1,7 +1,7 @@
---
name: playwright-test-planner
description: Use this agent when you need to create comprehensive test plan for a web application or website
-tools: Glob, Grep, Read, LS, Edit, MultiEdit, Write, mcp__playwright-test__browser_click, mcp__playwright-test__browser_close, mcp__playwright-test__browser_console_messages, mcp__playwright-test__browser_drag, mcp__playwright-test__browser_evaluate, mcp__playwright-test__browser_file_upload, mcp__playwright-test__browser_handle_dialog, mcp__playwright-test__browser_hover, mcp__playwright-test__browser_navigate, mcp__playwright-test__browser_navigate_back, mcp__playwright-test__browser_network_requests, mcp__playwright-test__browser_press_key, mcp__playwright-test__browser_select_option, mcp__playwright-test__browser_snapshot, mcp__playwright-test__browser_take_screenshot, mcp__playwright-test__browser_type, mcp__playwright-test__browser_wait_for, mcp__playwright-test__planner_setup_page
+tools: Glob, Grep, Read, LS, mcp__playwright-test__browser_click, mcp__playwright-test__browser_close, mcp__playwright-test__browser_console_messages, mcp__playwright-test__browser_drag, mcp__playwright-test__browser_evaluate, mcp__playwright-test__browser_file_upload, mcp__playwright-test__browser_handle_dialog, mcp__playwright-test__browser_hover, mcp__playwright-test__browser_navigate, mcp__playwright-test__browser_navigate_back, mcp__playwright-test__browser_network_requests, mcp__playwright-test__browser_press_key, mcp__playwright-test__browser_select_option, mcp__playwright-test__browser_snapshot, mcp__playwright-test__browser_take_screenshot, mcp__playwright-test__browser_type, mcp__playwright-test__browser_wait_for, mcp__playwright-test__planner_setup_page, mcp__playwright-test__planner_save_plan
model: sonnet
color: green
---
@@ -41,52 +41,7 @@ You will:
5. **Create Documentation**
- Save your test plan as requested:
- - Executive summary of the tested page/application
- - Individual scenarios as separate sections
- - Each scenario formatted with numbered steps
- - Each test case with proposed file name for implementation
- - Clear expected results for verification
-
-
-# TodoMVC Application - Comprehensive Test Plan
-
-## Application Overview
-
-The TodoMVC application is a React-based todo list manager that provides core task management functionality. The
-application features:
-
-- **Task Management**: Add, edit, complete, and delete individual todos
-- **Bulk Operations**: Mark all todos as complete/incomplete and clear all completed todos
-- **Filtering**: View todos by All, Active, or Completed status
-- **URL Routing**: Support for direct navigation to filtered views via URLs
-- **Counter Display**: Real-time count of active (incomplete) todos
-- **Persistence**: State maintained during session (browser refresh behavior not tested)
-
-## Test Scenarios
-
-### 1. Adding New Todos
-
-**Seed:** `tests/seed.spec.ts`
-
-#### 1.1 Add Valid Todo
-
-**File** `tests/adding-new-todos/add-valid-todo.spec.ts`
-
-**Steps:**
-1. Click in the "What needs to be done?" input field
-2. Type "Buy groceries"
-3. Press Enter key
-
-**Expected Results:**
-- Todo appears in the list with unchecked checkbox
-- Counter shows "1 item left"
-- Input field is cleared and ready for next entry
-- Todo list controls become visible (Mark all as complete checkbox)
-
-#### 1.2
-...
-
+ Submit your test plan using `planner_save_plan` tool.
**Quality Standards**:
- Write steps that are specific enough for any tester to follow
diff --git a/package-lock.json b/package-lock.json
index 94d7eff8a761f..e2c9a8bc4bf68 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -54,6 +54,7 @@
"react": "^19.1.1",
"react-dom": "^19.1.1",
"ssim.js": "^3.5.0",
+ "tiny-loop": "^0.0.6",
"typescript": "^5.9.2",
"vite": "^6.4.1",
"ws": "^8.17.1",
@@ -7389,6 +7390,16 @@
"node": ">=18"
}
},
+ "node_modules/tiny-loop": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/tiny-loop/-/tiny-loop-0.0.6.tgz",
+ "integrity": "sha512-rzNqgjcNwp/MDh7XDPrgXxqjSpxcKUwDYDHRPDca7QP8FKwH8YSImhtzZkkbASps3JYW5WE1AfAhmaqNOVWrMw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=20"
+ }
+ },
"node_modules/tinyglobby": {
"version": "0.2.14",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
diff --git a/package.json b/package.json
index ef4154d72d70f..450b2d9daf9ae 100644
--- a/package.json
+++ b/package.json
@@ -93,6 +93,7 @@
"react": "^19.1.1",
"react-dom": "^19.1.1",
"ssim.js": "^3.5.0",
+ "tiny-loop": "^0.0.6",
"typescript": "^5.9.2",
"vite": "^6.4.1",
"ws": "^8.17.1",
diff --git a/packages/playwright/ThirdPartyNotices.txt b/packages/playwright/ThirdPartyNotices.txt
index 8e00058b381e5..1c67672f8bc3e 100644
--- a/packages/playwright/ThirdPartyNotices.txt
+++ b/packages/playwright/ThirdPartyNotices.txt
@@ -207,6 +207,7 @@ This project incorporates components from the projects listed below. The origina
- statuses@2.0.2 (https://github.com/jshttp/statuses)
- stoppable@1.1.0 (https://github.com/hunterloftis/stoppable)
- supports-color@7.2.0 (https://github.com/chalk/supports-color)
+- tiny-loop@0.0.6 (https://github.com/pavelfeldman/tiny-loop)
- to-regex-range@5.0.1 (https://github.com/micromatch/to-regex-range)
- toidentifier@1.0.1 (https://github.com/component/toidentifier)
- type-is@2.0.1 (https://github.com/jshttp/type-is)
@@ -5883,6 +5884,212 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
=========================================
END OF supports-color@7.2.0 AND INFORMATION
+%% tiny-loop@0.0.6 NOTICES AND INFORMATION BEGIN HERE
+=========================================
+Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ 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.
+=========================================
+END OF tiny-loop@0.0.6 AND INFORMATION
+
%% to-regex-range@5.0.1 NOTICES AND INFORMATION BEGIN HERE
=========================================
The MIT License (MIT)
@@ -6192,6 +6399,6 @@ END OF zod@3.25.76 AND INFORMATION
SUMMARY BEGIN HERE
=========================================
-Total Packages: 216
+Total Packages: 217
=========================================
END OF SUMMARY
\ No newline at end of file
diff --git a/packages/playwright/bundles/mcp/package-lock.json b/packages/playwright/bundles/mcp/package-lock.json
index fa26c67851736..4efc9cc345216 100644
--- a/packages/playwright/bundles/mcp/package-lock.json
+++ b/packages/playwright/bundles/mcp/package-lock.json
@@ -9,6 +9,7 @@
"version": "0.0.1",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.17.5",
+ "tiny-loop": "^0.0.6",
"zod": "^3.25.76",
"zod-to-json-schema": "^3.24.6"
}
@@ -958,6 +959,15 @@
"node": ">= 0.8"
}
},
+ "node_modules/tiny-loop": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/tiny-loop/-/tiny-loop-0.0.6.tgz",
+ "integrity": "sha512-rzNqgjcNwp/MDh7XDPrgXxqjSpxcKUwDYDHRPDca7QP8FKwH8YSImhtzZkkbASps3JYW5WE1AfAhmaqNOVWrMw==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=20"
+ }
+ },
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
diff --git a/packages/playwright/bundles/mcp/package.json b/packages/playwright/bundles/mcp/package.json
index 9c39c6db792a7..785e006301d2b 100644
--- a/packages/playwright/bundles/mcp/package.json
+++ b/packages/playwright/bundles/mcp/package.json
@@ -4,6 +4,7 @@
"private": true,
"dependencies": {
"@modelcontextprotocol/sdk": "^1.17.5",
+ "tiny-loop": "^0.0.6",
"zod": "^3.25.76",
"zod-to-json-schema": "^3.24.6"
}
diff --git a/packages/playwright/bundles/mcp/src/mcpBundleImpl.ts b/packages/playwright/bundles/mcp/src/mcpBundleImpl.ts
index 613a648e15ee2..8e4e4ea8c9707 100644
--- a/packages/playwright/bundles/mcp/src/mcpBundleImpl.ts
+++ b/packages/playwright/bundles/mcp/src/mcpBundleImpl.ts
@@ -23,5 +23,6 @@ export { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
export { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
export { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
export { CallToolRequestSchema, ListRootsRequestSchema, ListToolsRequestSchema, PingRequestSchema, ProgressNotificationSchema } from '@modelcontextprotocol/sdk/types.js';
+export { Loop } from 'tiny-loop';
export { z } from 'zod';
export { zodToJsonSchema } from 'zod-to-json-schema';
diff --git a/packages/playwright/src/DEPS.list b/packages/playwright/src/DEPS.list
index d0c1ac71bc33a..680cf5e8870e8 100644
--- a/packages/playwright/src/DEPS.list
+++ b/packages/playwright/src/DEPS.list
@@ -14,6 +14,7 @@ common/
./mcp/sdk/
./mcp/test/
./transform/babelBundle.ts
+./agents/
[internalsForTest.ts]
**
diff --git a/packages/playwright/src/agents/DEPS.list b/packages/playwright/src/agents/DEPS.list
index 75300109dfd78..49f26daf1a5d6 100644
--- a/packages/playwright/src/agents/DEPS.list
+++ b/packages/playwright/src/agents/DEPS.list
@@ -1,3 +1,8 @@
-[generateAgents.ts]
+[*]
+../mcp/browser/
+../mcp/sdk/
../mcp/test/
../common/
+
+[generateAgents.ts]
+../mcp/test/
diff --git a/packages/playwright/src/agents/agent.ts b/packages/playwright/src/agents/agent.ts
new file mode 100644
index 0000000000000..c7f7a97e99002
--- /dev/null
+++ b/packages/playwright/src/agents/agent.ts
@@ -0,0 +1,97 @@
+/**
+ * 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 { Loop } from '../mcp/sdk/bundle';
+
+import type z from 'zod';
+import type { Client } from '@modelcontextprotocol/sdk/client/index.js';
+import type { CallToolResult, Tool } from '@modelcontextprotocol/sdk/types.js';
+import type { Loop as LoopType } from 'tiny-loop';
+
+type Logger = (category: string, text: string, details?: string) => void;
+
+export type AgentSpec = {
+ name: string;
+ description: string;
+ model: string;
+ color: string;
+ tools: string[];
+ instructions: string;
+ examples: string[];
+};
+
+export class Agent> {
+ readonly loop: LoopType;
+ readonly spec: AgentSpec;
+ readonly clients: Map;
+ readonly resultSchema: Tool['inputSchema'];
+
+ constructor(loopName: 'copilot' | 'claude' | 'openai', spec: AgentSpec, clients: Map, resultSchema: Tool['inputSchema']) {
+ this.loop = new Loop(loopName);
+ this.spec = spec;
+ this.clients = clients;
+ this.resultSchema = resultSchema;
+ }
+
+ async runTask(task: string, params: any, options?: { logger?: Logger }): Promise> {
+ const { clients, tools, callTool } = await this._initClients();
+ const prompt = this.spec.description;
+ try {
+ return await this.loop.run>(`${prompt}\n\nTask:\n${task}\n\nParams:\n${JSON.stringify(params, null, 2)}`, {
+ ...options,
+ tools,
+ callTool,
+ resultSchema: this.resultSchema
+ });
+ } finally {
+ await this._disconnectFromServers(clients);
+ }
+ }
+
+ private async _initClients() {
+ const clients: Record = {};
+ const agentToolNames = new Set(this.spec.tools);
+ const tools: Tool[] = [];
+
+ for (const [name, client] of this.clients.entries()) {
+ const list = await client.listTools();
+ for (const tool of list.tools) {
+ if (!agentToolNames.has(name + '/' + tool.name))
+ continue;
+ agentToolNames.delete(name + '/' + tool.name);
+ tools.push({ ...tool, name: name + '__' + tool.name });
+ }
+ clients[name] = client;
+ }
+
+ if (agentToolNames.size > 0)
+ throw new Error(`Required tools not found: ${Array.from(agentToolNames).join(', ')}`);
+
+ const callTool: (params: { name: string, arguments: any}) => Promise = async params => {
+ const [serverName, toolName] = params.name.split('__');
+ const client = clients[serverName];
+ if (!client)
+ throw new Error(`Unknown server: ${serverName}`);
+ return await client.callTool({ name: toolName, arguments: params.arguments }) as CallToolResult;
+ };
+ return { clients, tools, callTool };
+ }
+
+ private async _disconnectFromServers(clients: Record) {
+ for (const client of Object.values(clients))
+ await client.close();
+ }
+}
diff --git a/packages/playwright/src/agents/agentParser.ts b/packages/playwright/src/agents/agentParser.ts
new file mode 100644
index 0000000000000..fd3c6bd77da6e
--- /dev/null
+++ b/packages/playwright/src/agents/agentParser.ts
@@ -0,0 +1,92 @@
+/**
+ * Copyright Microsoft Corporation. All rights reserved.
+ *
+ * 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 fs from 'fs';
+import { yaml } from 'playwright-core/lib/utilsBundle';
+
+import type { AgentSpec } from './agent';
+
+type AgentSpecHeader = {
+ name: string;
+ description: string;
+ model: string;
+ color: string;
+ tools: string[];
+};
+
+export async function parseAgentSpec(filePath: string): Promise {
+ const source = await fs.promises.readFile(filePath, 'utf-8');
+ const { header, content } = extractYamlAndContent(source);
+ const { instructions, examples } = extractInstructionsAndExamples(content);
+ return {
+ ...header,
+ instructions,
+ examples,
+ };
+}
+
+function extractYamlAndContent(markdown: string): { header: AgentSpecHeader; content: string } {
+ const lines = markdown.split('\n');
+
+ if (lines[0] !== '---')
+ throw new Error('Markdown file must start with YAML front matter (---)');
+
+ let yamlEndIndex = -1;
+ for (let i = 1; i < lines.length; i++) {
+ if (lines[i] === '---') {
+ yamlEndIndex = i;
+ break;
+ }
+ }
+
+ if (yamlEndIndex === -1)
+ throw new Error('YAML front matter must be closed with ---');
+
+ const yamlLines = lines.slice(1, yamlEndIndex);
+ const yamlRaw = yamlLines.join('\n');
+ const contentLines = lines.slice(yamlEndIndex + 1);
+ const content = contentLines.join('\n');
+
+ let header: AgentSpecHeader;
+ try {
+ header = yaml.parse(yamlRaw) as AgentSpecHeader;
+ } catch (error: any) {
+ throw new Error(`Failed to parse YAML header: ${error.message}`);
+ }
+
+ if (!header.name)
+ throw new Error('YAML header must contain a "name" field');
+
+ if (!header.description)
+ throw new Error('YAML header must contain a "description" field');
+
+ return { header, content };
+}
+
+function extractInstructionsAndExamples(content: string): { instructions: string; examples: string[] } {
+ const examples: string[] = [];
+
+ const instructions = content.split('')[0].trim();
+ const exampleRegex = /([\s\S]*?)<\/example>/g;
+ let match: RegExpExecArray | null;
+
+ while ((match = exampleRegex.exec(content)) !== null) {
+ const example = match[1].trim();
+ examples.push(example.replace(/[\n]/g, ' ').replace(/ +/g, ' '));
+ }
+
+ return { instructions, examples };
+}
diff --git a/packages/playwright/src/agents/generateAgents.ts b/packages/playwright/src/agents/generateAgents.ts
index 5a5dff14a94b2..a8fc778e9876b 100644
--- a/packages/playwright/src/agents/generateAgents.ts
+++ b/packages/playwright/src/agents/generateAgents.ts
@@ -22,88 +22,15 @@ import { mkdirIfNeeded } from 'playwright-core/lib/utils';
import { FullConfigInternal } from '../common/config';
import { defaultSeedFile, findSeedFile, seedFileContent, seedProject } from '../mcp/test/seed';
+import { parseAgentSpec } from './agentParser';
-interface AgentHeader {
- name: string;
- description: string;
- model: string;
- color: string;
- tools: string[];
-}
-
-interface Agent {
- header: AgentHeader;
- instructions: string;
- examples: string[];
-}
+import type { AgentSpec } from './agent';
/* eslint-disable no-console */
-class AgentParser {
- static async loadAgents(): Promise {
- const files = await fs.promises.readdir(__dirname);
- return Promise.all(files.filter(file => file.endsWith('.agent.md')).map(file => AgentParser.parseFile(path.join(__dirname, file))));
- }
-
- static async parseFile(filePath: string): Promise {
- const source = await fs.promises.readFile(filePath, 'utf-8');
- const { header, content } = this.extractYamlAndContent(source);
- const { instructions, examples } = this.extractInstructionsAndExamples(content);
- return { header, instructions, examples };
- }
-
- static extractYamlAndContent(markdown: string): { header: AgentHeader; content: string } {
- const lines = markdown.split('\n');
-
- if (lines[0] !== '---')
- throw new Error('Markdown file must start with YAML front matter (---)');
-
- let yamlEndIndex = -1;
- for (let i = 1; i < lines.length; i++) {
- if (lines[i] === '---') {
- yamlEndIndex = i;
- break;
- }
- }
-
- if (yamlEndIndex === -1)
- throw new Error('YAML front matter must be closed with ---');
-
- const yamlLines = lines.slice(1, yamlEndIndex);
- const yamlRaw = yamlLines.join('\n');
- const contentLines = lines.slice(yamlEndIndex + 1);
- const content = contentLines.join('\n');
-
- let header: AgentHeader;
- try {
- header = yaml.parse(yamlRaw) as AgentHeader;
- } catch (error: any) {
- throw new Error(`Failed to parse YAML header: ${error.message}`);
- }
-
- if (!header.name)
- throw new Error('YAML header must contain a "name" field');
-
- if (!header.description)
- throw new Error('YAML header must contain a "description" field');
-
- return { header, content };
- }
-
- static extractInstructionsAndExamples(content: string): { instructions: string; examples: string[] } {
- const examples: string[] = [];
-
- const instructions = content.split('')[0].trim();
- const exampleRegex = /([\s\S]*?)<\/example>/g;
- let match: RegExpExecArray | null;
-
- while ((match = exampleRegex.exec(content)) !== null) {
- const example = match[1].trim();
- examples.push(example.replace(/[\n]/g, ' ').replace(/ +/g, ' '));
- }
-
- return { instructions, examples };
- }
+async function loadAgentSpecs(): Promise {
+ const files = await fs.promises.readdir(__dirname);
+ return Promise.all(files.filter(file => file.endsWith('.agent.md')).map(file => parseAgentSpec(path.join(__dirname, file))));
}
export class ClaudeGenerator {
@@ -112,11 +39,11 @@ export class ClaudeGenerator {
promptsFolder: prompts ? '.claude/prompts' : undefined,
});
- const agents = await AgentParser.loadAgents();
+ const agents = await loadAgentSpecs();
await fs.promises.mkdir('.claude/agents', { recursive: true });
for (const agent of agents)
- await writeFile(`.claude/agents/${agent.header.name}.md`, ClaudeGenerator.agentSpec(agent), '🤖', 'agent definition');
+ await writeFile(`.claude/agents/${agent.name}.md`, ClaudeGenerator.agentSpec(agent), '🤖', 'agent definition');
await writeFile('.mcp.json', JSON.stringify({
mcpServers: {
@@ -130,7 +57,7 @@ export class ClaudeGenerator {
initRepoDone();
}
- static agentSpec(agent: Agent): string {
+ static agentSpec(agent: AgentSpec): string {
const claudeToolMap = new Map([
['search', ['Glob', 'Grep', 'Read', 'LS']],
['edit', ['Edit', 'MultiEdit', 'Write']],
@@ -146,11 +73,11 @@ export class ClaudeGenerator {
const examples = agent.examples.length ? ` Examples: ${agent.examples.map(example => `${example}`).join('')}` : '';
const lines: string[] = [];
const header = {
- name: agent.header.name,
- description: agent.header.description + examples,
- tools: agent.header.tools.map(tool => asClaudeTool(tool)).join(', '),
- model: agent.header.model,
- color: agent.header.color,
+ name: agent.name,
+ description: agent.description + examples,
+ tools: agent.tools.map(tool => asClaudeTool(tool)).join(', '),
+ model: agent.model,
+ color: agent.color,
};
lines.push(`---`);
lines.push(yaml.stringify(header, { lineWidth: 100000 }) + `---`);
@@ -167,13 +94,13 @@ export class OpencodeGenerator {
promptsFolder: prompts ? '.opencode/prompts' : undefined
});
- const agents = await AgentParser.loadAgents();
+ const agents = await loadAgentSpecs();
for (const agent of agents) {
const prompt = [agent.instructions];
prompt.push('');
prompt.push(...agent.examples.map(example => `${example}`));
- await writeFile(`.opencode/prompts/${agent.header.name}.md`, prompt.join('\n'), '🤖', 'agent definition');
+ await writeFile(`.opencode/prompts/${agent.name}.md`, prompt.join('\n'), '🤖', 'agent definition');
}
await writeFile('opencode.json', OpencodeGenerator.configuration(agents), '🔧', 'opencode configuration');
@@ -181,7 +108,7 @@ export class OpencodeGenerator {
initRepoDone();
}
- static configuration(agents: Agent[]): string {
+ static configuration(agents: AgentSpec[]): string {
const opencodeToolMap = new Map([
['search', ['ls', 'glob', 'grep', 'read']],
['edit', ['edit', 'write']],
@@ -206,13 +133,13 @@ export class OpencodeGenerator {
result['agent'] = {};
for (const agent of agents) {
const tools: Record = {};
- result['agent'][agent.header.name] = {
- description: agent.header.description,
+ result['agent'][agent.name] = {
+ description: agent.description,
mode: 'subagent',
- prompt: `{file:.opencode/prompts/${agent.header.name}.md}`,
+ prompt: `{file:.opencode/prompts/${agent.name}.md}`,
tools,
};
- for (const tool of agent.header.tools)
+ for (const tool of agent.tools)
asOpencodeTool(tools, tool);
}
@@ -234,11 +161,11 @@ export class CopilotGenerator {
promptSuffix: 'prompt'
});
- const agents = await AgentParser.loadAgents();
+ const agents = await loadAgentSpecs();
await fs.promises.mkdir('.github/agents', { recursive: true });
for (const agent of agents)
- await writeFile(`.github/agents/${agent.header.name}.agent.md`, CopilotGenerator.agentSpec(agent), '🤖', 'agent definition');
+ await writeFile(`.github/agents/${agent.name}.agent.md`, CopilotGenerator.agentSpec(agent), '🤖', 'agent definition');
await deleteFile(`.github/chatmodes/ 🎠planner.chatmode.md`, 'legacy planner chatmode');
await deleteFile(`.github/chatmodes/🎠generator.chatmode.md`, 'legacy generator chatmode');
@@ -266,13 +193,13 @@ export class CopilotGenerator {
initRepoDone();
}
- static agentSpec(agent: Agent): string {
+ static agentSpec(agent: AgentSpec): string {
const examples = agent.examples.length ? ` Examples: ${agent.examples.map(example => `${example}`).join('')}` : '';
const lines: string[] = [];
const header = {
- 'name': agent.header.name,
- 'description': agent.header.description + examples,
- 'tools': agent.header.tools,
+ 'name': agent.name,
+ 'description': agent.description + examples,
+ 'tools': agent.tools,
'model': 'Claude Sonnet 4',
'mcp-servers': CopilotGenerator.mcpServers,
};
@@ -302,7 +229,7 @@ export class VSCodeGenerator {
await initRepo(config, projectName, {
promptsFolder: undefined
});
- const agents = await AgentParser.loadAgents();
+ const agents = await loadAgentSpecs();
const nameMap = new Map([
['playwright-test-planner', ' 🎠planner'],
@@ -312,7 +239,7 @@ export class VSCodeGenerator {
await fs.promises.mkdir('.github/chatmodes', { recursive: true });
for (const agent of agents)
- await writeFile(`.github/chatmodes/${nameMap.get(agent.header.name)}.chatmode.md`, VSCodeGenerator.agentSpec(agent), '🤖', 'chatmode definition');
+ await writeFile(`.github/chatmodes/${nameMap.get(agent.name)}.chatmode.md`, VSCodeGenerator.agentSpec(agent), '🤖', 'chatmode definition');
await VSCodeGenerator.appendToMCPJson();
@@ -343,7 +270,7 @@ export class VSCodeGenerator {
await writeFile(mcpJsonPath, JSON.stringify(mcpJson, null, 2), '🔧', 'mcp configuration');
}
- static agentSpec(agent: Agent): string {
+ static agentSpec(agent: AgentSpec): string {
const vscodeToolMap = new Map([
['search', ['search/listDirectory', 'search/fileSearch', 'search/textSearch']],
['read', ['search/readFile']],
@@ -359,7 +286,7 @@ export class VSCodeGenerator {
return `${vscodeMcpName}/${second}`;
return vscodeToolMap.get(first) || first;
}
- const tools = agent.header.tools.map(asVscodeTool).flat().sort((a, b) => {
+ const tools = agent.tools.map(asVscodeTool).flat().sort((a, b) => {
// VSCode insists on the specific tools order when editing agent config.
const indexA = vscodeToolsOrder.indexOf(a);
const indexB = vscodeToolsOrder.indexOf(b);
@@ -374,7 +301,7 @@ export class VSCodeGenerator {
const lines: string[] = [];
lines.push(`---`);
- lines.push(`description: ${agent.header.description}.`);
+ lines.push(`description: ${agent.description}.`);
lines.push(`tools: [${tools}]`);
lines.push(`---`);
lines.push('');
diff --git a/packages/playwright/src/agents/performTask.ts b/packages/playwright/src/agents/performTask.ts
new file mode 100644
index 0000000000000..78aecebd541f8
--- /dev/null
+++ b/packages/playwright/src/agents/performTask.ts
@@ -0,0 +1,59 @@
+/**
+ * Copyright Microsoft Corporation. All rights reserved.
+ *
+ * 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 { colors } from 'playwright-core/lib/utils';
+
+import { identityBrowserContextFactory } from '../mcp/browser/browserContextFactory';
+import { BrowserServerBackend } from '../mcp/browser/browserServerBackend';
+import { defaultConfig } from '../mcp/browser/config';
+import { Loop } from '../mcp/sdk/bundle';
+import { wrapInClient } from '../mcp/sdk/server';
+
+import type * as playwright from 'playwright-core';
+import type { CallToolResult } from '@modelcontextprotocol/sdk/types';
+
+export async function performTask(context: playwright.BrowserContext, task: string) {
+ const backend = new BrowserServerBackend(defaultConfig, identityBrowserContextFactory(context));
+ const client = await wrapInClient(backend, { name: 'Internal', version: '0.0.0' });
+ const loop = new Loop('copilot');
+
+ const callTool: (params: { name: string, arguments: any}) => Promise = async params => {
+ return await client.callTool(params) as CallToolResult;
+ };
+
+ try {
+ return await loop.run(task, {
+ tools: await backend.listTools(),
+ callTool,
+ logger,
+ });
+ } finally {
+ await client.close();
+ }
+}
+
+function logger(category: string, text: string, details = '') {
+ const trimmedText = trim(text, 100);
+ const trimmedDetails = trim(details, 100 - trimmedText.length - 1);
+ // eslint-disable-next-line no-console
+ console.log(colors.bold(colors.green(category)), trimmedText, colors.dim(trimmedDetails));
+}
+
+function trim(text: string, maxLength: number) {
+ if (text.length <= maxLength)
+ return text;
+ return text.slice(0, maxLength - 3) + '...';
+}
diff --git a/packages/playwright/src/index.ts b/packages/playwright/src/index.ts
index b8118c9039b11..5265de9cd4ebc 100644
--- a/packages/playwright/src/index.ts
+++ b/packages/playwright/src/index.ts
@@ -23,6 +23,7 @@ import { setBoxedStackPrefixes, createGuid, currentZone, debugMode, jsonStringif
import { currentTestInfo } from './common/globals';
import { rootTestType } from './common/testType';
import { createCustomMessageHandler } from './mcp/test/browserBackend';
+import { performTask } from './agents/performTask';
import type { Fixtures, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, ScreenshotMode, TestInfo, TestType, VideoMode } from '../types/test';
import type { ContextReuseMode } from './common/config';
@@ -57,6 +58,7 @@ type TestFixtures = PlaywrightTestArgs & PlaywrightTestOptions & {
_combinedContextOptions: BrowserContextOptions,
_setupContextOptions: void;
_setupArtifacts: void;
+ _perform: (task: string) => Promise;
_contextFactory: (options?: BrowserContextOptions) => Promise<{ context: BrowserContext, close: () => Promise }>;
};
@@ -458,6 +460,12 @@ const playwrightFixtures: Fixtures = ({
await request.dispose();
}
},
+
+ _perform: async ({ context }, use) => {
+ await use(async (task: string) => {
+ await performTask(context, task);
+ });
+ },
});
type ScreenshotOption = PlaywrightWorkerOptions['screenshot'] | undefined;
diff --git a/packages/playwright/src/mcp/browser/browserContextFactory.ts b/packages/playwright/src/mcp/browser/browserContextFactory.ts
index 5018b5bf9cc3f..bff1d664799e8 100644
--- a/packages/playwright/src/mcp/browser/browserContextFactory.ts
+++ b/packages/playwright/src/mcp/browser/browserContextFactory.ts
@@ -51,6 +51,17 @@ export interface BrowserContextFactory {
createContext(clientInfo: ClientInfo, abortSignal: AbortSignal, toolName: string | undefined): Promise;
}
+export function identityBrowserContextFactory(browserContext: playwright.BrowserContext): BrowserContextFactory {
+ return {
+ createContext: async (clientInfo: ClientInfo, abortSignal: AbortSignal, toolName: string | undefined) => {
+ return {
+ browserContext,
+ close: async () => {}
+ };
+ }
+ };
+}
+
class BaseContextFactory implements BrowserContextFactory {
readonly config: FullConfig;
private _logName: string;
diff --git a/packages/playwright/src/mcp/sdk/bundle.ts b/packages/playwright/src/mcp/sdk/bundle.ts
index 43fa2d39410cf..8a35b04fceace 100644
--- a/packages/playwright/src/mcp/sdk/bundle.ts
+++ b/packages/playwright/src/mcp/sdk/bundle.ts
@@ -31,6 +31,7 @@ const ListRootsRequestSchema: typeof import('@modelcontextprotocol/sdk/types.js'
const ProgressNotificationSchema: typeof import('@modelcontextprotocol/sdk/types.js').ProgressNotificationSchema = bundle.ProgressNotificationSchema;
const ListToolsRequestSchema: typeof import('@modelcontextprotocol/sdk/types.js').ListToolsRequestSchema = bundle.ListToolsRequestSchema;
const PingRequestSchema: typeof import('@modelcontextprotocol/sdk/types.js').PingRequestSchema = bundle.PingRequestSchema;
+const Loop: typeof import('tiny-loop').Loop = bundle.Loop;
const z: typeof import('zod') = bundle.z;
export {
@@ -48,5 +49,6 @@ export {
ListToolsRequestSchema,
PingRequestSchema,
ProgressNotificationSchema,
+ Loop,
z,
};
diff --git a/packages/playwright/src/mcp/sdk/proxyBackend.ts b/packages/playwright/src/mcp/sdk/proxyBackend.ts
index ba8fc790f40f9..327fc15f24b73 100644
--- a/packages/playwright/src/mcp/sdk/proxyBackend.ts
+++ b/packages/playwright/src/mcp/sdk/proxyBackend.ts
@@ -26,7 +26,7 @@ import type { Client } from '@modelcontextprotocol/sdk/client/index.js';
export type MCPProvider = {
name: string;
description: string;
- connect(): Promise;
+ connect(): Transport;
};
const errorsDebug = debug('pw:mcp:errors');
@@ -127,7 +127,7 @@ export class ProxyBackend implements ServerBackend {
client.setRequestHandler(mcpBundle.ListRootsRequestSchema, () => ({ roots: this._clientInfo?.roots || [] }));
client.setRequestHandler(mcpBundle.PingRequestSchema, () => ({}));
- const transport = await factory.connect();
+ const transport = factory.connect();
await client.connect(transport);
this._currentClient = client;
return client;
diff --git a/packages/playwright/src/mcp/sdk/server.ts b/packages/playwright/src/mcp/sdk/server.ts
index d6a60e3e14a14..48d42fb3811c9 100644
--- a/packages/playwright/src/mcp/sdk/server.ts
+++ b/packages/playwright/src/mcp/sdk/server.ts
@@ -27,6 +27,7 @@ import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
export type { Server } from '@modelcontextprotocol/sdk/server/index.js';
export type { Tool, CallToolResult, CallToolRequest, Root } from '@modelcontextprotocol/sdk/types.js';
import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
+import type { Client } from '@modelcontextprotocol/sdk/client/index.js';
const serverDebug = debug('pw:mcp:server');
const serverDebugResponse = debug('pw:mcp:server:response');
@@ -60,11 +61,20 @@ export async function connect(factory: ServerBackendFactory, transport: Transpor
await server.connect(transport);
}
-export async function wrapInProcess(backend: ServerBackend): Promise {
+export function wrapInProcess(backend: ServerBackend): Transport {
const server = createServer('Internal', '0.0.0', backend, false);
return new InProcessTransport(server);
}
+export async function wrapInClient(backend: ServerBackend, options: { name: string, version: string }): Promise {
+ const server = createServer('Internal', '0.0.0', backend, false);
+ const transport = new InProcessTransport(server);
+ const client = new mcpBundle.Client({ name: options.name, version: options.version });
+ await client.connect(transport);
+ await client.ping();
+ return client;
+}
+
export function createServer(name: string, version: string, backend: ServerBackend, runHeartbeat: boolean): Server {
const server = new mcpBundle.Server({ name, version }, {
capabilities: {
diff --git a/packages/playwright/src/mcp/test/DEPS.list b/packages/playwright/src/mcp/test/DEPS.list
index 385682de8cd58..4d49a78fb2bcd 100644
--- a/packages/playwright/src/mcp/test/DEPS.list
+++ b/packages/playwright/src/mcp/test/DEPS.list
@@ -2,6 +2,7 @@
../sdk/
../browser/config.ts
../browser/browserServerBackend.ts
+../browser/browserContextFactory.ts
../browser/response.ts
../browser/tab.ts
../browser/tools/
diff --git a/packages/playwright/src/mcp/test/browserBackend.ts b/packages/playwright/src/mcp/test/browserBackend.ts
index a362acda63b8f..12e8927ecddb7 100644
--- a/packages/playwright/src/mcp/test/browserBackend.ts
+++ b/packages/playwright/src/mcp/test/browserBackend.ts
@@ -19,10 +19,10 @@ import { defaultConfig } from '../browser/config';
import { BrowserServerBackend } from '../browser/browserServerBackend';
import { Tab } from '../browser/tab';
import { stripAnsiEscapes } from '../../util';
+import { identityBrowserContextFactory } from '../browser/browserContextFactory';
import type * as playwright from '../../../index';
import type { Page } from '../../../../playwright-core/src/client/page';
-import type { BrowserContextFactory } from '../browser/browserContextFactory';
import type { TestInfo } from '../../../test';
export type BrowserMCPRequest = {
@@ -45,7 +45,7 @@ export function createCustomMessageHandler(testInfo: TestInfo, context: playwrig
if (data.initialize) {
if (backend)
throw new Error('MCP backend is already initialized');
- backend = new BrowserServerBackend({ ...defaultConfig, capabilities: ['testing'] }, identityFactory(context));
+ backend = new BrowserServerBackend({ ...defaultConfig, capabilities: ['testing'] }, identityBrowserContextFactory(context));
await backend.initialize(data.initialize.clientInfo);
const pausedMessage = await generatePausedMessage(testInfo, context);
return { initialize: { pausedMessage } };
@@ -115,14 +115,3 @@ async function generatePausedMessage(testInfo: TestInfo, context: playwright.Bro
return lines.join('\n');
}
-
-function identityFactory(browserContext: playwright.BrowserContext): BrowserContextFactory {
- return {
- createContext: async (clientInfo: mcp.ClientInfo, abortSignal: AbortSignal, toolName: string | undefined) => {
- return {
- browserContext,
- close: async () => {}
- };
- }
- };
-}
diff --git a/packages/playwright/src/mcp/test/testBackend.ts b/packages/playwright/src/mcp/test/testBackend.ts
index e49c0322012d6..86d724df65191 100644
--- a/packages/playwright/src/mcp/test/testBackend.ts
+++ b/packages/playwright/src/mcp/test/testBackend.ts
@@ -42,15 +42,15 @@ export class TestServerBackend implements mcp.ServerBackend {
];
private _options: { muteConsole?: boolean, headless?: boolean };
private _context: TestContext | undefined;
- private _configOption: string | undefined;
+ private _configPath: string | undefined;
- constructor(configOption: string | undefined, options?: { muteConsole?: boolean, headless?: boolean }) {
+ constructor(configPath: string | undefined, options?: { muteConsole?: boolean, headless?: boolean }) {
this._options = options || {};
- this._configOption = configOption;
+ this._configPath = configPath;
}
async initialize(clientInfo: mcp.ClientInfo): Promise {
- this._context = new TestContext(clientInfo, this._configOption, this._options);
+ this._context = new TestContext(clientInfo, this._configPath, this._options);
}
async listTools(): Promise {
@@ -69,7 +69,7 @@ export class TestServerBackend implements mcp.ServerBackend {
}
serverClosed() {
- void this._context!.close();
+ void this._context?.close();
}
}
diff --git a/utils/build/build.js b/utils/build/build.js
index e7531892624c7..c6eae6eb79c65 100644
--- a/utils/build/build.js
+++ b/utils/build/build.js
@@ -260,7 +260,7 @@ bundles.push({
modulePath: 'packages/playwright/bundles/mcp',
outdir: 'packages/playwright/lib',
entryPoints: ['src/mcpBundleImpl.ts'],
- external: ['express'],
+ external: ['express', '@anthropic-ai/sdk'],
alias: {
'raw-body': 'raw-body.ts',
},