chore: brush up mcp servers (#35103)
This commit is contained in:
		
							parent
							
								
									07f54e7d8a
								
							
						
					
					
						commit
						a586a90e78
					
				|  | @ -127,22 +127,6 @@ | |||
|         "node": ">=6.0.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@anthropic-ai/sdk": { | ||||
|       "version": "0.33.1", | ||||
|       "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.33.1.tgz", | ||||
|       "integrity": "sha512-VrlbxiAdVRGuKP2UQlCnsShDHJKWepzvfRCkZMpU+oaUdKLpOfmylLMRojGrAgebV+kDtPjewCVP0laHXg+vsA==", | ||||
|       "dev": true, | ||||
|       "license": "MIT", | ||||
|       "dependencies": { | ||||
|         "@types/node": "^18.11.18", | ||||
|         "@types/node-fetch": "^2.6.4", | ||||
|         "abort-controller": "^3.0.0", | ||||
|         "agentkeepalive": "^4.2.1", | ||||
|         "form-data-encoder": "1.7.2", | ||||
|         "formdata-node": "^4.3.2", | ||||
|         "node-fetch": "^2.6.7" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@babel/cli": { | ||||
|       "version": "7.26.4", | ||||
|       "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.26.4.tgz", | ||||
|  | @ -1638,8 +1622,8 @@ | |||
|       "resolved": "packages/playwright-ct-vue", | ||||
|       "link": true | ||||
|     }, | ||||
|     "node_modules/@playwright/experimental-tools": { | ||||
|       "resolved": "packages/playwright-tools", | ||||
|     "node_modules/@playwright/mcp": { | ||||
|       "resolved": "packages/playwright-mcp", | ||||
|       "link": true | ||||
|     }, | ||||
|     "node_modules/@playwright/test": { | ||||
|  | @ -2078,17 +2062,6 @@ | |||
|         "undici-types": "~5.26.4" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@types/node-fetch": { | ||||
|       "version": "2.6.12", | ||||
|       "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz", | ||||
|       "integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==", | ||||
|       "dev": true, | ||||
|       "license": "MIT", | ||||
|       "dependencies": { | ||||
|         "@types/node": "*", | ||||
|         "form-data": "^4.0.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@types/prop-types": { | ||||
|       "version": "15.7.14", | ||||
|       "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", | ||||
|  | @ -2574,19 +2547,6 @@ | |||
|       "dev": true, | ||||
|       "license": "ISC" | ||||
|     }, | ||||
|     "node_modules/abort-controller": { | ||||
|       "version": "3.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", | ||||
|       "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", | ||||
|       "dev": true, | ||||
|       "license": "MIT", | ||||
|       "dependencies": { | ||||
|         "event-target-shim": "^5.0.0" | ||||
|       }, | ||||
|       "engines": { | ||||
|         "node": ">=6.5" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/accepts": { | ||||
|       "version": "2.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", | ||||
|  | @ -2643,19 +2603,6 @@ | |||
|         "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/agentkeepalive": { | ||||
|       "version": "4.6.0", | ||||
|       "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", | ||||
|       "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", | ||||
|       "dev": true, | ||||
|       "license": "MIT", | ||||
|       "dependencies": { | ||||
|         "humanize-ms": "^1.2.1" | ||||
|       }, | ||||
|       "engines": { | ||||
|         "node": ">= 8.0.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/ajv": { | ||||
|       "version": "6.12.6", | ||||
|       "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", | ||||
|  | @ -2926,13 +2873,6 @@ | |||
|         "node": ">= 0.4" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/asynckit": { | ||||
|       "version": "0.4.0", | ||||
|       "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", | ||||
|       "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", | ||||
|       "dev": true, | ||||
|       "license": "MIT" | ||||
|     }, | ||||
|     "node_modules/available-typed-arrays": { | ||||
|       "version": "1.0.7", | ||||
|       "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", | ||||
|  | @ -3335,19 +3275,6 @@ | |||
|         "node": ">=0.1.90" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/combined-stream": { | ||||
|       "version": "1.0.8", | ||||
|       "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", | ||||
|       "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", | ||||
|       "dev": true, | ||||
|       "license": "MIT", | ||||
|       "dependencies": { | ||||
|         "delayed-stream": "~1.0.0" | ||||
|       }, | ||||
|       "engines": { | ||||
|         "node": ">= 0.8" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/commander": { | ||||
|       "version": "6.2.1", | ||||
|       "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", | ||||
|  | @ -3689,16 +3616,6 @@ | |||
|         "url": "https://github.com/sponsors/ljharb" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/delayed-stream": { | ||||
|       "version": "1.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", | ||||
|       "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", | ||||
|       "dev": true, | ||||
|       "license": "MIT", | ||||
|       "engines": { | ||||
|         "node": ">=0.4.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/depd": { | ||||
|       "version": "2.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", | ||||
|  | @ -4492,16 +4409,6 @@ | |||
|         "node": ">= 0.6" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/event-target-shim": { | ||||
|       "version": "5.0.1", | ||||
|       "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", | ||||
|       "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", | ||||
|       "dev": true, | ||||
|       "license": "MIT", | ||||
|       "engines": { | ||||
|         "node": ">=6" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/eventsource": { | ||||
|       "version": "3.0.5", | ||||
|       "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.5.tgz", | ||||
|  | @ -4823,43 +4730,6 @@ | |||
|         "url": "https://github.com/sponsors/ljharb" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/form-data": { | ||||
|       "version": "4.0.2", | ||||
|       "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", | ||||
|       "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", | ||||
|       "dev": true, | ||||
|       "license": "MIT", | ||||
|       "dependencies": { | ||||
|         "asynckit": "^0.4.0", | ||||
|         "combined-stream": "^1.0.8", | ||||
|         "es-set-tostringtag": "^2.1.0", | ||||
|         "mime-types": "^2.1.12" | ||||
|       }, | ||||
|       "engines": { | ||||
|         "node": ">= 6" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/form-data-encoder": { | ||||
|       "version": "1.7.2", | ||||
|       "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", | ||||
|       "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", | ||||
|       "dev": true, | ||||
|       "license": "MIT" | ||||
|     }, | ||||
|     "node_modules/formdata-node": { | ||||
|       "version": "4.4.1", | ||||
|       "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", | ||||
|       "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", | ||||
|       "dev": true, | ||||
|       "license": "MIT", | ||||
|       "dependencies": { | ||||
|         "node-domexception": "1.0.0", | ||||
|         "web-streams-polyfill": "4.0.0-beta.3" | ||||
|       }, | ||||
|       "engines": { | ||||
|         "node": ">= 12.20" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/formidable": { | ||||
|       "version": "2.1.2", | ||||
|       "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.2.tgz", | ||||
|  | @ -5369,16 +5239,6 @@ | |||
|         "node": ">=10.19.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/humanize-ms": { | ||||
|       "version": "1.2.1", | ||||
|       "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", | ||||
|       "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", | ||||
|       "dev": true, | ||||
|       "license": "MIT", | ||||
|       "dependencies": { | ||||
|         "ms": "^2.0.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/iconv-lite": { | ||||
|       "version": "0.5.2", | ||||
|       "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.5.2.tgz", | ||||
|  | @ -6529,47 +6389,6 @@ | |||
|         "node": ">= 0.6" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/node-domexception": { | ||||
|       "version": "1.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", | ||||
|       "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", | ||||
|       "dev": true, | ||||
|       "funding": [ | ||||
|         { | ||||
|           "type": "github", | ||||
|           "url": "https://github.com/sponsors/jimmywarting" | ||||
|         }, | ||||
|         { | ||||
|           "type": "github", | ||||
|           "url": "https://paypal.me/jimmywarting" | ||||
|         } | ||||
|       ], | ||||
|       "license": "MIT", | ||||
|       "engines": { | ||||
|         "node": ">=10.5.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/node-fetch": { | ||||
|       "version": "2.7.0", | ||||
|       "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", | ||||
|       "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", | ||||
|       "dev": true, | ||||
|       "license": "MIT", | ||||
|       "dependencies": { | ||||
|         "whatwg-url": "^5.0.0" | ||||
|       }, | ||||
|       "engines": { | ||||
|         "node": "4.x || >=6.0.0" | ||||
|       }, | ||||
|       "peerDependencies": { | ||||
|         "encoding": "^0.1.0" | ||||
|       }, | ||||
|       "peerDependenciesMeta": { | ||||
|         "encoding": { | ||||
|           "optional": true | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/node-releases": { | ||||
|       "version": "2.0.19", | ||||
|       "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", | ||||
|  | @ -6801,37 +6620,6 @@ | |||
|         "wrappy": "1" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/openai": { | ||||
|       "version": "4.85.1", | ||||
|       "resolved": "https://registry.npmjs.org/openai/-/openai-4.85.1.tgz", | ||||
|       "integrity": "sha512-jkX2fntHljUvSH3MkWh4jShl10oNkb+SsCj4auKlbu2oF4KWAnmHLNR5EpnUHK1ZNW05Rp0fjbJzYwQzMsH8ZA==", | ||||
|       "dev": true, | ||||
|       "license": "Apache-2.0", | ||||
|       "dependencies": { | ||||
|         "@types/node": "^18.11.18", | ||||
|         "@types/node-fetch": "^2.6.4", | ||||
|         "abort-controller": "^3.0.0", | ||||
|         "agentkeepalive": "^4.2.1", | ||||
|         "form-data-encoder": "1.7.2", | ||||
|         "formdata-node": "^4.3.2", | ||||
|         "node-fetch": "^2.6.7" | ||||
|       }, | ||||
|       "bin": { | ||||
|         "openai": "bin/cli" | ||||
|       }, | ||||
|       "peerDependencies": { | ||||
|         "ws": "^8.18.0", | ||||
|         "zod": "^3.23.8" | ||||
|       }, | ||||
|       "peerDependenciesMeta": { | ||||
|         "ws": { | ||||
|           "optional": true | ||||
|         }, | ||||
|         "zod": { | ||||
|           "optional": true | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/optionator": { | ||||
|       "version": "0.9.4", | ||||
|       "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", | ||||
|  | @ -8345,13 +8133,6 @@ | |||
|         "node": ">=0.6" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/tr46": { | ||||
|       "version": "0.0.3", | ||||
|       "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", | ||||
|       "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", | ||||
|       "dev": true, | ||||
|       "license": "MIT" | ||||
|     }, | ||||
|     "node_modules/trace-viewer": { | ||||
|       "resolved": "packages/trace-viewer", | ||||
|       "link": true | ||||
|  | @ -9264,34 +9045,6 @@ | |||
|       "resolved": "packages/web", | ||||
|       "link": true | ||||
|     }, | ||||
|     "node_modules/web-streams-polyfill": { | ||||
|       "version": "4.0.0-beta.3", | ||||
|       "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", | ||||
|       "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", | ||||
|       "dev": true, | ||||
|       "license": "MIT", | ||||
|       "engines": { | ||||
|         "node": ">= 14" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/webidl-conversions": { | ||||
|       "version": "3.0.1", | ||||
|       "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", | ||||
|       "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", | ||||
|       "dev": true, | ||||
|       "license": "BSD-2-Clause" | ||||
|     }, | ||||
|     "node_modules/whatwg-url": { | ||||
|       "version": "5.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", | ||||
|       "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", | ||||
|       "dev": true, | ||||
|       "license": "MIT", | ||||
|       "dependencies": { | ||||
|         "tr46": "~0.0.3", | ||||
|         "webidl-conversions": "^3.0.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/which": { | ||||
|       "version": "2.0.2", | ||||
|       "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", | ||||
|  | @ -10308,6 +10061,20 @@ | |||
|         "node": ">=18" | ||||
|       } | ||||
|     }, | ||||
|     "packages/playwright-mcp": { | ||||
|       "name": "@playwright/mcp", | ||||
|       "version": "0.0.1", | ||||
|       "license": "Apache-2.0", | ||||
|       "dependencies": { | ||||
|         "playwright": "1.52.0-next" | ||||
|       }, | ||||
|       "devDependencies": { | ||||
|         "@modelcontextprotocol/sdk": "^1.6.1" | ||||
|       }, | ||||
|       "engines": { | ||||
|         "node": ">=18" | ||||
|       } | ||||
|     }, | ||||
|     "packages/playwright-test": { | ||||
|       "name": "@playwright/test", | ||||
|       "version": "1.52.0-next", | ||||
|  | @ -10325,6 +10092,7 @@ | |||
|     "packages/playwright-tools": { | ||||
|       "name": "@playwright/experimental-tools", | ||||
|       "version": "0.0.0", | ||||
|       "extraneous": true, | ||||
|       "license": "Apache-2.0", | ||||
|       "dependencies": { | ||||
|         "playwright": "1.52.0-next" | ||||
|  |  | |||
|  | @ -0,0 +1,33 @@ | |||
| { | ||||
|   "name": "@playwright/mcp", | ||||
|   "private": true, | ||||
|   "version": "0.0.1", | ||||
|   "description": "Playwright Tools for MCP", | ||||
|   "repository": { | ||||
|     "type": "git", | ||||
|     "url": "git+https://github.com/microsoft/playwright.git" | ||||
|   }, | ||||
|   "homepage": "https://playwright.dev", | ||||
|   "engines": { | ||||
|     "node": ">=18" | ||||
|   }, | ||||
|   "author": { | ||||
|     "name": "Microsoft Corporation" | ||||
|   }, | ||||
|   "license": "Apache-2.0", | ||||
|   "exports": { | ||||
|     "./servers/server": "./lib/servers/server.js", | ||||
|     "./servers/screenshot": "./lib/servers/screenshot.js", | ||||
|     "./servers/snapshot": "./lib/servers/snapshot.js", | ||||
|     "./tools/common": "./lib/tools/common.js", | ||||
|     "./tools/screenshot": "./lib/tools/screenshot.js", | ||||
|     "./tools/snapshot": "./lib/tools/snapshot.js", | ||||
|     "./package.json": "./package.json" | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "playwright": "1.52.0-next" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@modelcontextprotocol/sdk": "^1.6.1" | ||||
|   } | ||||
| } | ||||
|  | @ -14,6 +14,22 @@ | |||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| const { schema, call, snapshot } = require('./lib/tools/browser'); | ||||
| import { Server } from './server'; | ||||
| import { navigate, wait, pressKey } from '../tools/common'; | ||||
| import { screenshot, moveMouse, click, drag, type } from '../tools/screenshot'; | ||||
| 
 | ||||
| module.exports = { schema, call, snapshot }; | ||||
| const server = new Server({ | ||||
|   name: 'Playwright screenshot-based browser server', | ||||
|   version: '0.0.1', | ||||
|   tools: [ | ||||
|     navigate, | ||||
|     screenshot, | ||||
|     moveMouse, | ||||
|     click, | ||||
|     drag, | ||||
|     type, | ||||
|     pressKey, | ||||
|     wait, | ||||
|   ] | ||||
| }); | ||||
| server.start(); | ||||
|  | @ -0,0 +1,96 @@ | |||
| /** | ||||
|  * 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 { Server as MCPServer } from '@modelcontextprotocol/sdk/server/index.js'; | ||||
| import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; | ||||
| import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'; | ||||
| import * as playwright from 'playwright'; | ||||
| 
 | ||||
| import type { Tool } from '../tools/common'; | ||||
| 
 | ||||
| export class Server { | ||||
|   private _server: MCPServer; | ||||
|   private _tools: Tool[]; | ||||
|   private _page: playwright.Page | undefined; | ||||
| 
 | ||||
|   constructor(options: { name: string, version: string, tools: Tool[] }) { | ||||
|     const { name, version, tools } = options; | ||||
|     this._server = new MCPServer({ name, version }, { capabilities: { tools: {} } }); | ||||
|     this._tools = tools; | ||||
| 
 | ||||
|     this._server.setRequestHandler(ListToolsRequestSchema, async () => { | ||||
|       return { tools: tools.map(tool => tool.schema) }; | ||||
|     }); | ||||
| 
 | ||||
|     this._server.setRequestHandler(CallToolRequestSchema, async request => { | ||||
|       const page = await this._openPage(); | ||||
| 
 | ||||
|       const tool = this._tools.find(tool => tool.schema.name === request.params.name); | ||||
|       if (!tool) { | ||||
|         return { | ||||
|           content: [{ type: 'text', text: `Tool "${request.params.name}" not found` }], | ||||
|           isError: true, | ||||
|         }; | ||||
|       } | ||||
| 
 | ||||
|       try { | ||||
|         const result = await tool.handle({ page }, request.params.arguments); | ||||
|         return result; | ||||
|       } catch (error) { | ||||
|         return { | ||||
|           content: [{ type: 'text', text: String(error) }], | ||||
|           isError: true, | ||||
|         }; | ||||
|       } | ||||
|     }); | ||||
| 
 | ||||
|     this._setupExitWatchdog(); | ||||
|   } | ||||
| 
 | ||||
|   start() { | ||||
|     const transport = new StdioServerTransport(); | ||||
|     void this._server.connect(transport); | ||||
|   } | ||||
| 
 | ||||
|   private async _createBrowser(): Promise<playwright.Browser> { | ||||
|     if (process.env.PLAYWRIGHT_WS_ENDPOINT) { | ||||
|       return await playwright.chromium.connect( | ||||
|           process.env.PLAYWRIGHT_WS_ENDPOINT | ||||
|       ); | ||||
|     } | ||||
|     return await playwright.chromium.launch({ headless: false }); | ||||
|   } | ||||
| 
 | ||||
|   private async _openPage(): Promise<playwright.Page> { | ||||
|     if (!this._page) { | ||||
|       const browser = await this._createBrowser(); | ||||
|       const context = await browser.newContext(); | ||||
|       this._page = await context.newPage(); | ||||
|     } | ||||
|     return this._page; | ||||
|   } | ||||
| 
 | ||||
|   private _setupExitWatchdog() { | ||||
|     process.stdin.on('close', async () => { | ||||
|       this._server.close(); | ||||
|       // eslint-disable-next-line no-restricted-properties
 | ||||
|       setTimeout(() => process.exit(0), 15000); | ||||
|       await this._page?.context()?.browser()?.close(); | ||||
|       // eslint-disable-next-line no-restricted-properties
 | ||||
|       process.exit(0); | ||||
|     }); | ||||
|   } | ||||
| } | ||||
|  | @ -14,14 +14,21 @@ | |||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| import type playwright from 'playwright'; | ||||
| import { Server } from './server'; | ||||
| import { wait, pressKey } from '../tools/common'; | ||||
| import { navigate, snapshot, click, hover, type } from '../tools/snapshot'; | ||||
| 
 | ||||
| export type JSONSchemaType = string | number | boolean | JSONSchemaObject | JSONSchemaArray | null; | ||||
| interface JSONSchemaObject { [key: string]: JSONSchemaType; } | ||||
| interface JSONSchemaArray extends Array<JSONSchemaType> {} | ||||
| 
 | ||||
| export type ToolDeclaration = { | ||||
|   name: string; | ||||
|   description: string; | ||||
|   parameters: any; | ||||
| }; | ||||
| const server = new Server({ | ||||
|   name: 'Playwright snapshot-based browser server', | ||||
|   version: '0.0.1', | ||||
|   tools: [ | ||||
|     navigate, | ||||
|     snapshot, | ||||
|     click, | ||||
|     hover, | ||||
|     type, | ||||
|     pressKey, | ||||
|     wait, | ||||
|   ] | ||||
| }); | ||||
| server.start(); | ||||
|  | @ -0,0 +1,124 @@ | |||
| /** | ||||
|  * 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 { waitForCompletion } from '../utils'; | ||||
| 
 | ||||
| import type * as playwright from 'playwright'; | ||||
| import type { ImageContent, TextContent } from '@modelcontextprotocol/sdk/types'; | ||||
| 
 | ||||
| export type ToolContext = { | ||||
|   page: playwright.Page; | ||||
| }; | ||||
| 
 | ||||
| export type ToolSchema = { | ||||
|   name: string; | ||||
|   description: string; | ||||
|   inputSchema: Record<string, any>; | ||||
| }; | ||||
| 
 | ||||
| export type ToolResult = { | ||||
|   content: (ImageContent | TextContent)[]; | ||||
|   isError?: boolean; | ||||
| }; | ||||
| 
 | ||||
| export type Tool = { | ||||
|   schema: ToolSchema; | ||||
|   handle: (context: ToolContext, params?: Record<string, any>) => Promise<ToolResult>; | ||||
| }; | ||||
| 
 | ||||
| export const navigate: Tool = { | ||||
|   schema: { | ||||
|     name: 'navigate', | ||||
|     description: 'Navigate to a URL', | ||||
|     inputSchema: { | ||||
|       type: 'object', | ||||
|       properties: { | ||||
|         url: { | ||||
|           type: 'string', | ||||
|           description: 'URL to navigate to', | ||||
|         }, | ||||
|       }, | ||||
|     } | ||||
|   }, | ||||
| 
 | ||||
|   handle: async (context, params) => { | ||||
|     await waitForCompletion(context.page, async () => { | ||||
|       await context.page.goto(params!.url as string); | ||||
|     }); | ||||
|     return { | ||||
|       content: [{ | ||||
|         type: 'text', | ||||
|         text: `Navigated to ${params!.url}`, | ||||
|       }], | ||||
|     }; | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| export const wait: Tool = { | ||||
|   schema: { | ||||
|     name: 'wait', | ||||
|     description: `Wait for given amount of time to see if the page updates. Use it after action if you think page is not ready yet`, | ||||
|     inputSchema: { | ||||
|       type: 'object', | ||||
|       properties: { | ||||
|         time: { | ||||
|           type: 'integer', | ||||
|           description: 'Time to wait in seconds', | ||||
|         }, | ||||
|       }, | ||||
|       required: ['time'], | ||||
|     } | ||||
|   }, | ||||
| 
 | ||||
|   handle: async (context, params) => { | ||||
|     await context.page.waitForTimeout(Math.min(10000, params!.time as number * 1000)); | ||||
|     return { | ||||
|       content: [{ | ||||
|         type: 'text', | ||||
|         text: `Waited for ${params!.time} seconds`, | ||||
|       }], | ||||
|     }; | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| export const pressKey: Tool = { | ||||
|   schema: { | ||||
|     name: 'press_key', | ||||
|     description: 'Press a key', | ||||
|     inputSchema: { | ||||
|       type: 'object', | ||||
|       properties: { | ||||
|         key: { | ||||
|           type: 'string', | ||||
|           description: 'Name of the key to press or a character to generate, such as `ArrowLeft` or `a`', | ||||
|         }, | ||||
|       }, | ||||
|       required: ['key'], | ||||
|     } | ||||
|   }, | ||||
| 
 | ||||
|   handle: async (context, params) => { | ||||
|     await waitForCompletion(context.page, async () => { | ||||
|       await context.page.keyboard.press(params!.key as string); | ||||
|     }); | ||||
|     return { | ||||
|       content: [{ | ||||
|         type: 'text', | ||||
|         text: `Pressed key ${params!.key}`, | ||||
|       }], | ||||
|     }; | ||||
|   } | ||||
| }; | ||||
|  | @ -0,0 +1,143 @@ | |||
| /** | ||||
|  * 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 { Tool } from './common'; | ||||
| import { waitForCompletion } from '../utils'; | ||||
| 
 | ||||
| export const screenshot: Tool = { | ||||
|   schema: { | ||||
|     name: 'screenshot', | ||||
|     description: 'Take a screenshot of the current page', | ||||
|     inputSchema: { | ||||
|       type: 'object', | ||||
|       properties: {}, | ||||
|     } | ||||
|   }, | ||||
| 
 | ||||
|   handle: async context => { | ||||
|     const screenshot = await context.page.screenshot({ type: 'jpeg', quality: 50, scale: 'css' }); | ||||
|     return { | ||||
|       content: [{ type: 'image', data: screenshot.toString('base64'), mimeType: 'image/jpeg' }], | ||||
|     }; | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| export const moveMouse: Tool = { | ||||
|   schema: { | ||||
|     name: 'move_mouse', | ||||
|     description: 'Move mouse to a given position', | ||||
|     inputSchema: { | ||||
|       type: 'object', | ||||
|       properties: { | ||||
|         x: { | ||||
|           type: 'number', | ||||
|           description: 'X coordinate', | ||||
|         }, | ||||
|         y: { | ||||
|           type: 'number', | ||||
|           description: 'Y coordinate', | ||||
|         }, | ||||
|       }, | ||||
|       required: ['x', 'y'], | ||||
|     } | ||||
|   }, | ||||
| 
 | ||||
|   handle: async (context, params) => { | ||||
|     await context.page.mouse.move(params!.x as number, params!.y as number); | ||||
|     return { | ||||
|       content: [{ type: 'text', text: `Moved mouse to (${params!.x}, ${params!.y})` }], | ||||
|     }; | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| export const click: Tool = { | ||||
|   schema: { | ||||
|     name: 'click', | ||||
|     description: 'Click left mouse button', | ||||
|     inputSchema: { | ||||
|       type: 'object', | ||||
|       properties: {}, | ||||
|     } | ||||
|   }, | ||||
| 
 | ||||
|   handle: async context => { | ||||
|     await waitForCompletion(context.page, async () => { | ||||
|       await context.page.mouse.down(); | ||||
|       await context.page.mouse.up(); | ||||
|     }); | ||||
|     return { | ||||
|       content: [{ type: 'text', text: 'Clicked mouse' }], | ||||
|     }; | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| export const drag: Tool = { | ||||
|   schema: { | ||||
|     name: 'drag', | ||||
|     description: 'Drag left mouse button', | ||||
|     inputSchema: { | ||||
|       type: 'object', | ||||
|       properties: { | ||||
|         x: { | ||||
|           type: 'number', | ||||
|           description: 'X coordinate', | ||||
|         }, | ||||
|         y: { | ||||
|           type: 'number', | ||||
|           description: 'Y coordinate', | ||||
|         }, | ||||
|       }, | ||||
|       required: ['x', 'y'], | ||||
|     } | ||||
|   }, | ||||
| 
 | ||||
|   handle: async (context, params) => { | ||||
|     await waitForCompletion(context.page, async () => { | ||||
|       await context.page.mouse.down(); | ||||
|       await context.page.mouse.move(params!.x as number, params!.y as number); | ||||
|       await context.page.mouse.up(); | ||||
|     }); | ||||
|     return { | ||||
|       content: [{ type: 'text', text: `Dragged mouse to (${params!.x}, ${params!.y})` }], | ||||
|     }; | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| export const type: Tool = { | ||||
|   schema: { | ||||
|     name: 'type', | ||||
|     description: 'Type text', | ||||
|     inputSchema: { | ||||
|       type: 'object', | ||||
|       properties: { | ||||
|         text: { | ||||
|           type: 'string', | ||||
|           description: 'Text to type', | ||||
|         }, | ||||
|       }, | ||||
|       required: ['text'], | ||||
|     } | ||||
|   }, | ||||
| 
 | ||||
|   handle: async (context, params) => { | ||||
|     await waitForCompletion(context.page, async () => { | ||||
|       await context.page.keyboard.type(params!.text as string); | ||||
|     }); | ||||
|     return { | ||||
|       content: [{ type: 'text', text: `Typed text "${params!.text}"` }], | ||||
|     }; | ||||
|   } | ||||
| }; | ||||
|  | @ -0,0 +1,148 @@ | |||
| /** | ||||
|  * 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 { waitForCompletion } from '../utils'; | ||||
| 
 | ||||
| import type * as playwright from 'playwright'; | ||||
| import type { Tool, ToolContext, ToolResult } from './common'; | ||||
| 
 | ||||
| const elementIdProperty = { | ||||
|   elementId: { | ||||
|     type: 'number', | ||||
|     description: 'Target element', | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| export const snapshot: Tool = { | ||||
|   schema: { | ||||
|     name: 'snapshot', | ||||
|     description: 'Capture accessibility snapshot of the current page, this is better than screenshot', | ||||
|     inputSchema: { | ||||
|       type: 'object', | ||||
|       properties: {}, | ||||
|     } | ||||
|   }, | ||||
| 
 | ||||
|   handle: async context => { | ||||
|     return await captureAriaSnapshot(context.page); | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| export const navigate: Tool = { | ||||
|   schema: { | ||||
|     name: 'navigate', | ||||
|     description: 'Navigate to a URL', | ||||
|     inputSchema: { | ||||
|       type: 'object', | ||||
|       properties: { | ||||
|         url: { | ||||
|           type: 'string', | ||||
|           description: 'URL to navigate to', | ||||
|         }, | ||||
|       }, | ||||
|     } | ||||
|   }, | ||||
| 
 | ||||
|   handle: async (context, params) => { | ||||
|     return runAndCaptureSnapshot(context, () => context.page.goto(params!.url)); | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| export const click: Tool = { | ||||
|   schema: { | ||||
|     name: 'click', | ||||
|     description: 'Perform click on a web page', | ||||
|     inputSchema: { | ||||
|       type: 'object', | ||||
|       properties: { | ||||
|         ...elementIdProperty, | ||||
|       }, | ||||
|       required: ['elementId'], | ||||
|     } | ||||
|   }, | ||||
| 
 | ||||
|   handle: async (context, params) => { | ||||
|     const locator = elementIdLocator(context.page, params!); | ||||
|     return runAndCaptureSnapshot(context, () => locator.click()); | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| export const hover: Tool = { | ||||
|   schema: { | ||||
|     name: 'hover', | ||||
|     description: 'Hover over element on page', | ||||
|     inputSchema: { | ||||
|       type: 'object', | ||||
|       properties: { | ||||
|         ...elementIdProperty, | ||||
|       }, | ||||
|       required: ['elementId'], | ||||
|     } | ||||
|   }, | ||||
| 
 | ||||
|   handle: async (context, params) => { | ||||
|     const locator = elementIdLocator(context.page, params!); | ||||
|     return runAndCaptureSnapshot(context, () => locator.hover()); | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| export const type: Tool = { | ||||
|   schema: { | ||||
|     name: 'type', | ||||
|     description: 'Type text into editable element', | ||||
|     inputSchema: { | ||||
|       type: 'object', | ||||
|       properties: { | ||||
|         ...elementIdProperty, | ||||
|         text: { | ||||
|           type: 'string', | ||||
|           description: 'Text to enter', | ||||
|         }, | ||||
|         submit: { | ||||
|           type: 'boolean', | ||||
|           description: 'Whether to submit entered text (press Enter after)' | ||||
|         } | ||||
|       }, | ||||
|       required: ['elementId', 'text'], | ||||
|     } | ||||
|   }, | ||||
| 
 | ||||
|   handle: async (context, params) => { | ||||
|     const locator = elementIdLocator(context.page, params!); | ||||
|     return await runAndCaptureSnapshot(context, async () => { | ||||
|       locator.fill(params!.text as string); | ||||
|       if (params!.submit) | ||||
|         await locator.press('Enter'); | ||||
|     }); | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| function elementIdLocator(page: playwright.Page, params: Record<string, string>): playwright.Locator { | ||||
|   return page.locator(`internal:aria-id=${params.elementId}`); | ||||
| } | ||||
| 
 | ||||
| async function runAndCaptureSnapshot(context: ToolContext, callback: () => Promise<any>): Promise<ToolResult> { | ||||
|   const page = context.page; | ||||
|   await waitForCompletion(page, () => callback()); | ||||
|   return captureAriaSnapshot(page); | ||||
| } | ||||
| 
 | ||||
| async function captureAriaSnapshot(page: playwright.Page): Promise<ToolResult> { | ||||
|   const snapshot = await page.locator('html').ariaSnapshot({ _id: true } as any); | ||||
|   return { | ||||
|     content: [{ type: 'text', text: `# Current page snapshot\n${snapshot}` }], | ||||
|   }; | ||||
| } | ||||
|  | @ -1,12 +1,11 @@ | |||
| /** | ||||
|  * Copyright 2017 Google Inc. All rights reserved. | ||||
|  * Modifications copyright (c) Microsoft Corporation. | ||||
|  * 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
 | ||||
|  * 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, | ||||
|  | @ -15,20 +14,19 @@ | |||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| import { ManualPromise } from 'playwright-core/lib/utils'; | ||||
| import type * as playwright from 'playwright'; | ||||
| 
 | ||||
| import type playwright from 'playwright'; | ||||
| 
 | ||||
| export async function waitForNetwork<R>(page: playwright.Page, callback: () => Promise<R>): Promise<R> { | ||||
| export async function waitForCompletion<R>(page: playwright.Page, callback: () => Promise<R>): Promise<R> { | ||||
|   const requests = new Set<playwright.Request>(); | ||||
|   let frameNavigated = false; | ||||
|   const waitBarrier = new ManualPromise(); | ||||
|   let waitCallback: () => void = () => {}; | ||||
|   const waitBarrier = new Promise<void>(f => { waitCallback = f; }); | ||||
| 
 | ||||
|   const requestListener = (request: playwright.Request) => requests.add(request); | ||||
|   const requestFinishedListener = (request: playwright.Request) => { | ||||
|     requests.delete(request); | ||||
|     if (!requests.size) | ||||
|       waitBarrier.resolve(); | ||||
|       waitCallback(); | ||||
|   }; | ||||
| 
 | ||||
|   const frameNavigateListener = (frame: playwright.Frame) => { | ||||
|  | @ -38,13 +36,13 @@ export async function waitForNetwork<R>(page: playwright.Page, callback: () => P | |||
|     dispose(); | ||||
|     clearTimeout(timeout); | ||||
|     void frame.waitForLoadState('load').then(() => { | ||||
|       waitBarrier.resolve(); | ||||
|       waitCallback(); | ||||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   const onTimeout = () => { | ||||
|     dispose(); | ||||
|     waitBarrier.resolve(); | ||||
|     waitCallback(); | ||||
|   }; | ||||
| 
 | ||||
|   page.on('request', requestListener); | ||||
|  | @ -62,7 +60,7 @@ export async function waitForNetwork<R>(page: playwright.Page, callback: () => P | |||
|   try { | ||||
|     const result = await callback(); | ||||
|     if (!requests.size && !frameNavigated) | ||||
|       waitBarrier.resolve(); | ||||
|       waitCallback(); | ||||
|     await waitBarrier; | ||||
|     await page.evaluate(() => new Promise(f => setTimeout(f, 1000))); | ||||
|     return result; | ||||
|  | @ -1,30 +0,0 @@ | |||
| /** | ||||
|  * Copyright (c) Microsoft Corporation. | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0
 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| import type playwright from 'playwright'; | ||||
| import { ToolDeclaration, JSONSchemaType } from './types'; | ||||
| 
 | ||||
| export type ToolResult = { | ||||
|   error?: string; | ||||
|   code: Array<string>; | ||||
|   snapshot: string; | ||||
| } | ||||
| 
 | ||||
| export type ToolCall = (page: playwright.Page, tool: string, parameters: { [key: string]: JSONSchemaType; }) => Promise<ToolResult>; | ||||
| 
 | ||||
| export const schema: ToolDeclaration[]; | ||||
| export const call: ToolCall; | ||||
| export const snapshot: (page) => Promise<string>; | ||||
|  | @ -1,28 +0,0 @@ | |||
| /** | ||||
|  * Copyright (c) Microsoft Corporation. | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0
 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| import type playwright from 'playwright'; | ||||
| import { JSONSchemaType } from './types'; | ||||
| 
 | ||||
| export type ToolResult = { | ||||
|   output?: string; | ||||
|   error?: string; | ||||
|   base64_image?: string; | ||||
| }; | ||||
| 
 | ||||
| export type ToolCall = (page: playwright.Page, tool: string, parameters: { [key: string]: JSONSchemaType; }) => Promise<ToolResult>; | ||||
| 
 | ||||
| export const call: ToolCall; | ||||
|  | @ -1,19 +0,0 @@ | |||
| /** | ||||
|  * 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. | ||||
|  */ | ||||
| 
 | ||||
| const { call } = require('./lib/tools/computer-20241022'); | ||||
| 
 | ||||
| module.exports = { call }; | ||||
|  | @ -1,37 +0,0 @@ | |||
| { | ||||
|   "name": "@playwright/experimental-tools", | ||||
|   "private": true, | ||||
|   "version": "0.0.0", | ||||
|   "description": "Playwright Tools for AI", | ||||
|   "repository": { | ||||
|     "type": "git", | ||||
|     "url": "git+https://github.com/microsoft/playwright.git" | ||||
|   }, | ||||
|   "homepage": "https://playwright.dev", | ||||
|   "engines": { | ||||
|     "node": ">=18" | ||||
|   }, | ||||
|   "author": { | ||||
|     "name": "Microsoft Corporation" | ||||
|   }, | ||||
|   "license": "Apache-2.0", | ||||
|   "exports": { | ||||
|     "./browser": { | ||||
|       "types": "./browser.d.ts", | ||||
|       "default": "./browser.js" | ||||
|     }, | ||||
|     "./computer-20241022": { | ||||
|       "types": "./computer-20241022.d.ts", | ||||
|       "default": "./computer-20241022.js" | ||||
|     }, | ||||
|     "./package.json": "./package.json" | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "playwright": "1.52.0-next" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@anthropic-ai/sdk": "^0.33.1", | ||||
|     "@modelcontextprotocol/sdk": "^1.6.1", | ||||
|     "openai": "^4.79.1" | ||||
|   } | ||||
| } | ||||
|  | @ -1,143 +0,0 @@ | |||
| /** | ||||
|  * 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. | ||||
|  */ | ||||
| 
 | ||||
| /* eslint-disable no-console */ | ||||
| 
 | ||||
| import Anthropic from '@anthropic-ai/sdk'; | ||||
| import browser from '@playwright/experimental-tools/browser'; | ||||
| import dotenv from 'dotenv'; | ||||
| import playwright from 'playwright'; | ||||
| 
 | ||||
| dotenv.config(); | ||||
| 
 | ||||
| const anthropic = new Anthropic(); | ||||
| 
 | ||||
| export const system = ` | ||||
| You are a web tester. | ||||
| 
 | ||||
| <Instructions> | ||||
| - Perform test according to the provided checklist | ||||
| - Use browser tools to perform actions on web page | ||||
| - Never ask questions, always perform a best guess action | ||||
| - Use one tool at a time, wait for its result before proceeding. | ||||
| - When ready use "reportResult" tool to report result | ||||
| </Instructions>`;
 | ||||
| 
 | ||||
| const reportTool: Anthropic.Tool = { | ||||
|   name: 'reportResult', | ||||
|   description: 'Submit test result', | ||||
|   input_schema: { | ||||
|     type: 'object', | ||||
|     properties: { | ||||
|       'success': { type: 'boolean', description: 'Whether test passed' }, | ||||
|       'result': { type: 'string', description: 'Result of the test if some information has been requested' }, | ||||
|       'error': { type: 'string', description: 'Error message if test failed' } | ||||
|     }, | ||||
|     required: ['success'] | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| type Message = Anthropic.Beta.Messages.BetaMessageParam & { | ||||
|   history: Anthropic.Beta.Messages.BetaMessageParam['content'] | ||||
| }; | ||||
| 
 | ||||
| async function anthropicAgentLoop(page: playwright.Page, task: string) { | ||||
|   // Convert them into tools for Anthropic.
 | ||||
|   const pageTools: Anthropic.Tool[] = browser.schema.map(tool => { | ||||
|     return { | ||||
|       name: tool.name, | ||||
|       description: tool.description, | ||||
|       input_schema: tool.parameters as any, | ||||
|     }; | ||||
|   }); | ||||
| 
 | ||||
|   // Add report tool.
 | ||||
|   const tools = [reportTool, ...pageTools]; | ||||
| 
 | ||||
|   const history: Message[] = [{ | ||||
|     role: 'user', | ||||
|     history: `Task: ${task}`, | ||||
|     content: `Task: ${task}\n\n${await browser.snapshot(page)}`, | ||||
|   }]; | ||||
| 
 | ||||
|   // Run agentic loop, cap steps.
 | ||||
|   for (let i = 0; i < 50; i++) { | ||||
|     const response = await anthropic.messages.create({ | ||||
|       model: 'claude-3-5-sonnet-20241022', | ||||
|       max_tokens: 1024, | ||||
|       temperature: 0, | ||||
|       tools, | ||||
|       system, | ||||
|       messages: toAnthropicMessages(history), | ||||
|     }); | ||||
|     history.push({ role: 'assistant', content: response.content, history: response.content }); | ||||
| 
 | ||||
|     const toolUse = response.content.find(block => block.type === 'tool_use'); | ||||
|     if (!toolUse) { | ||||
|       history.push({ role: 'user', content: 'expected exactly one tool call', history: 'expected exactly one tool call' }); | ||||
|       continue; | ||||
|     } | ||||
| 
 | ||||
|     if (toolUse.name === 'reportResult') { | ||||
|       console.log(toolUse.input); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     // Run the Playwright tool.
 | ||||
|     const { error, snapshot, code } = await browser.call(page, toolUse.name, toolUse.input as any); | ||||
|     if (code.length) | ||||
|       console.log(code.join('\n')); | ||||
| 
 | ||||
|     // Report the result.
 | ||||
|     const resultText = error ? `Error: ${error}\n` : 'Done\n'; | ||||
|     history.push({ | ||||
|       role: 'user', | ||||
|       content: [{ | ||||
|         type: 'tool_result', | ||||
|         tool_use_id: toolUse.id, | ||||
|         content: [{ type: 'text', text: resultText + snapshot }], | ||||
|       }], | ||||
|       history: [{ | ||||
|         type: 'tool_result', | ||||
|         tool_use_id: toolUse.id, | ||||
|         content: [{ type: 'text', text: resultText }], | ||||
|       }], | ||||
|     }); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function toAnthropicMessages(messages: Message[]): Anthropic.Beta.Messages.BetaMessageParam[] { | ||||
|   return messages.map((message, i) => { | ||||
|     if (i === messages.length - 1) | ||||
|       return { ...message, history: undefined }; | ||||
|     return { ...message, content: message.history, history: undefined }; | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| async function main() { | ||||
|   const browser = await playwright.chromium.launch({ headless: false }); | ||||
|   const page = await browser.newPage(); | ||||
|   await anthropicAgentLoop(page, ` | ||||
|     - Go to http://github.com/microsoft
 | ||||
|     - Search for "playwright" repository | ||||
|     - Navigate to it | ||||
|     - Switch into the Issues tab | ||||
|     - Report 3 first issues | ||||
|   `);
 | ||||
|   await browser.close(); | ||||
| } | ||||
| 
 | ||||
| void main(); | ||||
|  | @ -1,157 +0,0 @@ | |||
| /** | ||||
|  * 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. | ||||
|  */ | ||||
| 
 | ||||
| /* eslint-disable no-console */ | ||||
| 
 | ||||
| import browser from '@playwright/experimental-tools/browser'; | ||||
| import dotenv from 'dotenv'; | ||||
| import OpenAI from 'openai'; | ||||
| import playwright from 'playwright'; | ||||
| 
 | ||||
| import type { ChatCompletionMessageParam, ChatCompletionTool } from 'openai/resources'; | ||||
| 
 | ||||
| dotenv.config(); | ||||
| 
 | ||||
| const openai = new OpenAI(); | ||||
| 
 | ||||
| export const system = ` | ||||
| You are a web tester. | ||||
| 
 | ||||
| <Instructions>to | ||||
| - Perform test according to the provided checklist | ||||
| - Use browser tools to perform actions on web page | ||||
| - Never ask questions, always perform a best guess action | ||||
| - When ready use "reportResult" tool to report result | ||||
| - You can only make one tool call at a time. | ||||
| </Instructions>`;
 | ||||
| 
 | ||||
| type Message = ChatCompletionMessageParam & { | ||||
|   history: any | ||||
| }; | ||||
| 
 | ||||
| const reportTool: ChatCompletionTool = { | ||||
|   type: 'function', | ||||
|   function: { | ||||
|     name: 'reportResult', | ||||
|     description: 'Submit test result', | ||||
|     parameters: { | ||||
|       type: 'object', | ||||
|       properties: { | ||||
|         success: { type: 'boolean', description: 'Whether test passed' }, | ||||
|         result: { type: 'string', description: 'Result of the test if requested' }, | ||||
|         error: { type: 'string', description: 'Error if test failed' }, | ||||
|       }, | ||||
|       required: ['success'], | ||||
|       additionalProperties: false, | ||||
|     }, | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| async function openAIAgentLoop(page: playwright.Page, task: string) { | ||||
|   const pageTools: ChatCompletionTool[] = browser.schema.map(tool => ({ | ||||
|     type: 'function', | ||||
|     function: { | ||||
|       name: tool.name, | ||||
|       description: tool.description, | ||||
|       parameters: { | ||||
|         ...tool.parameters, | ||||
|         additionalProperties: false, | ||||
|       }, | ||||
|     } | ||||
|   })); | ||||
| 
 | ||||
|   const tools = [reportTool, ...pageTools]; | ||||
| 
 | ||||
|   const history: Message[] = [ | ||||
|     { | ||||
|       role: 'system', content: system, history: system | ||||
|     }, | ||||
|     { | ||||
|       role: 'user', | ||||
|       history: `Task: ${task}`, | ||||
|       content: `Task: ${task}\n\n${await browser.snapshot(page)}`, | ||||
|     } | ||||
|   ]; | ||||
| 
 | ||||
|   // Run agentic loop, cap steps.
 | ||||
|   for (let i = 0; i < 50; i++) { | ||||
|     const completion = await openai.chat.completions.create({ | ||||
|       model: 'gpt-4o', | ||||
|       messages: toOpenAIMessages(history), | ||||
|       tools, | ||||
|       tool_choice: 'required', | ||||
|       store: true, | ||||
|     }); | ||||
| 
 | ||||
|     const toolCalls = completion.choices[0]?.message?.tool_calls; | ||||
|     if (!toolCalls || toolCalls.length !== 1 || toolCalls[0].type !== 'function') { | ||||
|       history.push({ role: 'user', content: 'expected exactly one tool call', history: 'expected exactly one tool call' }); | ||||
|       continue; | ||||
|     } | ||||
| 
 | ||||
|     const toolCall = toolCalls[0]; | ||||
|     if (toolCall.function.name === 'reportResult') { | ||||
|       console.log(JSON.parse(toolCall.function.arguments)); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     history.push({ ...completion.choices[0].message, history: null }); | ||||
| 
 | ||||
|     // Run the Playwright tool.
 | ||||
|     const params = JSON.parse(toolCall.function.arguments); | ||||
|     const { error, snapshot, code } = await browser.call(page, toolCall.function.name, params); | ||||
|     if (code.length) | ||||
|       console.log(code.join('\n')); | ||||
| 
 | ||||
|     if (toolCall.function.name === 'log') | ||||
|       return; | ||||
| 
 | ||||
|     // Report the result.
 | ||||
|     const resultText = error ? `Error: ${error}\n` : 'Done\n'; | ||||
|     history.push({ | ||||
|       role: 'tool', | ||||
|       tool_call_id: toolCall.id, | ||||
|       content: resultText + snapshot, | ||||
|       history: resultText, | ||||
|     }); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function toOpenAIMessages(messages: Message[]): ChatCompletionMessageParam[] { | ||||
|   return messages.map((message, i) => { | ||||
|     const copy: Message = { ...message }; | ||||
|     delete copy.history; | ||||
|     if (i === messages.length - 1) | ||||
|       return copy; | ||||
|     copy.content = message.history; | ||||
|     return copy; | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| async function main() { | ||||
|   const browser = await playwright.chromium.launch({ headless: false }); | ||||
|   const page = await browser.newPage(); | ||||
|   await openAIAgentLoop(page, ` | ||||
|     - Go to http://github.com/microsoft
 | ||||
|     - Search for "playwright" repository | ||||
|     - Navigate to it | ||||
|     - Switch into the Issues tab | ||||
|     - Report 3 first issues | ||||
|   `);
 | ||||
|   await browser.close(); | ||||
| } | ||||
| 
 | ||||
| void main(); | ||||
|  | @ -1,150 +0,0 @@ | |||
| /** | ||||
|  * 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. | ||||
|  */ | ||||
| 
 | ||||
| /* eslint-disable no-console */ | ||||
| 
 | ||||
| import Anthropic from '@anthropic-ai/sdk'; | ||||
| import computer from '@playwright/experimental-tools/computer-20241022'; | ||||
| import dotenv from 'dotenv'; | ||||
| import playwright from 'playwright'; | ||||
| 
 | ||||
| import type { BetaImageBlockParam, BetaTextBlockParam } from '@anthropic-ai/sdk/resources/beta/messages/messages'; | ||||
| import type { ToolResult } from '@playwright/experimental-tools/computer-20241022'; | ||||
| 
 | ||||
| dotenv.config(); | ||||
| 
 | ||||
| const anthropic = new Anthropic(); | ||||
| 
 | ||||
| export const system = ` | ||||
| You are a web tester. | ||||
| 
 | ||||
| <Instructions> | ||||
| - Perform test according to the provided checklist | ||||
| - Use browser tools to perform actions on web page | ||||
| - Never ask questions, always perform a best guess action | ||||
| - Use one tool at a time, wait for its result before proceeding. | ||||
| - When ready use "reportResult" tool to report result | ||||
| </Instructions>`;
 | ||||
| 
 | ||||
| const computerTool: Anthropic.Beta.BetaToolUnion = { | ||||
|   type: 'computer_20241022', | ||||
|   name: 'computer', | ||||
|   display_width_px: 1920, | ||||
|   display_height_px: 1080, | ||||
|   display_number: 1, | ||||
| }; | ||||
| 
 | ||||
| const reportTool: Anthropic.Tool = { | ||||
|   name: 'reportResult', | ||||
|   description: 'Submit test result', | ||||
|   input_schema: { | ||||
|     type: 'object', | ||||
|     properties: { | ||||
|       'success': { type: 'boolean', description: 'Whether test passed' }, | ||||
|       'result': { type: 'string', description: 'Result of the test if some information has been requested' }, | ||||
|       'error': { type: 'string', description: 'Error message if test failed' } | ||||
|     }, | ||||
|     required: ['success'] | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| type Message = Anthropic.Beta.Messages.BetaMessageParam & { | ||||
|   history: Anthropic.Beta.Messages.BetaMessageParam['content'] | ||||
| }; | ||||
| 
 | ||||
| async function anthropicAgentLoop(page: playwright.Page, task: string) { | ||||
|   // Add report tool.
 | ||||
|   const tools = [reportTool, computerTool]; | ||||
| 
 | ||||
|   const history: Message[] = [{ | ||||
|     role: 'user', | ||||
|     history: `Task: ${task}`, | ||||
|     content: `Task: ${task}`, | ||||
|   }]; | ||||
| 
 | ||||
|   // Run agentic loop, cap steps.
 | ||||
|   for (let i = 0; i < 50; i++) { | ||||
|     const response = await anthropic.beta.messages.create({ | ||||
|       model: 'claude-3-5-sonnet-20241022', | ||||
|       max_tokens: 1024, | ||||
|       temperature: 0, | ||||
|       tools, | ||||
|       system, | ||||
|       messages: toAnthropicMessages(history), | ||||
|       betas: ['computer-use-2024-10-22'], | ||||
|     }); | ||||
| 
 | ||||
|     history.push({ role: 'assistant', content: response.content, history: response.content }); | ||||
| 
 | ||||
|     const toolUse = response.content.find(block => block.type === 'tool_use'); | ||||
|     if (!toolUse) { | ||||
|       history.push({ role: 'user', content: 'expected exactly one tool call', history: 'expected exactly one tool call' }); | ||||
|       continue; | ||||
|     } | ||||
| 
 | ||||
|     if (toolUse.name === 'reportResult') { | ||||
|       console.log(toolUse.input); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     const result: ToolResult = await computer.call(page, toolUse.name, toolUse.input as any); | ||||
|     const contentEntry: BetaTextBlockParam | BetaImageBlockParam = result.base64_image ? { | ||||
|       type: 'image', | ||||
|       source: { type: 'base64', media_type: 'image/jpeg', data: result.base64_image } | ||||
|     } : { | ||||
|       type: 'text', | ||||
|       text: result.output || '', | ||||
|     }; | ||||
|     history.push({ | ||||
|       role: 'user', | ||||
|       content: [{ | ||||
|         type: 'tool_result', | ||||
|         tool_use_id: toolUse.id, | ||||
|         content: [contentEntry], | ||||
|       }], | ||||
|       history: [{ | ||||
|         type: 'tool_result', | ||||
|         tool_use_id: toolUse.id, | ||||
|         content: [{ type: 'text', text: '<redacted>' }], | ||||
|       }], | ||||
|     }); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function toAnthropicMessages(messages: Message[]): Anthropic.Beta.Messages.BetaMessageParam[] { | ||||
|   return messages.map((message, i) => { | ||||
|     if (i === messages.length - 1) | ||||
|       return { ...message, history: undefined }; | ||||
|     return { ...message, content: message.history, history: undefined }; | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| const githubTask = ` | ||||
|   - Search for "playwright" repository | ||||
|   - Navigate to it | ||||
|   - Switch into the Issues tab | ||||
|   - Report 3 first issues | ||||
| `;
 | ||||
| 
 | ||||
| async function main() { | ||||
|   const browser = await playwright.chromium.launch({ headless: false }); | ||||
|   const page = await browser.newPage(); | ||||
|   await page.goto('http://github.com/microsoft'); | ||||
|   await anthropicAgentLoop(page, githubTask); | ||||
|   await browser.close(); | ||||
| } | ||||
| 
 | ||||
| void main(); | ||||
|  | @ -1,99 +0,0 @@ | |||
| /** | ||||
|  * 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 { Server } from '@modelcontextprotocol/sdk/server/index.js'; | ||||
| import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; | ||||
| import { | ||||
|   CallToolRequestSchema, | ||||
|   ListToolsRequestSchema, | ||||
| } from '@modelcontextprotocol/sdk/types.js'; | ||||
| import * as playwright from 'playwright'; | ||||
| import browser from '@playwright/experimental-tools/browser'; | ||||
| 
 | ||||
| const server = new Server( | ||||
|     { | ||||
|       name: 'MCP Server for Playwright', | ||||
|       version: '0.0.1', | ||||
|     }, | ||||
|     { | ||||
|       capabilities: { | ||||
|         tools: {}, | ||||
|       }, | ||||
|     } | ||||
| ); | ||||
| 
 | ||||
| server.setRequestHandler(ListToolsRequestSchema, async () => { | ||||
|   return { | ||||
|     tools: browser.schema.map(tool => ({ | ||||
|       name: tool.name, | ||||
|       description: tool.description, | ||||
|       inputSchema: tool.parameters, | ||||
|     })), | ||||
|   }; | ||||
| }); | ||||
| 
 | ||||
| async function createBrowser(): Promise<playwright.Browser> { | ||||
|   if (process.env.PLAYWRIGHT_WS_ENDPOINT) { | ||||
|     return await playwright.chromium.connect( | ||||
|         process.env.PLAYWRIGHT_WS_ENDPOINT | ||||
|     ); | ||||
|   } | ||||
|   return await playwright.chromium.launch({ headless: false }); | ||||
| } | ||||
| 
 | ||||
| async function getPage(): Promise<playwright.Page> { | ||||
|   if (!page) { | ||||
|     const browser = await createBrowser(); | ||||
|     const context = await browser.newContext(); | ||||
|     page = await context.newPage(); | ||||
|   } | ||||
|   return page; | ||||
| } | ||||
| 
 | ||||
| let page: playwright.Page | undefined; | ||||
| 
 | ||||
| async function main() { | ||||
|   server.setRequestHandler(CallToolRequestSchema, async request => { | ||||
|     const page = await getPage(); | ||||
|     const response = await browser.call( | ||||
|         page, | ||||
|         request.params.name, | ||||
|       request.params.arguments as any | ||||
|     ); | ||||
|     const content: { type: string; text: string }[] = []; | ||||
|     if (response.error) | ||||
|       content.push({ type: 'text', text: response.error }); | ||||
|     if (response.snapshot) | ||||
|       content.push({ type: 'text', text: response.snapshot }); | ||||
|     return { | ||||
|       content, | ||||
|       isError: response.error ? true : false, | ||||
|     }; | ||||
|   }); | ||||
| 
 | ||||
|   process.stdin.on('close', async () => { | ||||
|     server.close(); | ||||
|     // eslint-disable-next-line no-restricted-properties
 | ||||
|     setTimeout(() => process.exit(0), 15000); | ||||
|     await page?.context()?.browser()?.close(); | ||||
|     // eslint-disable-next-line no-restricted-properties
 | ||||
|     process.exit(0); | ||||
|   }); | ||||
| 
 | ||||
|   await server.connect(new StdioServerTransport()); | ||||
| } | ||||
| 
 | ||||
| void main(); | ||||
|  | @ -1,150 +0,0 @@ | |||
| /** | ||||
|  * Copyright 2017 Google Inc. All rights reserved. | ||||
|  * Modifications 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 { waitForNetwork } from './utils'; | ||||
| 
 | ||||
| import type { ToolResult } from '../../browser'; | ||||
| import type { JSONSchemaType, ToolDeclaration } from '../../types'; | ||||
| import type playwright from 'playwright'; | ||||
| 
 | ||||
| 
 | ||||
| type LocatorEx = playwright.Locator & { | ||||
|   _generateLocatorString: () => Promise<string>; | ||||
| }; | ||||
| 
 | ||||
| const intentProperty = { | ||||
|   intent: { | ||||
|     type: 'string', | ||||
|     description: 'Intent behind this particular action. Used as a comment.', | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| const elementIdProperty = { | ||||
|   elementId: { | ||||
|     type: 'number', | ||||
|     description: 'Target element', | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| export const schema: ToolDeclaration[] = [ | ||||
|   { | ||||
|     name: 'navigate', | ||||
|     description: 'Navigate to a URL', | ||||
|     parameters: { | ||||
|       type: 'object', | ||||
|       properties: { | ||||
|         ...intentProperty, | ||||
|         url: { | ||||
|           type: 'string', | ||||
|           description: 'URL to navigate to', | ||||
|         }, | ||||
|       }, | ||||
|       required: ['intent', 'elementId'], | ||||
|     } | ||||
|   }, | ||||
|   { | ||||
|     name: 'click', | ||||
|     description: 'Perform click on a web page', | ||||
|     parameters: { | ||||
|       type: 'object', | ||||
|       properties: { | ||||
|         ...intentProperty, | ||||
|         ...elementIdProperty, | ||||
|       }, | ||||
|       required: ['intent', 'elementId'], | ||||
|     } | ||||
|   }, | ||||
|   { | ||||
|     name: 'enterText', | ||||
|     description: 'Enter text into editable element', | ||||
|     parameters: { | ||||
|       type: 'object', | ||||
|       properties: { | ||||
|         ...intentProperty, | ||||
|         ...elementIdProperty, | ||||
|         text: { | ||||
|           type: 'string', | ||||
|           description: 'Text to enter', | ||||
|         }, | ||||
|         submit: { | ||||
|           type: 'boolean', | ||||
|           description: 'Whether to submit entered text (press Enter after)' | ||||
|         } | ||||
|       }, | ||||
|       required: ['intent', 'elementId', 'text'], | ||||
|     } | ||||
|   }, | ||||
|   { | ||||
|     name: 'wait', | ||||
|     description: `Wait for given amount of time to see if the page updates. Use it after action if you think page is not ready yet`, | ||||
|     parameters: { | ||||
|       type: 'object', | ||||
|       properties: { | ||||
|         ...intentProperty, | ||||
|         time: { | ||||
|           type: 'integer', | ||||
|           description: 'Time to wait in seconds', | ||||
|         }, | ||||
|       }, | ||||
|       required: ['intent', 'time'], | ||||
|     } | ||||
|   }, | ||||
| ]; | ||||
| 
 | ||||
| export async function call(page: playwright.Page, toolName: string, params: Record<string, JSONSchemaType>): Promise<ToolResult> { | ||||
|   const code: string[] = []; | ||||
|   try { | ||||
|     await waitForNetwork(page, async () => { | ||||
|       await performAction(page, toolName, params, code); | ||||
|     }); | ||||
|   } catch (e) { | ||||
|     return { error: e.message, snapshot: await snapshot(page), code }; | ||||
|   } | ||||
|   return { snapshot: await snapshot(page), code }; | ||||
| } | ||||
| 
 | ||||
| export async function snapshot(page: playwright.Page) { | ||||
|   const params = { _id: true } as any; | ||||
|   return `<Page snapshot>\n${await page.locator('body').ariaSnapshot(params)}\n</Page snapshot>`; | ||||
| } | ||||
| 
 | ||||
| async function performAction(page: playwright.Page, toolName: string, params: Record<string, JSONSchemaType>, code: string[]) { | ||||
|   const locator = elementLocator(page, params); | ||||
|   code.push((params.intent as string).split('\n').map(line => `// ${line}`).join('\n')); | ||||
|   if (toolName === 'navigate') { | ||||
|     code.push(`await page.goto(${JSON.stringify(params.url)})`); | ||||
|     await page.goto(params.url as string); | ||||
|   } else if (toolName === 'wait') { | ||||
|     await page.waitForTimeout(Math.min(10000, params.time as number * 1000)); | ||||
|   } else if (toolName === 'click') { | ||||
|     code.push(`await page.${await locator._generateLocatorString()}.click()`); | ||||
|     await locator.click(); | ||||
|   } else if (toolName === 'enterText') { | ||||
|     code.push(`await page.${await locator._generateLocatorString()}.click()`); | ||||
|     await locator.click(); | ||||
|     code.push(`await page.${await locator._generateLocatorString()}.fill(${JSON.stringify(params.text)})`); | ||||
|     await locator.fill(params.text as string); | ||||
|     if (params.submit) { | ||||
|       code.push(`await page.${await locator._generateLocatorString()}.press("Enter")`); | ||||
|       await locator.press('Enter'); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function elementLocator(page: playwright.Page, params: any): LocatorEx { | ||||
|   return page.locator(`internal:aria-id=${params.elementId}`) as LocatorEx; | ||||
| } | ||||
|  | @ -1,160 +0,0 @@ | |||
| /** | ||||
|  * Copyright 2017 Google Inc. All rights reserved. | ||||
|  * Modifications 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 { waitForNetwork } from './utils'; | ||||
| 
 | ||||
| import type { ToolResult } from '../../computer-20241022'; | ||||
| import type { JSONSchemaType } from '../../types'; | ||||
| import type playwright from 'playwright'; | ||||
| 
 | ||||
| 
 | ||||
| export async function call(page: playwright.Page, toolName: string, input: Record<string, JSONSchemaType>): Promise<ToolResult> { | ||||
|   if (toolName !== 'computer') | ||||
|     throw new Error('Unsupported tool'); | ||||
|   return await waitForNetwork(page, async () => { | ||||
|     return await performAction(page, toolName, input); | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| type PageState = { | ||||
|   x: number; | ||||
|   y: number; | ||||
| }; | ||||
| 
 | ||||
| const pageStateSymbol = Symbol('pageState'); | ||||
| 
 | ||||
| function pageState(page: playwright.Page): PageState { | ||||
|   if (!(page as any)[pageStateSymbol]) | ||||
|     (page as any)[pageStateSymbol] = { x: 0, y: 0 }; | ||||
|   return (page as any)[pageStateSymbol]; | ||||
| } | ||||
| 
 | ||||
| async function performAction(page: playwright.Page, toolName: string, input: Record<string, JSONSchemaType>): Promise<ToolResult> { | ||||
|   const state = pageState(page); | ||||
|   const { action } = input as { action: string }; | ||||
|   if (action === 'screenshot') { | ||||
|     const screenshot = await page.screenshot({ type: 'jpeg', quality: 50, scale: 'css' }); | ||||
|     return { | ||||
|       output: 'Screenshot', | ||||
|       base64_image: screenshot.toString('base64'), | ||||
|     }; | ||||
|   } | ||||
|   if (action === 'mouse_move') { | ||||
|     const { coordinate } = input as { coordinate: [number, number] }; | ||||
|     state.x = coordinate[0]; | ||||
|     state.y = coordinate[1]; | ||||
|     await page.mouse.move(state.x, state.y); | ||||
|     return { output: 'Mouse moved' }; | ||||
|   } | ||||
|   if (action === 'left_click') { | ||||
|     await page.mouse.down(); | ||||
|     await page.mouse.up(); | ||||
|     return { output: 'Left clicked' }; | ||||
|   } | ||||
|   if (action === 'left_click_drag') { | ||||
|     await page.mouse.down(); | ||||
|     const { coordinate } = input as { coordinate: [number, number] }; | ||||
|     state.x = coordinate[0]; | ||||
|     state.y = coordinate[1]; | ||||
|     await page.mouse.move(state.x, state.y); | ||||
|     await page.mouse.up(); | ||||
|     return { output: 'Left dragged' }; | ||||
|   } | ||||
|   if (action === 'right_click') { | ||||
|     await page.mouse.down({ button: 'right' }); | ||||
|     await page.mouse.up({ button: 'right' }); | ||||
|     return { output: 'Right clicked' }; | ||||
|   } | ||||
|   if (action === 'double_click') { | ||||
|     await page.mouse.down(); | ||||
|     await page.mouse.up(); | ||||
|     await page.mouse.down(); | ||||
|     await page.mouse.up(); | ||||
|     return { output: 'Double clicked' }; | ||||
|   } | ||||
|   if (action === 'middle_click') { | ||||
|     await page.mouse.down({ button: 'middle' }); | ||||
|     await page.mouse.up({ button: 'middle' }); | ||||
|     return { output: 'Middle clicked' }; | ||||
|   } | ||||
|   if (action === 'key') { | ||||
|     const { text } = input as { text: string }; | ||||
|     await page.keyboard.press(xToPlaywright(text)); | ||||
|     return { output: 'Text typed' }; | ||||
|   } | ||||
|   if (action === 'cursor_position') | ||||
|     return { output: `X=${state.x},Y=${state.y}` }; | ||||
|   throw new Error('Unimplemented tool: ' + toolName); | ||||
| } | ||||
| 
 | ||||
| const xToPlaywrightKeyMap = new Map([ | ||||
|   ['BackSpace', 'Backspace'], | ||||
|   ['Tab', 'Tab'], | ||||
|   ['Return', 'Enter'], | ||||
|   ['Escape', 'Escape'], | ||||
|   ['space', ' '], | ||||
|   ['Delete', 'Delete'], | ||||
|   ['Home', 'Home'], | ||||
|   ['End', 'End'], | ||||
|   ['Left', 'ArrowLeft'], | ||||
|   ['Up', 'ArrowUp'], | ||||
|   ['Right', 'ArrowRight'], | ||||
|   ['Down', 'ArrowDown'], | ||||
|   ['Insert', 'Insert'], | ||||
|   ['Page_Up', 'PageUp'], | ||||
|   ['Page_Down', 'PageDown'], | ||||
|   ['F1', 'F1'], | ||||
|   ['F2', 'F2'], | ||||
|   ['F3', 'F3'], | ||||
|   ['F4', 'F4'], | ||||
|   ['F5', 'F5'], | ||||
|   ['F6', 'F6'], | ||||
|   ['F7', 'F7'], | ||||
|   ['F8', 'F8'], | ||||
|   ['F9', 'F9'], | ||||
|   ['F10', 'F10'], | ||||
|   ['F11', 'F11'], | ||||
|   ['F12', 'F12'], | ||||
|   ['Shift_L', 'Shift'], | ||||
|   ['Shift_R', 'Shift'], | ||||
|   ['Control_L', 'Control'], | ||||
|   ['Control_R', 'Control'], | ||||
|   ['Alt_L', 'Alt'], | ||||
|   ['Alt_R', 'Alt'], | ||||
|   ['Super_L', 'Meta'], | ||||
|   ['Super_R', 'Meta'], | ||||
| ]); | ||||
| 
 | ||||
| const xToPlaywrightModifierMap = new Map([ | ||||
|   ['alt', 'Alt'], | ||||
|   ['control', 'Control'], | ||||
|   ['meta', 'Meta'], | ||||
|   ['shift', 'Shift'], | ||||
| ]); | ||||
| 
 | ||||
| 
 | ||||
| const xToPlaywright = (key: string) => { | ||||
|   const tokens = key.split('+'); | ||||
|   if (tokens.length === 1) | ||||
|     return xToPlaywrightKeyMap.get(key) || key; | ||||
|   if (tokens.length === 2) { | ||||
|     const modifier = xToPlaywrightModifierMap.get(tokens[0]); | ||||
|     const key = xToPlaywrightKeyMap.get(tokens[1]) || tokens[1]; | ||||
|     return modifier + '+' + key; | ||||
|   } | ||||
|   throw new Error('Invalid key: ' + key); | ||||
| }; | ||||
|  | @ -174,7 +174,7 @@ const workspace = new Workspace(ROOT_PATH, [ | |||
|   }), | ||||
|   new PWPackage({ | ||||
|     name: '@playwright/experimental-tools', | ||||
|     path: path.join(ROOT_PATH, 'packages', 'playwright-tools'), | ||||
|     path: path.join(ROOT_PATH, 'packages', 'playwright-mcp'), | ||||
|     files: LICENCE_FILES, | ||||
|   }), | ||||
|   new PWPackage({ | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue