macos-control-mcp 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +396 -0
- package/build/src/index.d.ts +2 -0
- package/build/src/index.js +207 -0
- package/build/src/index.js.map +1 -0
- package/build/src/tools/apps.d.ts +4 -0
- package/build/src/tools/apps.js +27 -0
- package/build/src/tools/apps.js.map +1 -0
- package/build/src/tools/browser.d.ts +3 -0
- package/build/src/tools/browser.js +74 -0
- package/build/src/tools/browser.js.map +1 -0
- package/build/src/tools/clipboard.d.ts +2 -0
- package/build/src/tools/clipboard.js +11 -0
- package/build/src/tools/clipboard.js.map +1 -0
- package/build/src/tools/input.d.ts +2 -0
- package/build/src/tools/input.js +82 -0
- package/build/src/tools/input.js.map +1 -0
- package/build/src/tools/mouse.d.ts +15 -0
- package/build/src/tools/mouse.js +67 -0
- package/build/src/tools/mouse.js.map +1 -0
- package/build/src/tools/ocr.d.ts +2 -0
- package/build/src/tools/ocr.js +155 -0
- package/build/src/tools/ocr.js.map +1 -0
- package/build/src/tools/screen.d.ts +4 -0
- package/build/src/tools/screen.js +5 -0
- package/build/src/tools/screen.js.map +1 -0
- package/build/src/tools/ui.d.ts +8 -0
- package/build/src/tools/ui.js +128 -0
- package/build/src/tools/ui.js.map +1 -0
- package/build/src/utils/applescript.d.ts +6 -0
- package/build/src/utils/applescript.js +49 -0
- package/build/src/utils/applescript.js.map +1 -0
- package/build/src/utils/python.d.ts +4 -0
- package/build/src/utils/python.js +58 -0
- package/build/src/utils/python.js.map +1 -0
- package/build/src/utils/screenshot.d.ts +4 -0
- package/build/src/utils/screenshot.js +38 -0
- package/build/src/utils/screenshot.js.map +1 -0
- package/package.json +54 -0
- package/requirements.txt +2 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Pierre Haddad
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
# macos-control-mcp
|
|
2
|
+
|
|
3
|
+
Give AI agents **eyes and hands** on macOS.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/macos-control-mcp)
|
|
6
|
+
[](./LICENSE)
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
## What is this?
|
|
10
|
+
|
|
11
|
+
An [MCP server](https://modelcontextprotocol.io) that lets AI agents **see your screen, read text on it, and interact** — click, type, scroll — just like a human sitting at the keyboard. Unlike blind script runners, this MCP gives agents _state awareness_: they screenshot the screen, OCR it to get text with pixel coordinates, then click exactly where they need to.
|
|
12
|
+
|
|
13
|
+
## The See-Think-Act Loop
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
┌─────────────────────────────────────────────────┐
|
|
17
|
+
│ │
|
|
18
|
+
│ 1. SEE screenshot / screen_ocr │
|
|
19
|
+
│ ↓ "What's on the screen?" │
|
|
20
|
+
│ │
|
|
21
|
+
│ 2. THINK AI reasons about the content │
|
|
22
|
+
│ ↓ "I need to click the Save btn" │
|
|
23
|
+
│ │
|
|
24
|
+
│ 3. ACT click_at / type_text / press_key│
|
|
25
|
+
│ "Click at (425, 300)" │
|
|
26
|
+
│ │
|
|
27
|
+
│ ↻ repeat │
|
|
28
|
+
└─────────────────────────────────────────────────┘
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
This is what makes it powerful: the agent _sees_ the result of every action and can course-correct, retry, or move on — just like you would.
|
|
32
|
+
|
|
33
|
+
## Quick Start
|
|
34
|
+
|
|
35
|
+
No install needed — run directly with npx:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npx -y macos-control-mcp
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
On first run, a Python virtual environment is automatically created at `~/.macos-control-mcp/.venv` with the required Apple Vision and Quartz frameworks. This takes ~60 seconds once and persists across updates.
|
|
42
|
+
|
|
43
|
+
## Configure Your AI Client
|
|
44
|
+
|
|
45
|
+
All clients use the same command: `npx -y macos-control-mcp`
|
|
46
|
+
|
|
47
|
+
<details>
|
|
48
|
+
<summary><strong>Claude Desktop</strong></summary>
|
|
49
|
+
|
|
50
|
+
Edit `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"mcpServers": {
|
|
55
|
+
"macos-control": {
|
|
56
|
+
"command": "npx",
|
|
57
|
+
"args": ["-y", "macos-control-mcp"]
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Restart Claude Desktop after saving.
|
|
64
|
+
</details>
|
|
65
|
+
|
|
66
|
+
<details>
|
|
67
|
+
<summary><strong>Claude Code</strong></summary>
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
claude mcp add macos-control -- npx -y macos-control-mcp
|
|
71
|
+
```
|
|
72
|
+
</details>
|
|
73
|
+
|
|
74
|
+
<details>
|
|
75
|
+
<summary><strong>VS Code / GitHub Copilot</strong></summary>
|
|
76
|
+
|
|
77
|
+
Add to `.vscode/mcp.json` in your workspace:
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"servers": {
|
|
82
|
+
"macos-control": {
|
|
83
|
+
"command": "npx",
|
|
84
|
+
"args": ["-y", "macos-control-mcp"]
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
</details>
|
|
90
|
+
|
|
91
|
+
<details>
|
|
92
|
+
<summary><strong>Cursor</strong></summary>
|
|
93
|
+
|
|
94
|
+
Add to `.cursor/mcp.json` in your project:
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{
|
|
98
|
+
"mcpServers": {
|
|
99
|
+
"macos-control": {
|
|
100
|
+
"command": "npx",
|
|
101
|
+
"args": ["-y", "macos-control-mcp"]
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
</details>
|
|
107
|
+
|
|
108
|
+
<details>
|
|
109
|
+
<summary><strong>Cline</strong></summary>
|
|
110
|
+
|
|
111
|
+
Open Cline extension settings → MCP Servers → Add:
|
|
112
|
+
|
|
113
|
+
```json
|
|
114
|
+
{
|
|
115
|
+
"macos-control": {
|
|
116
|
+
"command": "npx",
|
|
117
|
+
"args": ["-y", "macos-control-mcp"]
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
</details>
|
|
122
|
+
|
|
123
|
+
<details>
|
|
124
|
+
<summary><strong>Windsurf</strong></summary>
|
|
125
|
+
|
|
126
|
+
Add to `~/.codeium/windsurf/mcp_config.json`:
|
|
127
|
+
|
|
128
|
+
```json
|
|
129
|
+
{
|
|
130
|
+
"mcpServers": {
|
|
131
|
+
"macos-control": {
|
|
132
|
+
"command": "npx",
|
|
133
|
+
"args": ["-y", "macos-control-mcp"]
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
</details>
|
|
139
|
+
|
|
140
|
+
## Permissions
|
|
141
|
+
|
|
142
|
+
macOS requires two permissions for full functionality:
|
|
143
|
+
|
|
144
|
+
1. **Screen Recording** — for screenshots and OCR
|
|
145
|
+
2. **Accessibility** — for clicking, typing, and reading UI elements
|
|
146
|
+
|
|
147
|
+
Go to **System Settings → Privacy & Security** and add your terminal app (Terminal, iTerm2, VS Code, etc.) to both lists. You'll be prompted on first use.
|
|
148
|
+
|
|
149
|
+
## Tools (15)
|
|
150
|
+
|
|
151
|
+
### See the screen
|
|
152
|
+
|
|
153
|
+
| Tool | Description |
|
|
154
|
+
|---|---|
|
|
155
|
+
| `screenshot` | Capture full screen or app window as PNG |
|
|
156
|
+
| `screen_ocr` | OCR the screen — returns text elements with pixel coordinates |
|
|
157
|
+
| `find_text_on_screen` | Find specific text and get clickable x,y coordinates |
|
|
158
|
+
|
|
159
|
+
### Interact with the screen
|
|
160
|
+
|
|
161
|
+
| Tool | Description |
|
|
162
|
+
|---|---|
|
|
163
|
+
| `click_at` | Click at x,y coordinates (returns screenshot) |
|
|
164
|
+
| `double_click_at` | Double-click at x,y (returns screenshot) |
|
|
165
|
+
| `type_text` | Type text into the frontmost app |
|
|
166
|
+
| `press_key` | Press key combos (Cmd+S, Ctrl+C, etc.) |
|
|
167
|
+
| `scroll` | Scroll up/down/left/right |
|
|
168
|
+
|
|
169
|
+
### App management
|
|
170
|
+
|
|
171
|
+
| Tool | Description |
|
|
172
|
+
|---|---|
|
|
173
|
+
| `launch_app` | Open or focus an application |
|
|
174
|
+
| `list_running_apps` | List visible running apps |
|
|
175
|
+
|
|
176
|
+
### Accessibility tree
|
|
177
|
+
|
|
178
|
+
| Tool | Description |
|
|
179
|
+
|---|---|
|
|
180
|
+
| `get_ui_elements` | Get accessibility tree of an app window |
|
|
181
|
+
| `click_element` | Click a named UI element (returns screenshot) |
|
|
182
|
+
|
|
183
|
+
### Utilities
|
|
184
|
+
|
|
185
|
+
| Tool | Description |
|
|
186
|
+
|---|---|
|
|
187
|
+
| `open_url` | Open URL in Safari or Chrome |
|
|
188
|
+
| `get_clipboard` | Read clipboard contents |
|
|
189
|
+
| `set_clipboard` | Write to clipboard |
|
|
190
|
+
|
|
191
|
+
## Example Workflows
|
|
192
|
+
|
|
193
|
+
### Fill out a web form
|
|
194
|
+
|
|
195
|
+
```
|
|
196
|
+
You: "Go to example.com/signup and fill in my details"
|
|
197
|
+
|
|
198
|
+
Agent:
|
|
199
|
+
1. open_url("https://example.com/signup")
|
|
200
|
+
2. screenshot() → sees the form
|
|
201
|
+
3. screen_ocr() → finds "Email" field at (300, 250)
|
|
202
|
+
4. click_at(300, 250) → clicks the email field
|
|
203
|
+
5. type_text("user@example.com")
|
|
204
|
+
6. find_text_on_screen("Submit") → gets button coordinates
|
|
205
|
+
7. click_at(350, 500) → submits the form
|
|
206
|
+
8. screenshot() → confirms success
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Navigate an unfamiliar app
|
|
210
|
+
|
|
211
|
+
```
|
|
212
|
+
You: "Change the font size to 16 in TextEdit"
|
|
213
|
+
|
|
214
|
+
Agent:
|
|
215
|
+
1. launch_app("TextEdit")
|
|
216
|
+
2. screenshot() → sees the app
|
|
217
|
+
3. get_ui_elements("TextEdit") → finds menu items
|
|
218
|
+
4. press_key("t", ["command"]) → opens Fonts panel
|
|
219
|
+
5. screenshot() → sees the font panel
|
|
220
|
+
6. find_text_on_screen("Size") → locates the size field
|
|
221
|
+
7. click_at(x, y) → clicks size field
|
|
222
|
+
8. type_text("16")
|
|
223
|
+
9. press_key("return")
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Extract info from an email
|
|
227
|
+
|
|
228
|
+
```
|
|
229
|
+
You: "Copy the tracking number from the shipping email in Mail"
|
|
230
|
+
|
|
231
|
+
Agent:
|
|
232
|
+
1. launch_app("Mail")
|
|
233
|
+
2. screenshot() → sees the inbox
|
|
234
|
+
3. find_text_on_screen("Your order has shipped") → locates the email
|
|
235
|
+
4. click_at(x, y) → opens the email
|
|
236
|
+
5. screenshot() → sees the email content
|
|
237
|
+
6. screen_ocr() → extracts all text including tracking number
|
|
238
|
+
7. set_clipboard("1Z999AA10123456784") → copies to clipboard
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Take a screenshot and describe what's on screen
|
|
242
|
+
|
|
243
|
+
```
|
|
244
|
+
You: "What do I have open right now?"
|
|
245
|
+
|
|
246
|
+
Agent:
|
|
247
|
+
1. screenshot() → captures the full screen
|
|
248
|
+
2. Describes what it sees: apps, windows, content
|
|
249
|
+
3. list_running_apps() → ["Safari", "Slack", "VS Code", "Spotify"]
|
|
250
|
+
4. "You have Safari open on Twitter, Slack with 3 unread messages, and VS Code editing index.ts"
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Debug a UI issue
|
|
254
|
+
|
|
255
|
+
```
|
|
256
|
+
You: "The submit button on this page isn't working, can you check what's happening?"
|
|
257
|
+
|
|
258
|
+
Agent:
|
|
259
|
+
1. screenshot() → sees the page
|
|
260
|
+
2. find_text_on_screen("Submit") → locates button at (500, 600)
|
|
261
|
+
3. get_ui_elements("Safari") → inspects accessibility tree
|
|
262
|
+
4. "The button has aria-disabled=true. There's a required field 'Phone' that's empty."
|
|
263
|
+
5. find_text_on_screen("Phone") → locates the field
|
|
264
|
+
6. click_at(x, y) → clicks the field
|
|
265
|
+
7. type_text("+1 555-0123")
|
|
266
|
+
8. click_at(500, 600) → clicks Submit again
|
|
267
|
+
9. screenshot() → confirms it worked
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Control Spotify
|
|
271
|
+
|
|
272
|
+
```
|
|
273
|
+
You: "Pause the music and turn the volume down"
|
|
274
|
+
|
|
275
|
+
Agent:
|
|
276
|
+
1. launch_app("Spotify")
|
|
277
|
+
2. screenshot() → sees Spotify is playing
|
|
278
|
+
3. find_text_on_screen("Pause") → locates pause button
|
|
279
|
+
4. click_at(x, y) → pauses playback
|
|
280
|
+
5. find_text_on_screen("Volume") → locates volume slider area
|
|
281
|
+
6. click_at(x, y) → adjusts volume
|
|
282
|
+
7. screenshot() → confirms paused and volume lowered
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Work with Finder
|
|
286
|
+
|
|
287
|
+
```
|
|
288
|
+
You: "Create a new folder on my Desktop called 'Project Assets' and move it to Documents"
|
|
289
|
+
|
|
290
|
+
Agent:
|
|
291
|
+
1. launch_app("Finder")
|
|
292
|
+
2. press_key("d", ["command", "shift"]) → opens Desktop
|
|
293
|
+
3. screenshot() → sees Desktop in Finder
|
|
294
|
+
4. press_key("n", ["command", "shift"]) → creates new folder
|
|
295
|
+
5. type_text("Project Assets")
|
|
296
|
+
6. press_key("return")
|
|
297
|
+
7. screenshot() → confirms folder created
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### Send a message in Slack
|
|
301
|
+
|
|
302
|
+
```
|
|
303
|
+
You: "Send 'build is green, ready to deploy' in the #engineering channel on Slack"
|
|
304
|
+
|
|
305
|
+
Agent:
|
|
306
|
+
1. launch_app("Slack")
|
|
307
|
+
2. screenshot() → sees Slack
|
|
308
|
+
3. press_key("k", ["command"]) → opens Quick Switcher
|
|
309
|
+
4. type_text("engineering")
|
|
310
|
+
5. press_key("return") → opens #engineering
|
|
311
|
+
6. screenshot() → confirms channel is open
|
|
312
|
+
7. click_at(x, y) → clicks message input
|
|
313
|
+
8. type_text("build is green, ready to deploy")
|
|
314
|
+
9. press_key("return") → sends message
|
|
315
|
+
10. screenshot() → confirms sent
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### Research and copy data from a website
|
|
319
|
+
|
|
320
|
+
```
|
|
321
|
+
You: "Look up the current price of AAPL on Google Finance and copy it"
|
|
322
|
+
|
|
323
|
+
Agent:
|
|
324
|
+
1. open_url("https://google.com/finance/quote/AAPL:NASDAQ")
|
|
325
|
+
2. screenshot() → sees the page loading
|
|
326
|
+
3. screen_ocr() → reads all text on the page
|
|
327
|
+
4. Finds the price: "$187.42"
|
|
328
|
+
5. set_clipboard("$187.42")
|
|
329
|
+
6. "Copied AAPL price $187.42 to your clipboard"
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### Multi-app workflow
|
|
333
|
+
|
|
334
|
+
```
|
|
335
|
+
You: "Take what's in my clipboard, search for it in Safari, and screenshot the results"
|
|
336
|
+
|
|
337
|
+
Agent:
|
|
338
|
+
1. get_clipboard() → "best mechanical keyboards 2025"
|
|
339
|
+
2. launch_app("Safari")
|
|
340
|
+
3. press_key("l", ["command"]) → focuses address bar
|
|
341
|
+
4. type_text("best mechanical keyboards 2025")
|
|
342
|
+
5. press_key("return") → searches
|
|
343
|
+
6. screenshot() → captures the search results
|
|
344
|
+
7. "Here are the search results for 'best mechanical keyboards 2025'"
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
### Navigate System Settings
|
|
348
|
+
|
|
349
|
+
```
|
|
350
|
+
You: "Turn on Dark Mode"
|
|
351
|
+
|
|
352
|
+
Agent:
|
|
353
|
+
1. launch_app("System Settings")
|
|
354
|
+
2. screenshot() → sees System Settings
|
|
355
|
+
3. find_text_on_screen("Appearance") → locates the option
|
|
356
|
+
4. click_at(x, y) → opens Appearance settings
|
|
357
|
+
5. screenshot() → sees Light/Dark/Auto options
|
|
358
|
+
6. find_text_on_screen("Dark") → locates Dark mode option
|
|
359
|
+
7. click_at(x, y) → enables Dark Mode
|
|
360
|
+
8. screenshot() → confirms Dark Mode is on
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
## Requirements
|
|
364
|
+
|
|
365
|
+
- **macOS 13+** (Ventura or later)
|
|
366
|
+
- **Node.js 18+**
|
|
367
|
+
- **Python 3.9+** (pre-installed on macOS — needed for OCR and mouse control)
|
|
368
|
+
|
|
369
|
+
## How It Works
|
|
370
|
+
|
|
371
|
+
- **Screenshots** — native `screencapture` CLI
|
|
372
|
+
- **OCR** — Apple Vision framework (VNRecognizeTextRequest) via Python bridge, returns text with bounding box coordinates
|
|
373
|
+
- **Mouse** — Quartz Core Graphics events via Python bridge for precise pixel-level control
|
|
374
|
+
- **Keyboard & Apps** — AppleScript via `osascript` for key presses, app launching, and UI element interaction
|
|
375
|
+
- **Python env** — auto-managed venv at `~/.macos-control-mcp/.venv/` with only two packages (`pyobjc-framework-Vision`, `pyobjc-framework-Quartz`)
|
|
376
|
+
|
|
377
|
+
## Troubleshooting
|
|
378
|
+
|
|
379
|
+
**"Permission denied" or blank screenshots**
|
|
380
|
+
→ Add your terminal to System Settings → Privacy & Security → Screen Recording
|
|
381
|
+
|
|
382
|
+
**Clicks don't work**
|
|
383
|
+
→ Add your terminal to System Settings → Privacy & Security → Accessibility
|
|
384
|
+
|
|
385
|
+
**Python setup fails**
|
|
386
|
+
→ Ensure `python3` is in your PATH. Run `python3 --version` to check. Non-Python tools (keyboard, apps, clipboard) still work without it.
|
|
387
|
+
|
|
388
|
+
**OCR returns empty results**
|
|
389
|
+
→ Make sure Screen Recording permission is granted. Try a full-screen OCR first (without the `app` parameter).
|
|
390
|
+
|
|
391
|
+
**"App not found" errors**
|
|
392
|
+
→ Use the exact app name as shown in Activity Monitor (e.g., "Google Chrome" not "Chrome").
|
|
393
|
+
|
|
394
|
+
## License
|
|
395
|
+
|
|
396
|
+
[MIT](./LICENSE)
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { getScreenState } from "./tools/screen.js";
|
|
6
|
+
import { screenOCR, findTextOnScreen } from "./tools/ocr.js";
|
|
7
|
+
import { clickAt, doubleClickAt, scroll } from "./tools/mouse.js";
|
|
8
|
+
import { typeText, pressKey } from "./tools/input.js";
|
|
9
|
+
import { launchApp, listRunningApps } from "./tools/apps.js";
|
|
10
|
+
import { getUIElements, clickElement } from "./tools/ui.js";
|
|
11
|
+
import { openUrl } from "./tools/browser.js";
|
|
12
|
+
import { getClipboard, setClipboard } from "./tools/clipboard.js";
|
|
13
|
+
import { ensurePythonVenv } from "./utils/python.js";
|
|
14
|
+
const server = new McpServer({
|
|
15
|
+
name: "macos-control",
|
|
16
|
+
version: "0.0.1",
|
|
17
|
+
});
|
|
18
|
+
// ── See the screen ──────────────────────────────────────────────
|
|
19
|
+
server.tool("screenshot", "Capture a screenshot of the entire screen or a specific app window. Returns a PNG image.", { app: z.string().optional().describe("App name to capture. Omit for full screen.") }, async ({ app }) => {
|
|
20
|
+
try {
|
|
21
|
+
const { base64, mimeType } = await getScreenState(app);
|
|
22
|
+
return { content: [{ type: "image", data: base64, mimeType }] };
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
return { isError: true, content: [{ type: "text", text: String(err) }] };
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
server.tool("screen_ocr", "OCR the screen using Apple Vision. Returns every text element with pixel coordinates (x, y, centerX, centerY). Use centerX/centerY with click_at to click on any text.", { app: z.string().optional().describe("App to OCR. Omit for full screen.") }, async ({ app }) => {
|
|
29
|
+
try {
|
|
30
|
+
const result = await screenOCR(app);
|
|
31
|
+
return { content: [{ type: "text", text: result }] };
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
return { isError: true, content: [{ type: "text", text: String(err) }] };
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
server.tool("find_text_on_screen", "Find specific text on screen and get its clickable coordinates. Like Ctrl+F for the entire screen. Returns matches with centerX/centerY for use with click_at.", {
|
|
38
|
+
text: z.string().describe("Text to find (case-insensitive)"),
|
|
39
|
+
app: z.string().optional().describe("App to search in. Omit for full screen."),
|
|
40
|
+
}, async ({ text, app }) => {
|
|
41
|
+
try {
|
|
42
|
+
const result = await findTextOnScreen(text, app);
|
|
43
|
+
return { content: [{ type: "text", text: result }] };
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
return { isError: true, content: [{ type: "text", text: String(err) }] };
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
// ── Interact with the screen ────────────────────────────────────
|
|
50
|
+
server.tool("click_at", "Click at x,y screen coordinates. Returns a screenshot after clicking. Use screenshot + screen_ocr to find coordinates first.", {
|
|
51
|
+
x: z.number().describe("X coordinate"),
|
|
52
|
+
y: z.number().describe("Y coordinate"),
|
|
53
|
+
}, async ({ x, y }) => {
|
|
54
|
+
try {
|
|
55
|
+
const { text, screenshot } = await clickAt(x, y);
|
|
56
|
+
return {
|
|
57
|
+
content: [
|
|
58
|
+
{ type: "text", text },
|
|
59
|
+
{ type: "image", data: screenshot.base64, mimeType: screenshot.mimeType },
|
|
60
|
+
],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
return { isError: true, content: [{ type: "text", text: String(err) }] };
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
server.tool("double_click_at", "Double-click at x,y screen coordinates. Returns a screenshot after clicking.", {
|
|
68
|
+
x: z.number().describe("X coordinate"),
|
|
69
|
+
y: z.number().describe("Y coordinate"),
|
|
70
|
+
}, async ({ x, y }) => {
|
|
71
|
+
try {
|
|
72
|
+
const { text, screenshot } = await doubleClickAt(x, y);
|
|
73
|
+
return {
|
|
74
|
+
content: [
|
|
75
|
+
{ type: "text", text },
|
|
76
|
+
{ type: "image", data: screenshot.base64, mimeType: screenshot.mimeType },
|
|
77
|
+
],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
return { isError: true, content: [{ type: "text", text: String(err) }] };
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
server.tool("type_text", "Type text into the frontmost app using keyboard input.", { text: z.string().describe("Text to type") }, async ({ text }) => {
|
|
85
|
+
try {
|
|
86
|
+
const result = await typeText(text);
|
|
87
|
+
return { content: [{ type: "text", text: result }] };
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
return { isError: true, content: [{ type: "text", text: String(err) }] };
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
server.tool("press_key", "Press a key combo (e.g. press 's' with ['command'] for Cmd+S). Supports a-z, 0-9, return, tab, space, delete, escape, arrows, f1-f12.", {
|
|
94
|
+
key: z.string().describe("Key name: a-z, 0-9, return, tab, space, delete, escape, up/down/left/right, f1-f12"),
|
|
95
|
+
modifiers: z
|
|
96
|
+
.array(z.string())
|
|
97
|
+
.optional()
|
|
98
|
+
.describe("Modifier keys: 'command', 'shift', 'option', 'control'"),
|
|
99
|
+
}, async ({ key, modifiers }) => {
|
|
100
|
+
try {
|
|
101
|
+
const result = await pressKey(key, modifiers);
|
|
102
|
+
return { content: [{ type: "text", text: result }] };
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
return { isError: true, content: [{ type: "text", text: String(err) }] };
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
server.tool("scroll", "Scroll in the frontmost application.", {
|
|
109
|
+
direction: z.enum(["up", "down", "left", "right"]).describe("Scroll direction"),
|
|
110
|
+
amount: z.number().default(3).describe("Number of lines to scroll (default 3)"),
|
|
111
|
+
}, async ({ direction, amount }) => {
|
|
112
|
+
try {
|
|
113
|
+
const result = await scroll(direction, amount);
|
|
114
|
+
return { content: [{ type: "text", text: result }] };
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
return { isError: true, content: [{ type: "text", text: String(err) }] };
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
// ── App management ──────────────────────────────────────────────
|
|
121
|
+
server.tool("launch_app", "Open or focus a macOS application by name.", { name: z.string().describe("Application name, e.g. 'Safari', 'Notes'") }, async ({ name }) => {
|
|
122
|
+
try {
|
|
123
|
+
const text = await launchApp(name);
|
|
124
|
+
return { content: [{ type: "text", text }] };
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
return { isError: true, content: [{ type: "text", text: String(err) }] };
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
server.tool("list_running_apps", "List all visible running macOS applications.", async () => {
|
|
131
|
+
try {
|
|
132
|
+
const text = await listRunningApps();
|
|
133
|
+
return { content: [{ type: "text", text }] };
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
return { isError: true, content: [{ type: "text", text: String(err) }] };
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
// ── Accessibility tree ──────────────────────────────────────────
|
|
140
|
+
server.tool("get_ui_elements", "Get the accessibility tree of an app window. Returns UI element roles, names, and positions.", { app: z.string().describe("Application process name") }, async ({ app }) => {
|
|
141
|
+
try {
|
|
142
|
+
const text = await getUIElements(app);
|
|
143
|
+
return { content: [{ type: "text", text }] };
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
return { isError: true, content: [{ type: "text", text: String(err) }] };
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
server.tool("click_element", "Click a named UI element in an app window. Returns a screenshot after clicking. Use get_ui_elements to discover element names.", {
|
|
150
|
+
app: z.string().describe("Application process name"),
|
|
151
|
+
name: z.string().describe("Name of the UI element to click"),
|
|
152
|
+
}, async ({ app, name }) => {
|
|
153
|
+
try {
|
|
154
|
+
const { text, screenshot } = await clickElement(app, name);
|
|
155
|
+
return {
|
|
156
|
+
content: [
|
|
157
|
+
{ type: "text", text },
|
|
158
|
+
{ type: "image", data: screenshot.base64, mimeType: screenshot.mimeType },
|
|
159
|
+
],
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
return { isError: true, content: [{ type: "text", text: String(err) }] };
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
// ── Utilities ───────────────────────────────────────────────────
|
|
167
|
+
server.tool("open_url", "Open a URL in Safari or Chrome.", {
|
|
168
|
+
url: z.string().describe("URL to open"),
|
|
169
|
+
browser: z.string().optional().describe("'safari' or 'chrome' (defaults to Safari)"),
|
|
170
|
+
}, async ({ url, browser }) => {
|
|
171
|
+
try {
|
|
172
|
+
const result = await openUrl(url, browser);
|
|
173
|
+
return { content: [{ type: "text", text: result }] };
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
return { isError: true, content: [{ type: "text", text: String(err) }] };
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
server.tool("get_clipboard", "Read the current text contents of the macOS clipboard.", async () => {
|
|
180
|
+
try {
|
|
181
|
+
const text = await getClipboard();
|
|
182
|
+
return { content: [{ type: "text", text }] };
|
|
183
|
+
}
|
|
184
|
+
catch (err) {
|
|
185
|
+
return { isError: true, content: [{ type: "text", text: String(err) }] };
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
server.tool("set_clipboard", "Write text to the macOS clipboard.", { text: z.string().describe("Text to copy to clipboard") }, async ({ text }) => {
|
|
189
|
+
try {
|
|
190
|
+
const result = await setClipboard(text);
|
|
191
|
+
return { content: [{ type: "text", text: result }] };
|
|
192
|
+
}
|
|
193
|
+
catch (err) {
|
|
194
|
+
return { isError: true, content: [{ type: "text", text: String(err) }] };
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
// ── Start server ────────────────────────────────────────────────
|
|
198
|
+
async function main() {
|
|
199
|
+
await ensurePythonVenv();
|
|
200
|
+
const transport = new StdioServerTransport();
|
|
201
|
+
await server.connect(transport);
|
|
202
|
+
}
|
|
203
|
+
main().catch((err) => {
|
|
204
|
+
console.error("Fatal error:", err);
|
|
205
|
+
process.exit(1);
|
|
206
|
+
});
|
|
207
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { runAppleScript, escapeForAppleScript } from "../utils/applescript.js";
|
|
4
|
+
const execFileAsync = promisify(execFile);
|
|
5
|
+
async function openApp(name) {
|
|
6
|
+
await execFileAsync("open", ["-a", name]);
|
|
7
|
+
}
|
|
8
|
+
export async function launchApp(name) {
|
|
9
|
+
await openApp(name);
|
|
10
|
+
return `Launched "${name}".`;
|
|
11
|
+
}
|
|
12
|
+
export async function quitApp(name) {
|
|
13
|
+
const safe = escapeForAppleScript(name);
|
|
14
|
+
await runAppleScript(`tell application "${safe}" to quit`);
|
|
15
|
+
return `Quit "${name}".`;
|
|
16
|
+
}
|
|
17
|
+
export async function focusApp(name) {
|
|
18
|
+
await openApp(name);
|
|
19
|
+
return `Focused "${name}".`;
|
|
20
|
+
}
|
|
21
|
+
export async function listRunningApps() {
|
|
22
|
+
const raw = await runAppleScript(`tell application "System Events" to get name of every application process whose background only is false`);
|
|
23
|
+
// AppleScript returns comma-separated list
|
|
24
|
+
const apps = raw.split(", ").map((s) => s.trim()).filter(Boolean);
|
|
25
|
+
return JSON.stringify(apps, null, 2);
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=apps.js.map
|