Pavel Feldman commited on
Commit
852709c
·
1 Parent(s): b1d5410

chore: initial code commit

Browse files
.gitignore ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ lib/
2
+ node_modules/
.npmignore ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ **/*
2
+ README.md
3
+ LICENSE
4
+ !lib/**/*.js
5
+ !cli.js
LICENSE CHANGED
@@ -1,201 +1,202 @@
1
- Apache License
2
- Version 2.0, January 2004
3
- http://www.apache.org/licenses/
4
-
5
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
-
7
- 1. Definitions.
8
-
9
- "License" shall mean the terms and conditions for use, reproduction,
10
- and distribution as defined by Sections 1 through 9 of this document.
11
-
12
- "Licensor" shall mean the copyright owner or entity authorized by
13
- the copyright owner that is granting the License.
14
-
15
- "Legal Entity" shall mean the union of the acting entity and all
16
- other entities that control, are controlled by, or are under common
17
- control with that entity. For the purposes of this definition,
18
- "control" means (i) the power, direct or indirect, to cause the
19
- direction or management of such entity, whether by contract or
20
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
- outstanding shares, or (iii) beneficial ownership of such entity.
22
-
23
- "You" (or "Your") shall mean an individual or Legal Entity
24
- exercising permissions granted by this License.
25
-
26
- "Source" form shall mean the preferred form for making modifications,
27
- including but not limited to software source code, documentation
28
- source, and configuration files.
29
-
30
- "Object" form shall mean any form resulting from mechanical
31
- transformation or translation of a Source form, including but
32
- not limited to compiled object code, generated documentation,
33
- and conversions to other media types.
34
-
35
- "Work" shall mean the work of authorship, whether in Source or
36
- Object form, made available under the License, as indicated by a
37
- copyright notice that is included in or attached to the work
38
- (an example is provided in the Appendix below).
39
-
40
- "Derivative Works" shall mean any work, whether in Source or Object
41
- form, that is based on (or derived from) the Work and for which the
42
- editorial revisions, annotations, elaborations, or other modifications
43
- represent, as a whole, an original work of authorship. For the purposes
44
- of this License, Derivative Works shall not include works that remain
45
- separable from, or merely link (or bind by name) to the interfaces of,
46
- the Work and Derivative Works thereof.
47
-
48
- "Contribution" shall mean any work of authorship, including
49
- the original version of the Work and any modifications or additions
50
- to that Work or Derivative Works thereof, that is intentionally
51
- submitted to Licensor for inclusion in the Work by the copyright owner
52
- or by an individual or Legal Entity authorized to submit on behalf of
53
- the copyright owner. For the purposes of this definition, "submitted"
54
- means any form of electronic, verbal, or written communication sent
55
- to the Licensor or its representatives, including but not limited to
56
- communication on electronic mailing lists, source code control systems,
57
- and issue tracking systems that are managed by, or on behalf of, the
58
- Licensor for the purpose of discussing and improving the Work, but
59
- excluding communication that is conspicuously marked or otherwise
60
- designated in writing by the copyright owner as "Not a Contribution."
61
-
62
- "Contributor" shall mean Licensor and any individual or Legal Entity
63
- on behalf of whom a Contribution has been received by Licensor and
64
- subsequently incorporated within the Work.
65
-
66
- 2. Grant of Copyright License. Subject to the terms and conditions of
67
- this License, each Contributor hereby grants to You a perpetual,
68
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
- copyright license to reproduce, prepare Derivative Works of,
70
- publicly display, publicly perform, sublicense, and distribute the
71
- Work and such Derivative Works in Source or Object form.
72
-
73
- 3. Grant of Patent License. Subject to the terms and conditions of
74
- this License, each Contributor hereby grants to You a perpetual,
75
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
- (except as stated in this section) patent license to make, have made,
77
- use, offer to sell, sell, import, and otherwise transfer the Work,
78
- where such license applies only to those patent claims licensable
79
- by such Contributor that are necessarily infringed by their
80
- Contribution(s) alone or by combination of their Contribution(s)
81
- with the Work to which such Contribution(s) was submitted. If You
82
- institute patent litigation against any entity (including a
83
- cross-claim or counterclaim in a lawsuit) alleging that the Work
84
- or a Contribution incorporated within the Work constitutes direct
85
- or contributory patent infringement, then any patent licenses
86
- granted to You under this License for that Work shall terminate
87
- as of the date such litigation is filed.
88
-
89
- 4. Redistribution. You may reproduce and distribute copies of the
90
- Work or Derivative Works thereof in any medium, with or without
91
- modifications, and in Source or Object form, provided that You
92
- meet the following conditions:
93
-
94
- (a) You must give any other recipients of the Work or
95
- Derivative Works a copy of this License; and
96
-
97
- (b) You must cause any modified files to carry prominent notices
98
- stating that You changed the files; and
99
-
100
- (c) You must retain, in the Source form of any Derivative Works
101
- that You distribute, all copyright, patent, trademark, and
102
- attribution notices from the Source form of the Work,
103
- excluding those notices that do not pertain to any part of
104
- the Derivative Works; and
105
-
106
- (d) If the Work includes a "NOTICE" text file as part of its
107
- distribution, then any Derivative Works that You distribute must
108
- include a readable copy of the attribution notices contained
109
- within such NOTICE file, excluding those notices that do not
110
- pertain to any part of the Derivative Works, in at least one
111
- of the following places: within a NOTICE text file distributed
112
- as part of the Derivative Works; within the Source form or
113
- documentation, if provided along with the Derivative Works; or,
114
- within a display generated by the Derivative Works, if and
115
- wherever such third-party notices normally appear. The contents
116
- of the NOTICE file are for informational purposes only and
117
- do not modify the License. You may add Your own attribution
118
- notices within Derivative Works that You distribute, alongside
119
- or as an addendum to the NOTICE text from the Work, provided
120
- that such additional attribution notices cannot be construed
121
- as modifying the License.
122
-
123
- You may add Your own copyright statement to Your modifications and
124
- may provide additional or different license terms and conditions
125
- for use, reproduction, or distribution of Your modifications, or
126
- for any such Derivative Works as a whole, provided Your use,
127
- reproduction, and distribution of the Work otherwise complies with
128
- the conditions stated in this License.
129
-
130
- 5. Submission of Contributions. Unless You explicitly state otherwise,
131
- any Contribution intentionally submitted for inclusion in the Work
132
- by You to the Licensor shall be under the terms and conditions of
133
- this License, without any additional terms or conditions.
134
- Notwithstanding the above, nothing herein shall supersede or modify
135
- the terms of any separate license agreement you may have executed
136
- with Licensor regarding such Contributions.
137
-
138
- 6. Trademarks. This License does not grant permission to use the trade
139
- names, trademarks, service marks, or product names of the Licensor,
140
- except as required for reasonable and customary use in describing the
141
- origin of the Work and reproducing the content of the NOTICE file.
142
-
143
- 7. Disclaimer of Warranty. Unless required by applicable law or
144
- agreed to in writing, Licensor provides the Work (and each
145
- Contributor provides its Contributions) on an "AS IS" BASIS,
146
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
- implied, including, without limitation, any warranties or conditions
148
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
- PARTICULAR PURPOSE. You are solely responsible for determining the
150
- appropriateness of using or redistributing the Work and assume any
151
- risks associated with Your exercise of permissions under this License.
152
-
153
- 8. Limitation of Liability. In no event and under no legal theory,
154
- whether in tort (including negligence), contract, or otherwise,
155
- unless required by applicable law (such as deliberate and grossly
156
- negligent acts) or agreed to in writing, shall any Contributor be
157
- liable to You for damages, including any direct, indirect, special,
158
- incidental, or consequential damages of any character arising as a
159
- result of this License or out of the use or inability to use the
160
- Work (including but not limited to damages for loss of goodwill,
161
- work stoppage, computer failure or malfunction, or any and all
162
- other commercial damages or losses), even if such Contributor
163
- has been advised of the possibility of such damages.
164
-
165
- 9. Accepting Warranty or Additional Liability. While redistributing
166
- the Work or Derivative Works thereof, You may choose to offer,
167
- and charge a fee for, acceptance of support, warranty, indemnity,
168
- or other liability obligations and/or rights consistent with this
169
- License. However, in accepting such obligations, You may act only
170
- on Your own behalf and on Your sole responsibility, not on behalf
171
- of any other Contributor, and only if You agree to indemnify,
172
- defend, and hold each Contributor harmless for any liability
173
- incurred by, or claims asserted against, such Contributor by reason
174
- of your accepting any such warranty or additional liability.
175
-
176
- END OF TERMS AND CONDITIONS
177
-
178
- APPENDIX: How to apply the Apache License to your work.
179
-
180
- To apply the Apache License to your work, attach the following
181
- boilerplate notice, with the fields enclosed by brackets "[]"
182
- replaced with your own identifying information. (Don't include
183
- the brackets!) The text should be enclosed in the appropriate
184
- comment syntax for the file format. We also recommend that a
185
- file or class name and description of purpose be included on the
186
- same "printed page" as the copyright notice for easier
187
- identification within third-party archives.
188
-
189
- Copyright [yyyy] [name of copyright owner]
190
-
191
- Licensed under the Apache License, Version 2.0 (the "License");
192
- you may not use this file except in compliance with the License.
193
- You may obtain a copy of the License at
194
-
195
- http://www.apache.org/licenses/LICENSE-2.0
196
-
197
- Unless required by applicable law or agreed to in writing, software
198
- distributed under the License is distributed on an "AS IS" BASIS,
199
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
- See the License for the specific language governing permissions and
201
- limitations under the License.
 
 
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Portions Copyright (c) Microsoft Corporation.
190
+ Portions Copyright 2017 Google Inc.
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
README.md CHANGED
@@ -1,3 +1,223 @@
1
- # Repository setup required :wave:
2
-
3
- Please visit the website URL :point_right: for this repository to complete the setup of this repository and configure access controls.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ## Playwright MCP
2
+
3
+ This package is experimental and not yet ready for production use.
4
+ It is a subject to change and will not respect semver versioning.
5
+
6
+ ### Example config
7
+
8
+ ```js
9
+ {
10
+ "mcpServers": {
11
+ "playwright": {
12
+ "command": "npx",
13
+ "args": [
14
+ "@playwright/mcp",
15
+ "--headless"
16
+ ]
17
+ }
18
+ }
19
+ }
20
+ ```
21
+
22
+ ### Running headed browser (Browser with GUI).
23
+
24
+ ```js
25
+ {
26
+ "mcpServers": {
27
+ "playwright": {
28
+ "command": "npx",
29
+ "args": [
30
+ "@playwright/mcp"
31
+ ]
32
+ }
33
+ }
34
+ }
35
+ ```
36
+
37
+ ### Running headed browser on Linux
38
+
39
+ When running headed browser on system w/o display or from worker processes of the IDEs,
40
+ you can run Playwright in a client-server manner. You'll run the Playwright server
41
+ from environment with the DISPLAY
42
+
43
+ ```sh
44
+ npx playwright run-server
45
+ ```
46
+
47
+ And then in MCP config, add following to the `env`:
48
+
49
+ ```js
50
+ {
51
+ "mcpServers": {
52
+ "playwright": {
53
+ "command": "npx",
54
+ "args": [
55
+ "@playwright/mcp"
56
+ ],
57
+ "env": {
58
+ // Use the endpoint from the output of the server above.
59
+ "PLAYWRIGHT_WS_ENDPOINT": "ws://localhost:<port>/"
60
+ }
61
+ }
62
+ }
63
+ }
64
+ ```
65
+
66
+ ### Tool Modes
67
+
68
+ The tools are available in two modes:
69
+
70
+ 1. **Snapshot Mode** (default): Uses accessibility snapshots for better performance and reliability
71
+ 2. **Vision Mode**: Uses screenshots for visual-based interactions
72
+
73
+ To use Vision Mode, add the `--vision` flag when starting the server:
74
+
75
+ ```js
76
+ {
77
+ "mcpServers": {
78
+ "playwright": {
79
+ "command": "npx",
80
+ "args": [
81
+ "@playwright/mcp",
82
+ "--vision"
83
+ ]
84
+ }
85
+ }
86
+ }
87
+ ```
88
+
89
+ Vision Mode works best with the computer use models that are able to interact with elements using
90
+ X Y coordinate space, based on the provided screenshot.
91
+
92
+ ### Snapshot Mode
93
+
94
+ The Playwright MCP provides a set of tools for browser automation. Here are all available tools:
95
+
96
+ - **browser_navigate**
97
+ - Description: Navigate to a URL
98
+ - Parameters:
99
+ - `url` (string): The URL to navigate to
100
+
101
+ - **browser_go_back**
102
+ - Description: Go back to the previous page
103
+ - Parameters: None
104
+
105
+ - **browser_go_forward**
106
+ - Description: Go forward to the next page
107
+ - Parameters: None
108
+
109
+ - **browser_click**
110
+ - Description: Perform click on a web page
111
+ - Parameters:
112
+ - `element` (string): Human-readable element description used to obtain permission to interact with the element
113
+ - `ref` (string): Exact target element reference from the page snapshot
114
+
115
+ - **browser_hover**
116
+ - Description: Hover over element on page
117
+ - Parameters:
118
+ - `element` (string): Human-readable element description used to obtain permission to interact with the element
119
+ - `ref` (string): Exact target element reference from the page snapshot
120
+
121
+ - **browser_drag**
122
+ - Description: Perform drag and drop between two elements
123
+ - Parameters:
124
+ - `startElement` (string): Human-readable source element description used to obtain permission to interact with the element
125
+ - `startRef` (string): Exact source element reference from the page snapshot
126
+ - `endElement` (string): Human-readable target element description used to obtain permission to interact with the element
127
+ - `endRef` (string): Exact target element reference from the page snapshot
128
+
129
+ - **browser_type**
130
+ - Description: Type text into editable element
131
+ - Parameters:
132
+ - `element` (string): Human-readable element description used to obtain permission to interact with the element
133
+ - `ref` (string): Exact target element reference from the page snapshot
134
+ - `text` (string): Text to type into the element
135
+ - `submit` (boolean): Whether to submit entered text (press Enter after)
136
+
137
+ - **browser_press_key**
138
+ - Description: Press a key on the keyboard
139
+ - Parameters:
140
+ - `key` (string): Name of the key to press or a character to generate, such as `ArrowLeft` or `a`
141
+
142
+ - **browser_snapshot**
143
+ - Description: Capture accessibility snapshot of the current page (better than screenshot)
144
+ - Parameters: None
145
+
146
+ - **browser_save_as_pdf**
147
+ - Description: Save page as PDF
148
+ - Parameters: None
149
+
150
+ - **browser_wait**
151
+ - Description: Wait for a specified time in seconds
152
+ - Parameters:
153
+ - `time` (number): The time to wait in seconds (capped at 10 seconds)
154
+
155
+ - **browser_close**
156
+ - Description: Close the page
157
+ - Parameters: None
158
+
159
+
160
+ ### Vision Mode
161
+
162
+ Vision Mode provides tools for visual-based interactions using screenshots. Here are all available tools:
163
+
164
+ - **browser_navigate**
165
+ - Description: Navigate to a URL
166
+ - Parameters:
167
+ - `url` (string): The URL to navigate to
168
+
169
+ - **browser_go_back**
170
+ - Description: Go back to the previous page
171
+ - Parameters: None
172
+
173
+ - **browser_go_forward**
174
+ - Description: Go forward to the next page
175
+ - Parameters: None
176
+
177
+ - **browser_screenshot**
178
+ - Description: Capture screenshot of the current page
179
+ - Parameters: None
180
+
181
+ - **browser_move_mouse**
182
+ - Description: Move mouse to specified coordinates
183
+ - Parameters:
184
+ - `x` (number): X coordinate
185
+ - `y` (number): Y coordinate
186
+
187
+ - **browser_click**
188
+ - Description: Click at specified coordinates
189
+ - Parameters:
190
+ - `x` (number): X coordinate to click at
191
+ - `y` (number): Y coordinate to click at
192
+
193
+ - **browser_drag**
194
+ - Description: Perform drag and drop operation
195
+ - Parameters:
196
+ - `startX` (number): Start X coordinate
197
+ - `startY` (number): Start Y coordinate
198
+ - `endX` (number): End X coordinate
199
+ - `endY` (number): End Y coordinate
200
+
201
+ - **browser_type**
202
+ - Description: Type text at specified coordinates
203
+ - Parameters:
204
+ - `text` (string): Text to type
205
+ - `submit` (boolean): Whether to submit entered text (press Enter after)
206
+
207
+ - **browser_press_key**
208
+ - Description: Press a key on the keyboard
209
+ - Parameters:
210
+ - `key` (string): Name of the key to press or a character to generate, such as `ArrowLeft` or `a`
211
+
212
+ - **browser_save_as_pdf**
213
+ - Description: Save page as PDF
214
+ - Parameters: None
215
+
216
+ - **browser_wait**
217
+ - Description: Wait for a specified time in seconds
218
+ - Parameters:
219
+ - `time` (number): The time to wait in seconds (capped at 10 seconds)
220
+
221
+ - **browser_close**
222
+ - Description: Close the page
223
+ - Parameters: None
cli.js ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Copyright (c) Microsoft Corporation.
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+
18
+ require('./lib/program');
eslint.config.mjs ADDED
@@ -0,0 +1,199 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Copyright (c) Microsoft Corporation.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import typescriptEslint from "@typescript-eslint/eslint-plugin";
18
+ import tsParser from "@typescript-eslint/parser";
19
+ import notice from "eslint-plugin-notice";
20
+ import path from "path";
21
+ import { fileURLToPath } from "url";
22
+ import stylistic from "@stylistic/eslint-plugin";
23
+ import importRules from "eslint-plugin-import";
24
+
25
+ const __filename = fileURLToPath(import.meta.url);
26
+ const __dirname = path.dirname(__filename);
27
+
28
+ const plugins = {
29
+ "@stylistic": stylistic,
30
+ "@typescript-eslint": typescriptEslint,
31
+ notice,
32
+ import: importRules,
33
+ };
34
+
35
+ export const baseRules = {
36
+ "@typescript-eslint/no-unused-vars": [
37
+ 2,
38
+ { args: "none", caughtErrors: "none" },
39
+ ],
40
+
41
+ /**
42
+ * Enforced rules
43
+ */
44
+ // syntax preferences
45
+ "object-curly-spacing": ["error", "always"],
46
+ quotes: [
47
+ 2,
48
+ "single",
49
+ {
50
+ avoidEscape: true,
51
+ allowTemplateLiterals: true,
52
+ },
53
+ ],
54
+ "jsx-quotes": [2, "prefer-single"],
55
+ "no-extra-semi": 2,
56
+ "@stylistic/semi": [2],
57
+ "comma-style": [2, "last"],
58
+ "wrap-iife": [2, "inside"],
59
+ "spaced-comment": [
60
+ 2,
61
+ "always",
62
+ {
63
+ markers: ["*"],
64
+ },
65
+ ],
66
+ eqeqeq: [2],
67
+ "accessor-pairs": [
68
+ 2,
69
+ {
70
+ getWithoutSet: false,
71
+ setWithoutGet: false,
72
+ },
73
+ ],
74
+ "brace-style": [2, "1tbs", { allowSingleLine: true }],
75
+ curly: [2, "multi-or-nest", "consistent"],
76
+ "new-parens": 2,
77
+ "arrow-parens": [2, "as-needed"],
78
+ "prefer-const": 2,
79
+ "quote-props": [2, "consistent"],
80
+ "nonblock-statement-body-position": [2, "below"],
81
+
82
+ // anti-patterns
83
+ "no-var": 2,
84
+ "no-with": 2,
85
+ "no-multi-str": 2,
86
+ "no-caller": 2,
87
+ "no-implied-eval": 2,
88
+ "no-labels": 2,
89
+ "no-new-object": 2,
90
+ "no-octal-escape": 2,
91
+ "no-self-compare": 2,
92
+ "no-shadow-restricted-names": 2,
93
+ "no-cond-assign": 2,
94
+ "no-debugger": 2,
95
+ "no-dupe-keys": 2,
96
+ "no-duplicate-case": 2,
97
+ "no-empty-character-class": 2,
98
+ "no-unreachable": 2,
99
+ "no-unsafe-negation": 2,
100
+ radix: 2,
101
+ "valid-typeof": 2,
102
+ "no-implicit-globals": [2],
103
+ "no-unused-expressions": [
104
+ 2,
105
+ { allowShortCircuit: true, allowTernary: true, allowTaggedTemplates: true },
106
+ ],
107
+ "no-proto": 2,
108
+
109
+ // es2015 features
110
+ "require-yield": 2,
111
+ "template-curly-spacing": [2, "never"],
112
+
113
+ // spacing details
114
+ "space-infix-ops": 2,
115
+ "space-in-parens": [2, "never"],
116
+ "array-bracket-spacing": [2, "never"],
117
+ "comma-spacing": [2, { before: false, after: true }],
118
+ "keyword-spacing": [2, "always"],
119
+ "space-before-function-paren": [
120
+ 2,
121
+ {
122
+ anonymous: "never",
123
+ named: "never",
124
+ asyncArrow: "always",
125
+ },
126
+ ],
127
+ "no-whitespace-before-property": 2,
128
+ "keyword-spacing": [
129
+ 2,
130
+ {
131
+ overrides: {
132
+ if: { after: true },
133
+ else: { after: true },
134
+ for: { after: true },
135
+ while: { after: true },
136
+ do: { after: true },
137
+ switch: { after: true },
138
+ return: { after: true },
139
+ },
140
+ },
141
+ ],
142
+ "arrow-spacing": [
143
+ 2,
144
+ {
145
+ after: true,
146
+ before: true,
147
+ },
148
+ ],
149
+ "@stylistic/func-call-spacing": 2,
150
+ "@stylistic/type-annotation-spacing": 2,
151
+
152
+ // file whitespace
153
+ "no-multiple-empty-lines": [2, { max: 2, maxEOF: 0 }],
154
+ "no-mixed-spaces-and-tabs": 2,
155
+ "no-trailing-spaces": 2,
156
+ "linebreak-style": [process.platform === "win32" ? 0 : 2, "unix"],
157
+ indent: [
158
+ 2,
159
+ 2,
160
+ { SwitchCase: 1, CallExpression: { arguments: 2 }, MemberExpression: 2 },
161
+ ],
162
+ "key-spacing": [
163
+ 2,
164
+ {
165
+ beforeColon: false,
166
+ },
167
+ ],
168
+ "eol-last": 2,
169
+
170
+ // copyright
171
+ "notice/notice": [
172
+ 2,
173
+ {
174
+ mustMatch: "Copyright",
175
+ templateFile: path.join(__dirname, "utils", "copyright.js"),
176
+ },
177
+ ],
178
+
179
+ // react
180
+ "react/react-in-jsx-scope": 0,
181
+ };
182
+
183
+ const languageOptions = {
184
+ parser: tsParser,
185
+ ecmaVersion: 9,
186
+ sourceType: "module",
187
+ };
188
+
189
+ export default [
190
+ {
191
+ ignores: ["**/*.js"],
192
+ },
193
+ {
194
+ files: ["**/*.ts", "**/*.tsx"],
195
+ plugins,
196
+ languageOptions,
197
+ rules: baseRules,
198
+ },
199
+ ];
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "@playwright/mcp",
3
+ "version": "0.0.1",
4
+ "description": "Playwright Tools for MCP",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/microsoft/playwright-mcp.git"
8
+ },
9
+ "homepage": "https://playwright.dev",
10
+ "engines": {
11
+ "node": ">=18"
12
+ },
13
+ "author": {
14
+ "name": "Microsoft Corporation"
15
+ },
16
+ "license": "Apache-2.0",
17
+ "scripts": {
18
+ "build": "tsc",
19
+ "lint": "eslint .",
20
+ "watch": "tsc --watch"
21
+ },
22
+ "exports": {
23
+ "./servers/server": "./lib/servers/server.js",
24
+ "./servers/screenshot": "./lib/servers/screenshot.js",
25
+ "./servers/snapshot": "./lib/servers/snapshot.js",
26
+ "./tools/common": "./lib/tools/common.js",
27
+ "./tools/screenshot": "./lib/tools/screenshot.js",
28
+ "./tools/snapshot": "./lib/tools/snapshot.js",
29
+ "./package.json": "./package.json"
30
+ },
31
+ "dependencies": {
32
+ "@modelcontextprotocol/sdk": "^1.6.1",
33
+ "commander": "^13.1.0",
34
+ "playwright": "1.52.0-alpha-2025-03-21",
35
+ "zod-to-json-schema": "^3.24.4"
36
+ },
37
+ "devDependencies": {
38
+ "@eslint/eslintrc": "^3.2.0",
39
+ "@eslint/js": "^9.19.0",
40
+ "@stylistic/eslint-plugin": "^3.0.1",
41
+ "@typescript-eslint/eslint-plugin": "^8.26.1",
42
+ "@typescript-eslint/parser": "^8.26.1",
43
+ "@typescript-eslint/utils": "^8.26.1",
44
+ "@types/node": "^22.13.10",
45
+ "eslint": "^9.19.0",
46
+ "eslint-plugin-import": "^2.31.0",
47
+ "eslint-plugin-notice": "^1.0.0",
48
+ "typescript": "^5.8.2"
49
+ },
50
+ "bin": {
51
+ "mcp": "cli.js"
52
+ }
53
+ }
playwright.config.ts ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Copyright (c) Microsoft Corporation.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import { defineConfig } from '@playwright/test';
18
+
19
+ export default defineConfig({
20
+ testDir: './tests',
21
+ fullyParallel: true,
22
+ forbidOnly: !!process.env.CI,
23
+ retries: process.env.CI ? 2 : 0,
24
+ workers: process.env.CI ? 1 : undefined,
25
+ reporter: 'list',
26
+ projects: [{ name: 'default' }],
27
+ });
src/context.ts ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Copyright (c) Microsoft Corporation.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import * as playwright from 'playwright';
18
+
19
+ export class Context {
20
+ private _launchOptions: playwright.LaunchOptions;
21
+ private _page: playwright.Page | undefined;
22
+ private _console: playwright.ConsoleMessage[] = [];
23
+ private _initializePromise: Promise<void> | undefined;
24
+
25
+ constructor(launchOptions: playwright.LaunchOptions) {
26
+ this._launchOptions = launchOptions;
27
+ }
28
+
29
+ async ensurePage(): Promise<playwright.Page> {
30
+ await this._initialize();
31
+ return this._page!;
32
+ }
33
+
34
+ async ensureConsole(): Promise<playwright.ConsoleMessage[]> {
35
+ await this._initialize();
36
+ return this._console;
37
+ }
38
+
39
+ async close() {
40
+ const page = await this.ensurePage();
41
+ await page.close();
42
+ this._initializePromise = undefined;
43
+ }
44
+
45
+ private async _initialize() {
46
+ if (this._initializePromise)
47
+ return this._initializePromise;
48
+ this._initializePromise = (async () => {
49
+ const browser = await this._createBrowser();
50
+ this._page = await browser.newPage();
51
+ this._page.on('console', event => this._console.push(event));
52
+ this._page.on('framenavigated', () => this._console.length = 0);
53
+ })();
54
+ return this._initializePromise;
55
+ }
56
+
57
+ private async _createBrowser(): Promise<playwright.Browser> {
58
+ if (process.env.PLAYWRIGHT_WS_ENDPOINT) {
59
+ const url = new URL(process.env.PLAYWRIGHT_WS_ENDPOINT);
60
+ url.searchParams.set('launch-options', JSON.stringify(this._launchOptions));
61
+ return await playwright.chromium.connect(String(url));
62
+ }
63
+ return await playwright.chromium.launch({ channel: 'chrome', ...this._launchOptions });
64
+ }
65
+ }
src/program.ts ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Copyright (c) Microsoft Corporation.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import { program } from 'commander';
18
+
19
+ import { Server } from './server';
20
+ import * as snapshot from './tools/snapshot';
21
+ import * as common from './tools/common';
22
+ import * as screenshot from './tools/screenshot';
23
+ import { console } from './resources/console';
24
+
25
+ import type { LaunchOptions } from './server';
26
+ import type { Tool } from './tools/tool';
27
+ import type { Resource } from './resources/resource';
28
+
29
+ const packageJSON = require('../package.json');
30
+
31
+ program
32
+ .version('Version ' + packageJSON.version)
33
+ .name(packageJSON.name)
34
+ .option('--headless', 'Run browser in headless mode, headed by default')
35
+ .option('--vision', 'Run server that uses screenshots (Aria snapshots are used by default)')
36
+ .action(async options => {
37
+ const launchOptions: LaunchOptions = {
38
+ headless: !!options.headless,
39
+ };
40
+ const tools = options.vision ? screenshotTools : snapshotTools;
41
+ const server = new Server({
42
+ name: 'Playwright',
43
+ version: packageJSON.version,
44
+ tools,
45
+ resources,
46
+ }, launchOptions);
47
+ setupExitWatchdog(server);
48
+ await server.start();
49
+ });
50
+
51
+ function setupExitWatchdog(server: Server) {
52
+ process.stdin.on('close', async () => {
53
+ setTimeout(() => process.exit(0), 15000);
54
+ await server?.stop();
55
+ process.exit(0);
56
+ });
57
+ }
58
+
59
+ const commonTools: Tool[] = [
60
+ common.pressKey,
61
+ common.wait,
62
+ common.pdf,
63
+ common.close,
64
+ ];
65
+
66
+ const snapshotTools: Tool[] = [
67
+ common.navigate(true),
68
+ common.goBack(true),
69
+ common.goForward(true),
70
+ snapshot.snapshot,
71
+ snapshot.click,
72
+ snapshot.hover,
73
+ snapshot.type,
74
+ ...commonTools,
75
+ ];
76
+
77
+ const screenshotTools: Tool[] = [
78
+ common.navigate(false),
79
+ common.goBack(false),
80
+ common.goForward(false),
81
+ screenshot.screenshot,
82
+ screenshot.moveMouse,
83
+ screenshot.click,
84
+ screenshot.drag,
85
+ screenshot.type,
86
+ ...commonTools,
87
+ ];
88
+
89
+ const resources: Resource[] = [
90
+ console,
91
+ ];
92
+
93
+ program.parse(process.argv);
src/resources/console.ts ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Copyright (c) Microsoft Corporation.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import type { Resource, ResourceResult } from './resource';
18
+
19
+ export const console: Resource = {
20
+ schema: {
21
+ uri: 'browser://console',
22
+ name: 'Page console',
23
+ mimeType: 'text/plain',
24
+ },
25
+
26
+ read: async (context, uri) => {
27
+ const result: ResourceResult[] = [];
28
+ for (const message of await context.ensureConsole()) {
29
+ result.push({
30
+ uri,
31
+ mimeType: 'text/plain',
32
+ text: `[${message.type().toUpperCase()}] ${message.text()}`,
33
+ });
34
+ }
35
+ return result;
36
+ },
37
+ };
src/resources/resource.ts ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Copyright (c) Microsoft Corporation.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import type { Context } from '../context';
18
+
19
+ export type ResourceSchema = {
20
+ uri: string;
21
+ name: string;
22
+ description?: string;
23
+ mimeType?: string;
24
+ };
25
+
26
+ export type ResourceResult = {
27
+ uri: string;
28
+ mimeType?: string;
29
+ text?: string;
30
+ blob?: string;
31
+ };
32
+
33
+ export type Resource = {
34
+ schema: ResourceSchema;
35
+ read: (context: Context, uri: string) => Promise<ResourceResult[]>;
36
+ };
src/server.ts ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Copyright (c) Microsoft Corporation.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import { Server as MCPServer } from '@modelcontextprotocol/sdk/server/index.js';
18
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
19
+ import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema } from '@modelcontextprotocol/sdk/types.js';
20
+ import * as playwright from 'playwright';
21
+
22
+ import { Context } from './context';
23
+
24
+ import type { Tool } from './tools/tool';
25
+ import type { Resource } from './resources/resource';
26
+
27
+ export type LaunchOptions = {
28
+ headless?: boolean;
29
+ };
30
+
31
+ export class Server {
32
+ private _server: MCPServer;
33
+ private _tools: Tool[];
34
+ private _page: playwright.Page | undefined;
35
+ private _context: Context;
36
+
37
+ constructor(options: { name: string, version: string, tools: Tool[], resources: Resource[] }, launchOptions: LaunchOptions) {
38
+ const { name, version, tools, resources } = options;
39
+ this._context = new Context(launchOptions);
40
+ this._server = new MCPServer({ name, version }, {
41
+ capabilities: {
42
+ tools: {},
43
+ resources: {},
44
+ }
45
+ });
46
+ this._tools = tools;
47
+
48
+ this._server.setRequestHandler(ListToolsRequestSchema, async () => {
49
+ return { tools: tools.map(tool => tool.schema) };
50
+ });
51
+
52
+ this._server.setRequestHandler(ListResourcesRequestSchema, async () => {
53
+ return { resources: resources.map(resource => resource.schema) };
54
+ });
55
+
56
+ this._server.setRequestHandler(CallToolRequestSchema, async request => {
57
+ const tool = this._tools.find(tool => tool.schema.name === request.params.name);
58
+ if (!tool) {
59
+ return {
60
+ content: [{ type: 'text', text: `Tool "${request.params.name}" not found` }],
61
+ isError: true,
62
+ };
63
+ }
64
+
65
+ try {
66
+ const result = await tool.handle(this._context, request.params.arguments);
67
+ return result;
68
+ } catch (error) {
69
+ return {
70
+ content: [{ type: 'text', text: String(error) }],
71
+ isError: true,
72
+ };
73
+ }
74
+ });
75
+
76
+ this._server.setRequestHandler(ReadResourceRequestSchema, async request => {
77
+ const resource = resources.find(resource => resource.schema.uri === request.params.uri);
78
+ if (!resource)
79
+ return { contents: [] };
80
+
81
+ const contents = await resource.read(this._context, request.params.uri);
82
+ return { contents };
83
+ });
84
+ }
85
+
86
+ async start() {
87
+ const transport = new StdioServerTransport();
88
+ await this._server.connect(transport);
89
+ }
90
+
91
+ async stop() {
92
+ await this._server.close();
93
+ await this._page?.context()?.browser()?.close();
94
+ }
95
+ }
src/tools/common.ts ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Copyright (c) Microsoft Corporation.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import os from 'os';
18
+ import path from 'path';
19
+
20
+ import { z } from 'zod';
21
+ import { zodToJsonSchema } from 'zod-to-json-schema';
22
+
23
+ import { captureAriaSnapshot, runAndWait } from './utils';
24
+
25
+ import type { ToolFactory, Tool } from './tool';
26
+
27
+ const navigateSchema = z.object({
28
+ url: z.string().describe('The URL to navigate to'),
29
+ });
30
+
31
+ export const navigate: ToolFactory = snapshot => ({
32
+ schema: {
33
+ name: 'browser_navigate',
34
+ description: 'Navigate to a URL',
35
+ inputSchema: zodToJsonSchema(navigateSchema),
36
+ },
37
+ handle: async (context, params) => {
38
+ const validatedParams = navigateSchema.parse(params);
39
+ const page = await context.ensurePage();
40
+ await page.goto(validatedParams.url, { waitUntil: 'domcontentloaded' });
41
+ // Cap load event to 5 seconds, the page is operational at this point.
42
+ await page.waitForLoadState('load', { timeout: 5000 }).catch(() => {});
43
+ if (snapshot)
44
+ return captureAriaSnapshot(page);
45
+ return {
46
+ content: [{
47
+ type: 'text',
48
+ text: `Navigated to ${validatedParams.url}`,
49
+ }],
50
+ };
51
+ },
52
+ });
53
+
54
+ const goBackSchema = z.object({});
55
+
56
+ export const goBack: ToolFactory = snapshot => ({
57
+ schema: {
58
+ name: 'browser_go_back',
59
+ description: 'Go back to the previous page',
60
+ inputSchema: zodToJsonSchema(goBackSchema),
61
+ },
62
+ handle: async context => {
63
+ return await runAndWait(context, 'Navigated back', async () => {
64
+ const page = await context.ensurePage();
65
+ await page.goBack();
66
+ }, snapshot);
67
+ },
68
+ });
69
+
70
+ const goForwardSchema = z.object({});
71
+
72
+ export const goForward: ToolFactory = snapshot => ({
73
+ schema: {
74
+ name: 'browser_go_forward',
75
+ description: 'Go forward to the next page',
76
+ inputSchema: zodToJsonSchema(goForwardSchema),
77
+ },
78
+ handle: async context => {
79
+ return await runAndWait(context, 'Navigated forward', async () => {
80
+ const page = await context.ensurePage();
81
+ await page.goForward();
82
+ }, snapshot);
83
+ },
84
+ });
85
+
86
+ const waitSchema = z.object({
87
+ time: z.number().describe('The time to wait in seconds'),
88
+ });
89
+
90
+ export const wait: Tool = {
91
+ schema: {
92
+ name: 'browser_wait',
93
+ description: 'Wait for a specified time in seconds',
94
+ inputSchema: zodToJsonSchema(waitSchema),
95
+ },
96
+ handle: async (context, params) => {
97
+ const validatedParams = waitSchema.parse(params);
98
+ const page = await context.ensurePage();
99
+ await page.waitForTimeout(Math.min(10000, validatedParams.time * 1000));
100
+ return {
101
+ content: [{
102
+ type: 'text',
103
+ text: `Waited for ${validatedParams.time} seconds`,
104
+ }],
105
+ };
106
+ },
107
+ };
108
+
109
+ const pressKeySchema = z.object({
110
+ key: z.string().describe('Name of the key to press or a character to generate, such as `ArrowLeft` or `a`'),
111
+ });
112
+
113
+ export const pressKey: Tool = {
114
+ schema: {
115
+ name: 'browser_press_key',
116
+ description: 'Press a key on the keyboard',
117
+ inputSchema: zodToJsonSchema(pressKeySchema),
118
+ },
119
+ handle: async (context, params) => {
120
+ const validatedParams = pressKeySchema.parse(params);
121
+ return await runAndWait(context, `Pressed key ${validatedParams.key}`, async page => {
122
+ await page.keyboard.press(validatedParams.key);
123
+ });
124
+ },
125
+ };
126
+
127
+ const pdfSchema = z.object({});
128
+
129
+ export const pdf: Tool = {
130
+ schema: {
131
+ name: 'browser_save_as_pdf',
132
+ description: 'Save page as PDF',
133
+ inputSchema: zodToJsonSchema(pdfSchema),
134
+ },
135
+ handle: async context => {
136
+ const page = await context.ensurePage();
137
+ const fileName = path.join(os.tmpdir(), `/page-${new Date().toISOString()}.pdf`);
138
+ await page.pdf({ path: fileName });
139
+ return {
140
+ content: [{
141
+ type: 'text',
142
+ text: `Saved as ${fileName}`,
143
+ }],
144
+ };
145
+ },
146
+ };
147
+
148
+ const closeSchema = z.object({});
149
+
150
+ export const close: Tool = {
151
+ schema: {
152
+ name: 'browser_close',
153
+ description: 'Close the page',
154
+ inputSchema: zodToJsonSchema(closeSchema),
155
+ },
156
+ handle: async context => {
157
+ await context.close();
158
+ return {
159
+ content: [{
160
+ type: 'text',
161
+ text: `Page closed`,
162
+ }],
163
+ };
164
+ },
165
+ };
src/tools/screenshot.ts ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Copyright (c) Microsoft Corporation.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import { z } from 'zod';
18
+ import { zodToJsonSchema } from 'zod-to-json-schema';
19
+
20
+ import { runAndWait } from './utils';
21
+
22
+ import type { Tool } from './tool';
23
+
24
+ export const screenshot: Tool = {
25
+ schema: {
26
+ name: 'browser_screenshot',
27
+ description: 'Take a screenshot of the current page',
28
+ inputSchema: zodToJsonSchema(z.object({})),
29
+ },
30
+
31
+ handle: async context => {
32
+ const page = await context.ensurePage();
33
+ const screenshot = await page.screenshot({ type: 'jpeg', quality: 50, scale: 'css' });
34
+ return {
35
+ content: [{ type: 'image', data: screenshot.toString('base64'), mimeType: 'image/jpeg' }],
36
+ };
37
+ },
38
+ };
39
+
40
+ const elementSchema = z.object({
41
+ element: z.string().describe('Human-readable element description used to obtain permission to interact with the element'),
42
+ });
43
+
44
+ const moveMouseSchema = elementSchema.extend({
45
+ x: z.number().describe('X coordinate'),
46
+ y: z.number().describe('Y coordinate'),
47
+ });
48
+
49
+ export const moveMouse: Tool = {
50
+ schema: {
51
+ name: 'browser_move_mouse',
52
+ description: 'Move mouse to a given position',
53
+ inputSchema: zodToJsonSchema(moveMouseSchema),
54
+ },
55
+
56
+ handle: async (context, params) => {
57
+ const validatedParams = moveMouseSchema.parse(params);
58
+ const page = await context.ensurePage();
59
+ await page.mouse.move(validatedParams.x, validatedParams.y);
60
+ return {
61
+ content: [{ type: 'text', text: `Moved mouse to (${validatedParams.x}, ${validatedParams.y})` }],
62
+ };
63
+ },
64
+ };
65
+
66
+ const clickSchema = elementSchema.extend({
67
+ x: z.number().describe('X coordinate'),
68
+ y: z.number().describe('Y coordinate'),
69
+ });
70
+
71
+ export const click: Tool = {
72
+ schema: {
73
+ name: 'browser_click',
74
+ description: 'Click left mouse button',
75
+ inputSchema: zodToJsonSchema(clickSchema),
76
+ },
77
+
78
+ handle: async (context, params) => {
79
+ return await runAndWait(context, 'Clicked mouse', async page => {
80
+ const validatedParams = clickSchema.parse(params);
81
+ await page.mouse.move(validatedParams.x, validatedParams.y);
82
+ await page.mouse.down();
83
+ await page.mouse.up();
84
+ });
85
+ },
86
+ };
87
+
88
+ const dragSchema = elementSchema.extend({
89
+ startX: z.number().describe('Start X coordinate'),
90
+ startY: z.number().describe('Start Y coordinate'),
91
+ endX: z.number().describe('End X coordinate'),
92
+ endY: z.number().describe('End Y coordinate'),
93
+ });
94
+
95
+ export const drag: Tool = {
96
+ schema: {
97
+ name: 'browser_drag',
98
+ description: 'Drag left mouse button',
99
+ inputSchema: zodToJsonSchema(dragSchema),
100
+ },
101
+
102
+ handle: async (context, params) => {
103
+ const validatedParams = dragSchema.parse(params);
104
+ return await runAndWait(context, `Dragged mouse from (${validatedParams.startX}, ${validatedParams.startY}) to (${validatedParams.endX}, ${validatedParams.endY})`, async page => {
105
+ await page.mouse.move(validatedParams.startX, validatedParams.startY);
106
+ await page.mouse.down();
107
+ await page.mouse.move(validatedParams.endX, validatedParams.endY);
108
+ await page.mouse.up();
109
+ });
110
+ },
111
+ };
112
+
113
+ const typeSchema = z.object({
114
+ text: z.string().describe('Text to type into the element'),
115
+ submit: z.boolean().describe('Whether to submit entered text (press Enter after)'),
116
+ });
117
+
118
+ export const type: Tool = {
119
+ schema: {
120
+ name: 'browser_type',
121
+ description: 'Type text',
122
+ inputSchema: zodToJsonSchema(typeSchema),
123
+ },
124
+
125
+ handle: async (context, params) => {
126
+ const validatedParams = typeSchema.parse(params);
127
+ return await runAndWait(context, `Typed text "${validatedParams.text}"`, async page => {
128
+ await page.keyboard.type(validatedParams.text);
129
+ if (validatedParams.submit)
130
+ await page.keyboard.press('Enter');
131
+ });
132
+ },
133
+ };
src/tools/snapshot.ts ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Copyright (c) Microsoft Corporation.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import { z } from 'zod';
18
+ import zodToJsonSchema from 'zod-to-json-schema';
19
+
20
+ import { captureAriaSnapshot, runAndWait } from './utils';
21
+
22
+ import type * as playwright from 'playwright';
23
+ import type { Tool } from './tool';
24
+
25
+ export const snapshot: Tool = {
26
+ schema: {
27
+ name: 'browser_snapshot',
28
+ description: 'Capture accessibility snapshot of the current page, this is better than screenshot',
29
+ inputSchema: zodToJsonSchema(z.object({})),
30
+ },
31
+
32
+ handle: async context => {
33
+ return await captureAriaSnapshot(await context.ensurePage());
34
+ },
35
+ };
36
+
37
+ const elementSchema = z.object({
38
+ element: z.string().describe('Human-readable element description used to obtain permission to interact with the element'),
39
+ ref: z.string().describe('Exact target element reference from the page snapshot'),
40
+ });
41
+
42
+ export const click: Tool = {
43
+ schema: {
44
+ name: 'browser_click',
45
+ description: 'Perform click on a web page',
46
+ inputSchema: zodToJsonSchema(elementSchema),
47
+ },
48
+
49
+ handle: async (context, params) => {
50
+ const validatedParams = elementSchema.parse(params);
51
+ return runAndWait(context, `"${validatedParams.element}" clicked`, page => refLocator(page, validatedParams.ref).click(), true);
52
+ },
53
+ };
54
+
55
+ const dragSchema = z.object({
56
+ startElement: z.string().describe('Human-readable source element description used to obtain the permission to interact with the element'),
57
+ startRef: z.string().describe('Exact source element reference from the page snapshot'),
58
+ endElement: z.string().describe('Human-readable target element description used to obtain the permission to interact with the element'),
59
+ endRef: z.string().describe('Exact target element reference from the page snapshot'),
60
+ });
61
+
62
+ export const drag: Tool = {
63
+ schema: {
64
+ name: 'browser_drag',
65
+ description: 'Perform drag and drop between two elements',
66
+ inputSchema: zodToJsonSchema(dragSchema),
67
+ },
68
+
69
+ handle: async (context, params) => {
70
+ const validatedParams = dragSchema.parse(params);
71
+ return runAndWait(context, `Dragged "${validatedParams.startElement}" to "${validatedParams.endElement}"`, async page => {
72
+ const startLocator = refLocator(page, validatedParams.startRef);
73
+ const endLocator = refLocator(page, validatedParams.endRef);
74
+ await startLocator.dragTo(endLocator);
75
+ }, true);
76
+ },
77
+ };
78
+
79
+ export const hover: Tool = {
80
+ schema: {
81
+ name: 'browser_hover',
82
+ description: 'Hover over element on page',
83
+ inputSchema: zodToJsonSchema(elementSchema),
84
+ },
85
+
86
+ handle: async (context, params) => {
87
+ const validatedParams = elementSchema.parse(params);
88
+ return runAndWait(context, `Hovered over "${validatedParams.element}"`, page => refLocator(page, validatedParams.ref).hover(), true);
89
+ },
90
+ };
91
+
92
+ const typeSchema = elementSchema.extend({
93
+ text: z.string().describe('Text to type into the element'),
94
+ submit: z.boolean().describe('Whether to submit entered text (press Enter after)'),
95
+ });
96
+
97
+ export const type: Tool = {
98
+ schema: {
99
+ name: 'browser_type',
100
+ description: 'Type text into editable element',
101
+ inputSchema: zodToJsonSchema(typeSchema),
102
+ },
103
+
104
+ handle: async (context, params) => {
105
+ const validatedParams = typeSchema.parse(params);
106
+ return await runAndWait(context, `Typed "${validatedParams.text}" into "${validatedParams.element}"`, async page => {
107
+ const locator = refLocator(page, validatedParams.ref);
108
+ await locator.fill(validatedParams.text);
109
+ if (validatedParams.submit)
110
+ await locator.press('Enter');
111
+ }, true);
112
+ },
113
+ };
114
+
115
+ function refLocator(page: playwright.Page, ref: string): playwright.Locator {
116
+ return page.locator(`aria-ref=${ref}`);
117
+ }
src/tools/tool.ts ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Copyright (c) Microsoft Corporation.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import type { ImageContent, TextContent } from '@modelcontextprotocol/sdk/types';
18
+ import type { JsonSchema7Type } from 'zod-to-json-schema';
19
+ import type { Context } from '../context';
20
+
21
+ export type ToolSchema = {
22
+ name: string;
23
+ description: string;
24
+ inputSchema: JsonSchema7Type;
25
+ };
26
+
27
+ export type ToolResult = {
28
+ content: (ImageContent | TextContent)[];
29
+ isError?: boolean;
30
+ };
31
+
32
+ export type Tool = {
33
+ schema: ToolSchema;
34
+ handle: (context: Context, params?: Record<string, any>) => Promise<ToolResult>;
35
+ };
36
+
37
+ export type ToolFactory = (snapshot: boolean) => Tool;
src/tools/utils.ts ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Copyright (c) Microsoft Corporation.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import type * as playwright from 'playwright';
18
+ import type { ToolResult } from './tool';
19
+ import type { Context } from '../context';
20
+
21
+ async function waitForCompletion<R>(page: playwright.Page, callback: () => Promise<R>): Promise<R> {
22
+ const requests = new Set<playwright.Request>();
23
+ let frameNavigated = false;
24
+ let waitCallback: () => void = () => {};
25
+ const waitBarrier = new Promise<void>(f => { waitCallback = f; });
26
+
27
+ const requestListener = (request: playwright.Request) => requests.add(request);
28
+ const requestFinishedListener = (request: playwright.Request) => {
29
+ requests.delete(request);
30
+ if (!requests.size)
31
+ waitCallback();
32
+ };
33
+
34
+ const frameNavigateListener = (frame: playwright.Frame) => {
35
+ if (frame.parentFrame())
36
+ return;
37
+ frameNavigated = true;
38
+ dispose();
39
+ clearTimeout(timeout);
40
+ void frame.waitForLoadState('load').then(() => {
41
+ waitCallback();
42
+ });
43
+ };
44
+
45
+ const onTimeout = () => {
46
+ dispose();
47
+ waitCallback();
48
+ };
49
+
50
+ page.on('request', requestListener);
51
+ page.on('requestfinished', requestFinishedListener);
52
+ page.on('framenavigated', frameNavigateListener);
53
+ const timeout = setTimeout(onTimeout, 10000);
54
+
55
+ const dispose = () => {
56
+ page.off('request', requestListener);
57
+ page.off('requestfinished', requestFinishedListener);
58
+ page.off('framenavigated', frameNavigateListener);
59
+ clearTimeout(timeout);
60
+ };
61
+
62
+ try {
63
+ const result = await callback();
64
+ if (!requests.size && !frameNavigated)
65
+ waitCallback();
66
+ await waitBarrier;
67
+ await page.evaluate(() => new Promise(f => setTimeout(f, 1000)));
68
+ return result;
69
+ } finally {
70
+ dispose();
71
+ }
72
+ }
73
+
74
+ export async function runAndWait(context: Context, status: string, callback: (page: playwright.Page) => Promise<any>, snapshot: boolean = false): Promise<ToolResult> {
75
+ const page = await context.ensurePage();
76
+ await waitForCompletion(page, () => callback(page));
77
+ return snapshot ? captureAriaSnapshot(page, status) : {
78
+ content: [{ type: 'text', text: status }],
79
+ };
80
+ }
81
+
82
+ export async function captureAriaSnapshot(page: playwright.Page, status: string = ''): Promise<ToolResult> {
83
+ const snapshot = await page.locator('html').ariaSnapshot({ ref: true });
84
+ return {
85
+ content: [{ type: 'text', text: `${status ? `${status}\n` : ''}
86
+ - Page URL: ${page.url()}
87
+ - Page Title: ${await page.title()}
88
+ - Page Snapshot
89
+ \`\`\`yaml
90
+ ${snapshot}
91
+ \`\`\`
92
+ `
93
+ }],
94
+ };
95
+ }
tests/basic.spec.ts ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Copyright (c) Microsoft Corporation.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import { test, expect } from './fixtures';
18
+
19
+ test('test tool list', async ({ server }) => {
20
+ const list = await server.send({
21
+ jsonrpc: '2.0',
22
+ id: 1,
23
+ method: 'tools/list',
24
+ });
25
+
26
+ expect(list).toEqual(expect.objectContaining({
27
+ id: 1,
28
+ result: expect.objectContaining({
29
+ tools: [
30
+ expect.objectContaining({
31
+ name: 'browser_navigate',
32
+ }),
33
+ expect.objectContaining({
34
+ name: 'browser_go_back',
35
+ }),
36
+ expect.objectContaining({
37
+ name: 'browser_go_forward',
38
+ }),
39
+ expect.objectContaining({
40
+ name: 'browser_snapshot',
41
+ }),
42
+ expect.objectContaining({
43
+ name: 'browser_click',
44
+ }),
45
+ expect.objectContaining({
46
+ name: 'browser_hover',
47
+ }),
48
+ expect.objectContaining({
49
+ name: 'browser_type',
50
+ }),
51
+ expect.objectContaining({
52
+ name: 'browser_press_key',
53
+ }),
54
+ expect.objectContaining({
55
+ name: 'browser_wait',
56
+ }),
57
+ expect.objectContaining({
58
+ name: 'browser_save_as_pdf',
59
+ }),
60
+ expect.objectContaining({
61
+ name: 'browser_close',
62
+ }),
63
+ ],
64
+ }),
65
+ }));
66
+ });
67
+
68
+ test('test resources list', async ({ server }) => {
69
+ const list = await server.send({
70
+ jsonrpc: '2.0',
71
+ id: 2,
72
+ method: 'resources/list',
73
+ });
74
+
75
+ expect(list).toEqual(expect.objectContaining({
76
+ id: 2,
77
+ result: expect.objectContaining({
78
+ resources: [
79
+ expect.objectContaining({
80
+ uri: 'browser://console',
81
+ mimeType: 'text/plain',
82
+ }),
83
+ ],
84
+ }),
85
+ }));
86
+ });
87
+
88
+ test('test browser_navigate', async ({ server }) => {
89
+ const response = await server.send({
90
+ jsonrpc: '2.0',
91
+ id: 2,
92
+ method: 'tools/call',
93
+ params: {
94
+ name: 'browser_navigate',
95
+ arguments: {
96
+ url: 'data:text/html,<html><title>Title</title><body>Hello, world!</body></html>',
97
+ },
98
+ },
99
+ });
100
+
101
+ expect(response).toEqual(expect.objectContaining({
102
+ id: 2,
103
+ result: {
104
+ content: [{
105
+ type: 'text',
106
+ text: `
107
+ - Page URL: data:text/html,<html><title>Title</title><body>Hello, world!</body></html>
108
+ - Page Title: Title
109
+ - Page Snapshot
110
+ \`\`\`yaml
111
+ - document [ref=s1e2]: Hello, world!
112
+ \`\`\`
113
+ `,
114
+ }],
115
+ },
116
+ }));
117
+ });
118
+
119
+ test('test browser_click', async ({ server }) => {
120
+ await server.send({
121
+ jsonrpc: '2.0',
122
+ id: 2,
123
+ method: 'tools/call',
124
+ params: {
125
+ name: 'browser_navigate',
126
+ arguments: {
127
+ url: 'data:text/html,<html><title>Title</title><button>Submit</button></html>',
128
+ },
129
+ },
130
+ });
131
+
132
+ const response = await server.send({
133
+ jsonrpc: '2.0',
134
+ id: 3,
135
+ method: 'tools/call',
136
+ params: {
137
+ name: 'browser_click',
138
+ arguments: {
139
+ element: 'Submit button',
140
+ ref: 's1e4',
141
+ },
142
+ },
143
+ });
144
+
145
+ expect(response).toEqual(expect.objectContaining({
146
+ id: 3,
147
+ result: {
148
+ content: [{
149
+ type: 'text',
150
+ text: `\"Submit button\" clicked
151
+
152
+ - Page URL: data:text/html,<html><title>Title</title><button>Submit</button></html>
153
+ - Page Title: Title
154
+ - Page Snapshot
155
+ \`\`\`yaml
156
+ - document [ref=s2e2]:
157
+ - button \"Submit\" [ref=s2e4]
158
+ \`\`\`
159
+ `,
160
+ }],
161
+ },
162
+ }));
163
+ });
tests/fixtures.ts ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Copyright (c) Microsoft Corporation.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import path from 'path';
18
+ import { spawn } from 'child_process';
19
+ import EventEmitter from 'events';
20
+
21
+ import { test as baseTest, expect } from '@playwright/test';
22
+
23
+ import type { ChildProcess } from 'child_process';
24
+
25
+ export { expect } from '@playwright/test';
26
+
27
+ class MCPServer extends EventEmitter {
28
+ private _child: ChildProcess;
29
+ private _messageQueue: any[] = [];
30
+ private _messageResolvers: ((value: any) => void)[] = [];
31
+ private _buffer: string = '';
32
+
33
+ constructor(command: string, args: string[]) {
34
+ super();
35
+ this._child = spawn(command, args, {
36
+ stdio: ['pipe', 'pipe', 'pipe'],
37
+ });
38
+
39
+ this._child.stdout?.on('data', data => {
40
+ this._buffer += data.toString();
41
+ let newlineIndex: number;
42
+
43
+ while ((newlineIndex = this._buffer.indexOf('\n')) !== -1) {
44
+ const message = this._buffer.slice(0, newlineIndex).trim();
45
+ this._buffer = this._buffer.slice(newlineIndex + 1);
46
+
47
+ if (!message)
48
+ continue;
49
+
50
+ const parsed = JSON.parse(message);
51
+ if (this._messageResolvers.length > 0) {
52
+ const resolve = this._messageResolvers.shift();
53
+ resolve!(parsed);
54
+ } else {
55
+ this._messageQueue.push(parsed);
56
+ }
57
+ }
58
+ });
59
+
60
+ this._child.stderr?.on('data', data => {
61
+ throw new Error('Server stderr:', data.toString());
62
+ });
63
+
64
+ this._child.on('exit', code => {
65
+ if (code !== 0)
66
+ throw new Error(`Server exited with code ${code}`);
67
+ });
68
+ }
69
+
70
+ async send(message: any, options?: { timeout?: number }): Promise<void> {
71
+ await this.sendNoReply(message);
72
+ return this._waitForResponse(options || {});
73
+ }
74
+
75
+ async sendNoReply(message: any): Promise<void> {
76
+ const jsonMessage = JSON.stringify(message) + '\n';
77
+ await new Promise<void>((resolve, reject) => {
78
+ this._child.stdin?.write(jsonMessage, err => {
79
+ if (err)
80
+ reject(err);
81
+ else
82
+ resolve();
83
+ });
84
+ });
85
+ }
86
+
87
+ private async _waitForResponse(options: { timeout?: number }): Promise<any> {
88
+ if (this._messageQueue.length > 0)
89
+ return this._messageQueue.shift();
90
+
91
+ return new Promise((resolve, reject) => {
92
+ const timeoutId = setTimeout(() => {
93
+ reject(new Error('Timeout waiting for message'));
94
+ }, options.timeout || 5000);
95
+
96
+ this._messageResolvers.push(message => {
97
+ clearTimeout(timeoutId);
98
+ resolve(message);
99
+ });
100
+ });
101
+ }
102
+
103
+ async close(): Promise<void> {
104
+ return new Promise(resolve => {
105
+ this._child.on('exit', () => resolve());
106
+ this._child.stdin?.end();
107
+ });
108
+ }
109
+ }
110
+
111
+ export const test = baseTest.extend<{ server: MCPServer }>({
112
+ server: async ({}, use) => {
113
+ const server = new MCPServer('node', [path.join(__dirname, '../cli.js'), '--headless']);
114
+ const initialize = await server.send({
115
+ jsonrpc: '2.0',
116
+ id: 0,
117
+ method: 'initialize',
118
+ params: {
119
+ protocolVersion: '2024-11-05',
120
+ capabilities: {},
121
+ clientInfo: {
122
+ name: 'Playwright Test',
123
+ version: '0.0.0',
124
+ },
125
+ },
126
+ });
127
+
128
+ expect(initialize).toEqual(expect.objectContaining({
129
+ id: 0,
130
+ result: expect.objectContaining({
131
+ protocolVersion: '2024-11-05',
132
+ capabilities: {
133
+ tools: {},
134
+ resources: {},
135
+ },
136
+ serverInfo: expect.objectContaining({
137
+ name: 'Playwright',
138
+ version: expect.any(String),
139
+ }),
140
+ }),
141
+ }));
142
+
143
+ await server.sendNoReply({
144
+ jsonrpc: '2.0',
145
+ method: 'notifications/initialized',
146
+ });
147
+
148
+ await use(server);
149
+ await server.close();
150
+ },
151
+ });
tsconfig.json ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "skipLibCheck": true,
5
+ "esModuleInterop": true,
6
+ "moduleResolution": "node",
7
+ "strict": true,
8
+ "module": "ESNext",
9
+ "outDir": "./lib"
10
+ },
11
+ "include": [
12
+ "src",
13
+ ],
14
+ }
utils/copyright.js ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Copyright (c) Microsoft Corporation.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */