mcp-macos 2.0.0
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 +360 -0
- package/bin/dev.cjs +10 -0
- package/bin/run.cjs +15 -0
- package/dist/config/index.d.ts +36 -0
- package/dist/config/index.js +127 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/schema.d.ts +161 -0
- package/dist/config/schema.js +45 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +75 -0
- package/dist/index.js.map +1 -0
- package/dist/server/handlers.d.ts +10 -0
- package/dist/server/handlers.js +43 -0
- package/dist/server/handlers.js.map +1 -0
- package/dist/server/promptAbstractions.d.ts +74 -0
- package/dist/server/promptAbstractions.js +150 -0
- package/dist/server/promptAbstractions.js.map +1 -0
- package/dist/server/prompts.d.ts +8 -0
- package/dist/server/prompts.js +480 -0
- package/dist/server/prompts.js.map +1 -0
- package/dist/server/server.d.ts +29 -0
- package/dist/server/server.js +52 -0
- package/dist/server/server.js.map +1 -0
- package/dist/server/transports/http/auth.d.ts +34 -0
- package/dist/server/transports/http/auth.js +148 -0
- package/dist/server/transports/http/auth.js.map +1 -0
- package/dist/server/transports/http/health.d.ts +35 -0
- package/dist/server/transports/http/health.js +93 -0
- package/dist/server/transports/http/health.js.map +1 -0
- package/dist/server/transports/http/index.d.ts +43 -0
- package/dist/server/transports/http/index.js +141 -0
- package/dist/server/transports/http/index.js.map +1 -0
- package/dist/server/transports/http/middleware.d.ts +53 -0
- package/dist/server/transports/http/middleware.js +133 -0
- package/dist/server/transports/http/middleware.js.map +1 -0
- package/dist/tools/definitions.d.ts +10 -0
- package/dist/tools/definitions.js +633 -0
- package/dist/tools/definitions.js.map +1 -0
- package/dist/tools/handlers/calendarHandlers.d.ts +11 -0
- package/dist/tools/handlers/calendarHandlers.js +123 -0
- package/dist/tools/handlers/calendarHandlers.js.map +1 -0
- package/dist/tools/handlers/contactsHandlers.d.ts +17 -0
- package/dist/tools/handlers/contactsHandlers.js +397 -0
- package/dist/tools/handlers/contactsHandlers.js.map +1 -0
- package/dist/tools/handlers/index.d.ts +11 -0
- package/dist/tools/handlers/index.js +12 -0
- package/dist/tools/handlers/index.js.map +1 -0
- package/dist/tools/handlers/listHandlers.d.ts +10 -0
- package/dist/tools/handlers/listHandlers.js +40 -0
- package/dist/tools/handlers/listHandlers.js.map +1 -0
- package/dist/tools/handlers/mailHandlers.d.ts +11 -0
- package/dist/tools/handlers/mailHandlers.js +301 -0
- package/dist/tools/handlers/mailHandlers.js.map +1 -0
- package/dist/tools/handlers/messagesHandlers.d.ts +17 -0
- package/dist/tools/handlers/messagesHandlers.js +350 -0
- package/dist/tools/handlers/messagesHandlers.js.map +1 -0
- package/dist/tools/handlers/notesHandlers.d.ts +12 -0
- package/dist/tools/handlers/notesHandlers.js +305 -0
- package/dist/tools/handlers/notesHandlers.js.map +1 -0
- package/dist/tools/handlers/reminderHandlers.d.ts +10 -0
- package/dist/tools/handlers/reminderHandlers.js +96 -0
- package/dist/tools/handlers/reminderHandlers.js.map +1 -0
- package/dist/tools/handlers/shared.d.ts +51 -0
- package/dist/tools/handlers/shared.js +107 -0
- package/dist/tools/handlers/shared.js.map +1 -0
- package/dist/tools/index.d.ts +10 -0
- package/dist/tools/index.js +125 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/types/index.d.ts +265 -0
- package/dist/types/index.js +67 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/prompts.d.ts +84 -0
- package/dist/types/prompts.js +6 -0
- package/dist/types/prompts.js.map +1 -0
- package/dist/types/repository.d.ts +102 -0
- package/dist/types/repository.js +6 -0
- package/dist/types/repository.js.map +1 -0
- package/dist/utils/binaryValidator.d.ts +52 -0
- package/dist/utils/binaryValidator.js +152 -0
- package/dist/utils/binaryValidator.js.map +1 -0
- package/dist/utils/calendarRepository.d.ts +25 -0
- package/dist/utils/calendarRepository.js +100 -0
- package/dist/utils/calendarRepository.js.map +1 -0
- package/dist/utils/cliExecutor.d.ts +28 -0
- package/dist/utils/cliExecutor.js +196 -0
- package/dist/utils/cliExecutor.js.map +1 -0
- package/dist/utils/constants.d.ts +96 -0
- package/dist/utils/constants.js +97 -0
- package/dist/utils/constants.js.map +1 -0
- package/dist/utils/contactResolver.d.ts +142 -0
- package/dist/utils/contactResolver.js +386 -0
- package/dist/utils/contactResolver.js.map +1 -0
- package/dist/utils/dateFiltering.d.ts +22 -0
- package/dist/utils/dateFiltering.js +72 -0
- package/dist/utils/dateFiltering.js.map +1 -0
- package/dist/utils/dateUtils.d.ts +20 -0
- package/dist/utils/dateUtils.js +36 -0
- package/dist/utils/dateUtils.js.map +1 -0
- package/dist/utils/errorHandling.d.ts +30 -0
- package/dist/utils/errorHandling.js +101 -0
- package/dist/utils/errorHandling.js.map +1 -0
- package/dist/utils/helpers.d.ts +35 -0
- package/dist/utils/helpers.js +59 -0
- package/dist/utils/helpers.js.map +1 -0
- package/dist/utils/jxaExecutor.d.ts +47 -0
- package/dist/utils/jxaExecutor.js +194 -0
- package/dist/utils/jxaExecutor.js.map +1 -0
- package/dist/utils/logging.d.ts +31 -0
- package/dist/utils/logging.js +98 -0
- package/dist/utils/logging.js.map +1 -0
- package/dist/utils/permissionPrompt.d.ts +16 -0
- package/dist/utils/permissionPrompt.js +42 -0
- package/dist/utils/permissionPrompt.js.map +1 -0
- package/dist/utils/preflight.d.ts +30 -0
- package/dist/utils/preflight.js +196 -0
- package/dist/utils/preflight.js.map +1 -0
- package/dist/utils/projectUtils.d.ts +11 -0
- package/dist/utils/projectUtils.js +76 -0
- package/dist/utils/projectUtils.js.map +1 -0
- package/dist/utils/reminderDateParser.d.ts +8 -0
- package/dist/utils/reminderDateParser.js +77 -0
- package/dist/utils/reminderDateParser.js.map +1 -0
- package/dist/utils/reminderRepository.d.ts +23 -0
- package/dist/utils/reminderRepository.js +91 -0
- package/dist/utils/reminderRepository.js.map +1 -0
- package/dist/utils/sqliteContactReader.d.ts +51 -0
- package/dist/utils/sqliteContactReader.js +216 -0
- package/dist/utils/sqliteContactReader.js.map +1 -0
- package/dist/utils/sqliteMailReader.d.ts +97 -0
- package/dist/utils/sqliteMailReader.js +310 -0
- package/dist/utils/sqliteMailReader.js.map +1 -0
- package/dist/utils/sqliteMessageReader.d.ts +71 -0
- package/dist/utils/sqliteMessageReader.js +400 -0
- package/dist/utils/sqliteMessageReader.js.map +1 -0
- package/dist/utils/timeHelpers.d.ts +40 -0
- package/dist/utils/timeHelpers.js +136 -0
- package/dist/utils/timeHelpers.js.map +1 -0
- package/dist/utils/timezone.d.ts +24 -0
- package/dist/utils/timezone.js +39 -0
- package/dist/utils/timezone.js.map +1 -0
- package/dist/validation/schemas.d.ts +610 -0
- package/dist/validation/schemas.js +354 -0
- package/dist/validation/schemas.js.map +1 -0
- package/package.json +97 -0
- package/scripts/build-swift.mjs +86 -0
- package/src/swift/EventKitCLI.swift +778 -0
- package/src/swift/Info.plist +38 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Frad LEE, 2026 Kyle Jensen
|
|
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,360 @@
|
|
|
1
|
+
# macos-mcp 
|
|
2
|
+
|
|
3
|
+
> Based on [FradSer/mcp-server-apple-events](https://github.com/FradSer/mcp-server-apple-events)
|
|
4
|
+
|
|
5
|
+
A Model Context Protocol (MCP) server that gives Claude genuine access to your macOS personal data — **Reminders**, **Calendar**, **Notes**, **Mail**, **Messages**, and **Contacts**. Six apps, working together: ask Claude about tomorrow's meeting and it pulls the calendar event, finds related emails, and creates a prep note — from your phone, your desktop, or the web.
|
|
6
|
+
|
|
7
|
+
Use with Claude Desktop locally, or Claude iOS/web remotely via Cloudflare Tunnel.
|
|
8
|
+
|
|
9
|
+
## Quick Start
|
|
10
|
+
|
|
11
|
+
### Install from npm
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install -g mcp-macos
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### Or build from source
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
git clone https://github.com/krmj22/macos-mcp.git
|
|
21
|
+
cd macos-mcp
|
|
22
|
+
pnpm install
|
|
23
|
+
pnpm build
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Verify setup
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
node dist/index.js --check # or: macos-mcp --check (if installed globally)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The preflight check validates macOS version, Node.js, EventKit binary, Full Disk Access, and JXA automation permissions.
|
|
33
|
+
|
|
34
|
+
**Next step:** [Using Claude Desktop?](#local-setup-stdio) | [Using Claude iOS/web remotely?](#remote-setup-claude-iosweb)
|
|
35
|
+
|
|
36
|
+
## Available MCP Tools
|
|
37
|
+
|
|
38
|
+
| Tool | App | Bridge | Actions |
|
|
39
|
+
|------|-----|--------|---------|
|
|
40
|
+
| `reminders_tasks` | Reminders | EventKit | read, create, update, delete |
|
|
41
|
+
| `reminders_lists` | Reminders | EventKit | read, create, update, delete |
|
|
42
|
+
| `calendar_events` | Calendar | EventKit | read, create, update, delete |
|
|
43
|
+
| `calendar_calendars` | Calendar | EventKit | read |
|
|
44
|
+
| `notes_items` | Notes | JXA | read, create, update, delete |
|
|
45
|
+
| `notes_folders` | Notes | JXA | read, create |
|
|
46
|
+
| `mail_messages` | Mail | SQLite + JXA | read, create, update, delete |
|
|
47
|
+
| `messages_chat` | Messages | SQLite + JXA | read, create |
|
|
48
|
+
| `contacts_people` | Contacts | JXA | read, search, create, update, delete |
|
|
49
|
+
|
|
50
|
+
All tools support both underscore (`reminders_tasks`) and dot (`reminders.tasks`) notation.
|
|
51
|
+
|
|
52
|
+
## Local Setup (stdio)
|
|
53
|
+
|
|
54
|
+
### Prerequisites
|
|
55
|
+
|
|
56
|
+
- **Node.js 18 or later**
|
|
57
|
+
- **macOS** (required for EventKit and JXA)
|
|
58
|
+
- **Xcode Command Line Tools** (required for compiling Swift code)
|
|
59
|
+
- **pnpm** (recommended for package management)
|
|
60
|
+
|
|
61
|
+
### Configure Your Client
|
|
62
|
+
|
|
63
|
+
#### Claude Desktop
|
|
64
|
+
|
|
65
|
+
Add to your `claude_desktop_config.json`:
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"mcpServers": {
|
|
70
|
+
"macos-mcp": {
|
|
71
|
+
"command": "npx",
|
|
72
|
+
"args": ["mcp-macos"]
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
#### Cursor
|
|
79
|
+
|
|
80
|
+
1. Open Cursor Settings > MCP > Add new global MCP server
|
|
81
|
+
2. Configure:
|
|
82
|
+
```json
|
|
83
|
+
{
|
|
84
|
+
"mcpServers": {
|
|
85
|
+
"macos-mcp": {
|
|
86
|
+
"command": "npx",
|
|
87
|
+
"args": ["mcp-macos"]
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
#### Claude Code
|
|
94
|
+
|
|
95
|
+
Add a `.mcp.json` to your project root:
|
|
96
|
+
|
|
97
|
+
```json
|
|
98
|
+
{
|
|
99
|
+
"mcpServers": {
|
|
100
|
+
"macos-mcp": {
|
|
101
|
+
"command": "npx",
|
|
102
|
+
"args": ["mcp-macos"]
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Permissions
|
|
109
|
+
|
|
110
|
+
On first use, macOS will prompt you to allow access for each app. Click **Allow** when prompted.
|
|
111
|
+
|
|
112
|
+
- **Reminders & Calendar** — EventKit permission dialogs appear automatically.
|
|
113
|
+
- **Notes, Mail, Contacts** — Automation permission dialogs appear when `osascript` first controls each app. Grant access via **System Settings > Privacy & Security > Automation**.
|
|
114
|
+
- **Messages & Mail** — Require **Full Disk Access** for your terminal app (Terminal, iTerm2, etc.) since both read SQLite databases directly.
|
|
115
|
+
|
|
116
|
+
Run `node dist/index.js --check` to verify all permissions are granted. See [Troubleshooting](#troubleshooting) if anything fails.
|
|
117
|
+
|
|
118
|
+
## Remote Setup (Claude iOS/web)
|
|
119
|
+
|
|
120
|
+
Use this path to access your Mac's apps from Claude iOS or Claude web via a secure Cloudflare Tunnel.
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
|
124
|
+
│ Claude iOS │──────│ Cloudflare │──────│ Your Mac │
|
|
125
|
+
│ or Web │ HTTPS│ Edge + Access │tunnel│ macos-mcp │
|
|
126
|
+
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### What you'll set up
|
|
130
|
+
|
|
131
|
+
1. **Cloudflare Tunnel** — Secure outbound connection from your Mac to Cloudflare's edge
|
|
132
|
+
2. **Cloudflare Access** — Email OTP authentication so only you can connect
|
|
133
|
+
3. **LaunchAgents** — Auto-start on boot for both the server and tunnel
|
|
134
|
+
4. **Permissions** — Automation + Full Disk Access granted to the node binary
|
|
135
|
+
|
|
136
|
+
### Prerequisites
|
|
137
|
+
|
|
138
|
+
- **Cloudflare account** (free tier works)
|
|
139
|
+
- **Custom domain** in Cloudflare (or use `.cfargotunnel.com` subdomain)
|
|
140
|
+
- **Always-on Mac** (Mac Mini/Studio recommended)
|
|
141
|
+
- **macos-mcp built and working locally** (`pnpm build && node dist/index.js --check`)
|
|
142
|
+
|
|
143
|
+
### Full Setup Guide
|
|
144
|
+
|
|
145
|
+
Follow the step-by-step instructions in **[`docs/CLOUDFLARE_SETUP.md`](docs/CLOUDFLARE_SETUP.md)** — covers tunnel creation, Cloudflare Access configuration, LaunchAgent setup, permission granting, and registering in Claude iOS.
|
|
146
|
+
|
|
147
|
+
## Usage Examples
|
|
148
|
+
|
|
149
|
+
### Reminders
|
|
150
|
+
```
|
|
151
|
+
Create a reminder to "Buy groceries" for tomorrow at 5 PM.
|
|
152
|
+
Show all reminders in my "Work" list.
|
|
153
|
+
Organize my reminders by priority.
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Calendar
|
|
157
|
+
```
|
|
158
|
+
Create a meeting "Team Standup" tomorrow from 10 AM to 10:30 AM.
|
|
159
|
+
Show my calendar events for this week.
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Notes
|
|
163
|
+
```
|
|
164
|
+
Create a note titled "Meeting Notes" in the Work folder.
|
|
165
|
+
Search my notes for "project plan".
|
|
166
|
+
List all note folders.
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Mail
|
|
170
|
+
```
|
|
171
|
+
Show my inbox.
|
|
172
|
+
Read the email from John about the project.
|
|
173
|
+
Draft an email to alice@example.com about the meeting.
|
|
174
|
+
Reply to the last email from Bob.
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Messages
|
|
178
|
+
```
|
|
179
|
+
Show my recent iMessage chats.
|
|
180
|
+
Read messages from the chat with John.
|
|
181
|
+
Send "On my way!" to the group chat.
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Troubleshooting
|
|
185
|
+
|
|
186
|
+
### Permission Quick Reference
|
|
187
|
+
|
|
188
|
+
| App | Permission | System Settings Path |
|
|
189
|
+
|-----|------------|---------------------|
|
|
190
|
+
| Reminders | Full Access | Privacy & Security > Reminders |
|
|
191
|
+
| Calendar | Full Access | Privacy & Security > Calendars |
|
|
192
|
+
| Notes | Automation | Privacy & Security > Automation > Notes |
|
|
193
|
+
| Mail | Automation + Full Disk Access | Both locations |
|
|
194
|
+
| Messages | Automation + Full Disk Access | Both locations |
|
|
195
|
+
| Contacts | Automation | Privacy & Security > Automation > Contacts |
|
|
196
|
+
|
|
197
|
+
### Quick-Fix Commands
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
# Open specific settings panes
|
|
201
|
+
open "x-apple.systempreferences:com.apple.preference.security?Privacy_Reminders"
|
|
202
|
+
open "x-apple.systempreferences:com.apple.preference.security?Privacy_Calendars"
|
|
203
|
+
open "x-apple.systempreferences:com.apple.preference.security?Privacy_Automation"
|
|
204
|
+
open "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles"
|
|
205
|
+
|
|
206
|
+
# Run preflight check
|
|
207
|
+
node dist/index.js --check
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### EventKit (Reminders & Calendar)
|
|
211
|
+
|
|
212
|
+
Apple separates Reminders and Calendar permissions into *write-only* and *full-access* scopes. The Swift bridge declares the following privacy keys:
|
|
213
|
+
|
|
214
|
+
- `NSRemindersUsageDescription` / `NSRemindersFullAccessUsageDescription` / `NSRemindersWriteOnlyAccessUsageDescription`
|
|
215
|
+
- `NSCalendarsUsageDescription` / `NSCalendarsFullAccessUsageDescription` / `NSCalendarsWriteOnlyAccessUsageDescription`
|
|
216
|
+
|
|
217
|
+
If a permission failure occurs, the Node.js layer automatically runs a minimal AppleScript to surface the dialog and retries.
|
|
218
|
+
|
|
219
|
+
### JXA Automation (Notes, Mail, Contacts)
|
|
220
|
+
|
|
221
|
+
JXA-based tools require macOS Automation permissions. On first use, macOS will prompt you to allow `osascript` to control each app.
|
|
222
|
+
|
|
223
|
+
> **Headless / LaunchAgent:** Automation permission dialogs **cannot appear** through a LaunchAgent, SSH session, or any non-GUI context. You must grant them once from a local graphical Terminal session (physical access or Screen Sharing). See [`docs/CLOUDFLARE_SETUP.md`](docs/CLOUDFLARE_SETUP.md) Step 10 for the full procedure. Once granted, permissions persist across reboots.
|
|
224
|
+
|
|
225
|
+
**Verify all Automation permissions:**
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
/usr/bin/osascript -l JavaScript -e 'Application("Contacts").people().length'
|
|
229
|
+
/usr/bin/osascript -l JavaScript -e 'Application("Calendar").calendars().length'
|
|
230
|
+
/usr/bin/osascript -l JavaScript -e 'Application("Reminders").defaultList().name()'
|
|
231
|
+
/usr/bin/osascript -l JavaScript -e 'Application("Mail").inbox().messages().length'
|
|
232
|
+
/usr/bin/osascript -l JavaScript -e 'Application("Notes").notes().length'
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Each command should return a value without errors or hanging. A hang means the permission dialog is trying (and failing) to appear.
|
|
236
|
+
|
|
237
|
+
### Full Disk Access (Messages & Mail)
|
|
238
|
+
|
|
239
|
+
The Messages and Mail tools read SQLite databases directly (`~/Library/Messages/chat.db` and `~/Library/Mail/V10/MailData/Envelope Index`). These databases are protected by **Full Disk Access (FDA)**.
|
|
240
|
+
|
|
241
|
+
- **stdio transport**: Grant FDA to your terminal app (Terminal, iTerm2, etc.)
|
|
242
|
+
- **HTTP transport / LaunchAgent**: Grant FDA to the **actual node binary**, not a version manager shim
|
|
243
|
+
|
|
244
|
+
To find and grant access to the correct binary:
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
# Find the actual node binary path
|
|
248
|
+
node -e "console.log(process.execPath)"
|
|
249
|
+
|
|
250
|
+
# Reveal it in Finder (drag-and-drop into FDA settings)
|
|
251
|
+
open -R "$(node -e "console.log(process.execPath)")"
|
|
252
|
+
|
|
253
|
+
# Open Full Disk Access settings
|
|
254
|
+
open "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles"
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
> **Note:** Version managers (Volta, nvm, fnm) use shims that point to a launcher, not the real binary. The System Settings file picker may not show binaries in hidden directories — use the drag-and-drop method above instead. See [`docs/CLOUDFLARE_SETUP.md`](docs/CLOUDFLARE_SETUP.md) Step 11 for detailed instructions and troubleshooting.
|
|
258
|
+
|
|
259
|
+
### Version Manager Shim Resolution
|
|
260
|
+
|
|
261
|
+
If you use **Volta**, **nvm**, or **fnm**, the `node` command is a shim/launcher. System Settings needs the real binary:
|
|
262
|
+
|
|
263
|
+
| Manager | Find real binary |
|
|
264
|
+
|---------|-----------------|
|
|
265
|
+
| Volta | `volta which node` or `node -e "console.log(process.execPath)"` |
|
|
266
|
+
| nvm | `nvm which current` |
|
|
267
|
+
| fnm | `fnm exec -- node -e "console.log(process.execPath)"` |
|
|
268
|
+
|
|
269
|
+
The System Settings file picker may not show binaries in hidden directories — use the `open -R` command above to reveal the binary in Finder, then drag-and-drop it into the FDA list.
|
|
270
|
+
|
|
271
|
+
### Gmail Labels / Missing Inbox Messages
|
|
272
|
+
|
|
273
|
+
Gmail stores all messages in `[Gmail]/All Mail` and uses **labels** for folder membership. The server checks both the direct mailbox and the labels join table for inbox queries. If you're not seeing Gmail inbox messages, verify the Mail app has fully synced your account.
|
|
274
|
+
|
|
275
|
+
### Server Restart (LaunchAgent)
|
|
276
|
+
|
|
277
|
+
If running as a LaunchAgent with Cloudflare Tunnel, restart **both** services:
|
|
278
|
+
|
|
279
|
+
```bash
|
|
280
|
+
launchctl kickstart -k gui/$(id -u)/com.macos-mcp.server
|
|
281
|
+
launchctl kickstart -k gui/$(id -u)/com.cloudflare.macos-mcp-tunnel
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
## Development
|
|
285
|
+
|
|
286
|
+
```bash
|
|
287
|
+
pnpm install # Install dependencies
|
|
288
|
+
pnpm build # Build TypeScript + Swift binary
|
|
289
|
+
pnpm test # Run full test suite
|
|
290
|
+
pnpm lint # Lint and format with Biome + TypeScript check
|
|
291
|
+
pnpm dev # Run from source via tsx (stdio only, no build needed)
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
> **Note:** The production entry point (`bin/run.cjs`) requires `pnpm build` first. Use `pnpm dev` for quick local development with stdio transport.
|
|
295
|
+
|
|
296
|
+
### End-to-End Testing
|
|
297
|
+
|
|
298
|
+
For HTTP transport testing, an E2E script is available:
|
|
299
|
+
|
|
300
|
+
```bash
|
|
301
|
+
./scripts/test-e2e.sh
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
This script:
|
|
305
|
+
- Starts the server in HTTP mode
|
|
306
|
+
- Tests health endpoints
|
|
307
|
+
- Verifies CORS headers and OPTIONS preflight
|
|
308
|
+
- Tests MCP endpoint availability
|
|
309
|
+
- Tests rate limit headers
|
|
310
|
+
- Verifies graceful shutdown
|
|
311
|
+
|
|
312
|
+
Requirements: `jq` (install with `brew install jq`)
|
|
313
|
+
|
|
314
|
+
### Architecture
|
|
315
|
+
|
|
316
|
+
The server uses two bridging strategies to communicate with Apple apps:
|
|
317
|
+
|
|
318
|
+
- **EventKit (Swift binary)** — Reminders and Calendar. A compiled Swift CLI binary performs EventKit operations and returns JSON.
|
|
319
|
+
- **JXA (JavaScript for Automation)** — Notes, Mail, Contacts. Scripts run via `osascript -l JavaScript` with template-based parameter interpolation.
|
|
320
|
+
- **SQLite** — Messages reads (`~/Library/Messages/chat.db`) and Mail reads (`~/Library/Mail/V10/MailData/Envelope Index`). JXA message reading is broken on macOS Sonoma+; JXA mail reading is too slow for real inboxes. Writes still use JXA.
|
|
321
|
+
|
|
322
|
+
### HTTP Transport Configuration
|
|
323
|
+
|
|
324
|
+
The server supports HTTP transport for remote access. Set via environment variables or `macos-mcp.config.json`:
|
|
325
|
+
|
|
326
|
+
| Variable | Default | Description |
|
|
327
|
+
|----------|---------|-------------|
|
|
328
|
+
| `MCP_TRANSPORT` | `stdio` | Transport mode: `stdio`, `http`, or `both` |
|
|
329
|
+
| `MCP_HTTP_ENABLED` | `false` | Enable HTTP transport |
|
|
330
|
+
| `MCP_HTTP_PORT` | `3847` | HTTP server port |
|
|
331
|
+
|
|
332
|
+
Key design decisions:
|
|
333
|
+
- **Stateless mode** — required for multi-client support (Claude.ai serves multiple users)
|
|
334
|
+
- **Root endpoint** — MCP handler at `/` (Claude expects this, not `/mcp`)
|
|
335
|
+
- **JSON fallback** — `enableJsonResponse: true` for clients without SSE support
|
|
336
|
+
|
|
337
|
+
### Structured Prompt Library
|
|
338
|
+
|
|
339
|
+
The server ships with prompt templates exposed via MCP `ListPrompts` and `GetPrompt` endpoints:
|
|
340
|
+
|
|
341
|
+
- **daily-task-organizer** — optional `today_focus` input produces a same-day execution blueprint
|
|
342
|
+
- **smart-reminder-creator** — optional `task_idea` generates an optimally scheduled reminder
|
|
343
|
+
- **reminder-review-assistant** — optional `review_focus` to audit and optimize existing reminders
|
|
344
|
+
- **weekly-planning-workflow** — optional `user_ideas` guides a Monday-through-Sunday reset
|
|
345
|
+
|
|
346
|
+
Run `pnpm test -- src/server/prompts.test.ts` to validate prompt metadata and schema compatibility.
|
|
347
|
+
|
|
348
|
+
### Dependencies
|
|
349
|
+
|
|
350
|
+
**Runtime:** `@modelcontextprotocol/sdk`, `exit-on-epipe`, `tsx`, `zod`
|
|
351
|
+
|
|
352
|
+
**Dev:** `typescript`, `jest`, `ts-jest`, `babel-jest`, `@biomejs/biome`
|
|
353
|
+
|
|
354
|
+
## License
|
|
355
|
+
|
|
356
|
+
MIT
|
|
357
|
+
|
|
358
|
+
## Contributing
|
|
359
|
+
|
|
360
|
+
Contributions welcome! Please read the contributing guidelines first.
|
package/bin/dev.cjs
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Development entry point — runs src/index.ts directly via tsx.
|
|
5
|
+
* Use for local stdio-only development; HTTP transport requires a compiled build.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { register } = require('tsx/cjs/api');
|
|
9
|
+
register();
|
|
10
|
+
require('../src/index.ts');
|
package/bin/run.cjs
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
|
|
6
|
+
const entryPoint = path.resolve(__dirname, '..', 'dist', 'index.js');
|
|
7
|
+
|
|
8
|
+
if (!fs.existsSync(entryPoint)) {
|
|
9
|
+
process.stderr.write(
|
|
10
|
+
'Error: dist/index.js not found. Run `pnpm build` before starting the server.\n',
|
|
11
|
+
);
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
import(entryPoint);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Configuration loading and management for macos-mcp server
|
|
3
|
+
* @module config
|
|
4
|
+
* @description Loads configuration from file and environment variables with auto-injection of package.json metadata
|
|
5
|
+
*/
|
|
6
|
+
import { type CloudflareAccessConfig, type FullServerConfig, type HttpConfig } from './schema.js';
|
|
7
|
+
/**
|
|
8
|
+
* Loads server configuration from file and environment variables
|
|
9
|
+
*
|
|
10
|
+
* Configuration is loaded in the following priority (highest to lowest):
|
|
11
|
+
* 1. Environment variables (MCP_TRANSPORT, MCP_HTTP_*, CF_ACCESS_*)
|
|
12
|
+
* 2. Configuration file (macos-mcp.config.json in project root)
|
|
13
|
+
* 3. Default values from schema
|
|
14
|
+
*
|
|
15
|
+
* Name and version are always auto-injected from package.json
|
|
16
|
+
*
|
|
17
|
+
* @returns Validated server configuration
|
|
18
|
+
* @throws Error if configuration is invalid
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* // Basic usage
|
|
22
|
+
* const config = loadConfig();
|
|
23
|
+
* console.log(config.transport); // 'stdio' (default)
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* // With environment variables
|
|
27
|
+
* // MCP_TRANSPORT=http
|
|
28
|
+
* // MCP_HTTP_ENABLED=true
|
|
29
|
+
* // MCP_HTTP_PORT=8080
|
|
30
|
+
* const config = loadConfig();
|
|
31
|
+
* console.log(config.transport); // 'http'
|
|
32
|
+
* console.log(config.http?.port); // 8080
|
|
33
|
+
*/
|
|
34
|
+
export declare function loadConfig(): FullServerConfig;
|
|
35
|
+
export type { CloudflareAccessConfig, FullServerConfig, HttpConfig };
|
|
36
|
+
export { CloudflareAccessConfigSchema, HttpConfigSchema, ServerConfigSchema, } from './schema.js';
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Configuration loading and management for macos-mcp server
|
|
3
|
+
* @module config
|
|
4
|
+
* @description Loads configuration from file and environment variables with auto-injection of package.json metadata
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { findProjectRoot } from '../utils/projectUtils.js';
|
|
9
|
+
import { ServerConfigSchema, } from './schema.js';
|
|
10
|
+
/** Configuration file name */
|
|
11
|
+
const CONFIG_FILENAME = 'macos-mcp.config.json';
|
|
12
|
+
/**
|
|
13
|
+
* Loads server configuration from file and environment variables
|
|
14
|
+
*
|
|
15
|
+
* Configuration is loaded in the following priority (highest to lowest):
|
|
16
|
+
* 1. Environment variables (MCP_TRANSPORT, MCP_HTTP_*, CF_ACCESS_*)
|
|
17
|
+
* 2. Configuration file (macos-mcp.config.json in project root)
|
|
18
|
+
* 3. Default values from schema
|
|
19
|
+
*
|
|
20
|
+
* Name and version are always auto-injected from package.json
|
|
21
|
+
*
|
|
22
|
+
* @returns Validated server configuration
|
|
23
|
+
* @throws Error if configuration is invalid
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* // Basic usage
|
|
27
|
+
* const config = loadConfig();
|
|
28
|
+
* console.log(config.transport); // 'stdio' (default)
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* // With environment variables
|
|
32
|
+
* // MCP_TRANSPORT=http
|
|
33
|
+
* // MCP_HTTP_ENABLED=true
|
|
34
|
+
* // MCP_HTTP_PORT=8080
|
|
35
|
+
* const config = loadConfig();
|
|
36
|
+
* console.log(config.transport); // 'http'
|
|
37
|
+
* console.log(config.http?.port); // 8080
|
|
38
|
+
*/
|
|
39
|
+
export function loadConfig() {
|
|
40
|
+
const projectRoot = findProjectRoot();
|
|
41
|
+
const configPath = join(projectRoot, CONFIG_FILENAME);
|
|
42
|
+
// Load file configuration if it exists
|
|
43
|
+
let fileConfig = {};
|
|
44
|
+
if (existsSync(configPath)) {
|
|
45
|
+
const fileContent = readFileSync(configPath, 'utf-8');
|
|
46
|
+
fileConfig = JSON.parse(fileContent);
|
|
47
|
+
}
|
|
48
|
+
// Build environment variable configuration
|
|
49
|
+
const envConfig = buildEnvConfig();
|
|
50
|
+
// Deep merge file config with env config (env takes precedence)
|
|
51
|
+
const merged = deepMerge(fileConfig, envConfig);
|
|
52
|
+
// Load package.json for name and version
|
|
53
|
+
const packageJsonPath = join(projectRoot, 'package.json');
|
|
54
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
|
55
|
+
// Parse and validate configuration
|
|
56
|
+
return ServerConfigSchema.parse({
|
|
57
|
+
name: packageJson.name,
|
|
58
|
+
version: packageJson.version,
|
|
59
|
+
...merged,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Builds configuration object from environment variables
|
|
64
|
+
* @returns Partial configuration from environment variables
|
|
65
|
+
*/
|
|
66
|
+
function buildEnvConfig() {
|
|
67
|
+
const config = {};
|
|
68
|
+
// Transport mode
|
|
69
|
+
if (process.env.MCP_TRANSPORT) {
|
|
70
|
+
config.transport = process.env.MCP_TRANSPORT;
|
|
71
|
+
}
|
|
72
|
+
// HTTP configuration (only if explicitly enabled)
|
|
73
|
+
if (process.env.MCP_HTTP_ENABLED === 'true') {
|
|
74
|
+
const httpConfig = {
|
|
75
|
+
enabled: true,
|
|
76
|
+
};
|
|
77
|
+
if (process.env.MCP_HTTP_HOST) {
|
|
78
|
+
httpConfig.host = process.env.MCP_HTTP_HOST;
|
|
79
|
+
}
|
|
80
|
+
if (process.env.MCP_HTTP_PORT) {
|
|
81
|
+
const port = Number.parseInt(process.env.MCP_HTTP_PORT, 10);
|
|
82
|
+
if (!Number.isNaN(port)) {
|
|
83
|
+
httpConfig.port = port;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Cloudflare Access configuration
|
|
87
|
+
if (process.env.CF_ACCESS_TEAM_DOMAIN && process.env.CF_ACCESS_POLICY_AUD) {
|
|
88
|
+
const cfConfig = {
|
|
89
|
+
teamDomain: process.env.CF_ACCESS_TEAM_DOMAIN,
|
|
90
|
+
policyAUD: process.env.CF_ACCESS_POLICY_AUD,
|
|
91
|
+
};
|
|
92
|
+
if (process.env.CF_ACCESS_ALLOWED_EMAILS) {
|
|
93
|
+
cfConfig.allowedEmails = process.env.CF_ACCESS_ALLOWED_EMAILS.split(',').map((e) => e.trim());
|
|
94
|
+
}
|
|
95
|
+
httpConfig.cloudflareAccess = cfConfig;
|
|
96
|
+
}
|
|
97
|
+
config.http = httpConfig;
|
|
98
|
+
}
|
|
99
|
+
return config;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Deep merges two objects, with source taking precedence
|
|
103
|
+
* @param target - Base object
|
|
104
|
+
* @param source - Object to merge in (takes precedence)
|
|
105
|
+
* @returns Merged object
|
|
106
|
+
*/
|
|
107
|
+
function deepMerge(target, source) {
|
|
108
|
+
const result = { ...target };
|
|
109
|
+
for (const key of Object.keys(source)) {
|
|
110
|
+
const sourceValue = source[key];
|
|
111
|
+
const targetValue = result[key];
|
|
112
|
+
if (sourceValue !== null &&
|
|
113
|
+
typeof sourceValue === 'object' &&
|
|
114
|
+
!Array.isArray(sourceValue) &&
|
|
115
|
+
targetValue !== null &&
|
|
116
|
+
typeof targetValue === 'object' &&
|
|
117
|
+
!Array.isArray(targetValue)) {
|
|
118
|
+
result[key] = deepMerge(targetValue, sourceValue);
|
|
119
|
+
}
|
|
120
|
+
else if (sourceValue !== undefined) {
|
|
121
|
+
result[key] = sourceValue;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return result;
|
|
125
|
+
}
|
|
126
|
+
export { CloudflareAccessConfigSchema, HttpConfigSchema, ServerConfigSchema, } from './schema.js';
|
|
127
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/config/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAC3D,OAAO,EAIL,kBAAkB,GACnB,MAAM,aAAa,CAAC;AAErB,8BAA8B;AAC9B,MAAM,eAAe,GAAG,uBAAuB,CAAC;AAEhD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,UAAU,UAAU;IACxB,MAAM,WAAW,GAAG,eAAe,EAAE,CAAC;IACtC,MAAM,UAAU,GAAG,IAAI,CAAC,WAAW,EAAE,eAAe,CAAC,CAAC;IAEtD,uCAAuC;IACvC,IAAI,UAAU,GAA4B,EAAE,CAAC;IAC7C,IAAI,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC3B,MAAM,WAAW,GAAG,YAAY,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;QACtD,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,WAAW,CAA4B,CAAC;IAClE,CAAC;IAED,2CAA2C;IAC3C,MAAM,SAAS,GAAG,cAAc,EAAE,CAAC;IAEnC,gEAAgE;IAChE,MAAM,MAAM,GAAG,SAAS,CAAC,UAAU,EAAE,SAAS,CAAC,CAAC;IAEhD,yCAAyC;IACzC,MAAM,eAAe,GAAG,IAAI,CAAC,WAAW,EAAE,cAAc,CAAC,CAAC;IAC1D,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,eAAe,EAAE,OAAO,CAAC,CAGpE,CAAC;IAEF,mCAAmC;IACnC,OAAO,kBAAkB,CAAC,KAAK,CAAC;QAC9B,IAAI,EAAE,WAAW,CAAC,IAAI;QACtB,OAAO,EAAE,WAAW,CAAC,OAAO;QAC5B,GAAG,MAAM;KACV,CAAC,CAAC;AACL,CAAC;AAED;;;GAGG;AACH,SAAS,cAAc;IACrB,MAAM,MAAM,GAA4B,EAAE,CAAC;IAE3C,iBAAiB;IACjB,IAAI,OAAO,CAAC,GAAG,CAAC,aAAa,EAAE,CAAC;QAC9B,MAAM,CAAC,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC;IAC/C,CAAC;IAED,kDAAkD;IAClD,IAAI,OAAO,CAAC,GAAG,CAAC,gBAAgB,KAAK,MAAM,EAAE,CAAC;QAC5C,MAAM,UAAU,GAA4B;YAC1C,OAAO,EAAE,IAAI;SACd,CAAC;QAEF,IAAI,OAAO,CAAC,GAAG,CAAC,aAAa,EAAE,CAAC;YAC9B,UAAU,CAAC,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC;QAC9C,CAAC;QAED,IAAI,OAAO,CAAC,GAAG,CAAC,aAAa,EAAE,CAAC;YAC9B,MAAM,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,aAAa,EAAE,EAAE,CAAC,CAAC;YAC5D,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;gBACxB,UAAU,CAAC,IAAI,GAAG,IAAI,CAAC;YACzB,CAAC;QACH,CAAC;QAED,kCAAkC;QAClC,IAAI,OAAO,CAAC,GAAG,CAAC,qBAAqB,IAAI,OAAO,CAAC,GAAG,CAAC,oBAAoB,EAAE,CAAC;YAC1E,MAAM,QAAQ,GAA4B;gBACxC,UAAU,EAAE,OAAO,CAAC,GAAG,CAAC,qBAAqB;gBAC7C,SAAS,EAAE,OAAO,CAAC,GAAG,CAAC,oBAAoB;aAC5C,CAAC;YAEF,IAAI,OAAO,CAAC,GAAG,CAAC,wBAAwB,EAAE,CAAC;gBACzC,QAAQ,CAAC,aAAa,GAAG,OAAO,CAAC,GAAG,CAAC,wBAAwB,CAAC,KAAK,CACjE,GAAG,CACJ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;YACzB,CAAC;YAED,UAAU,CAAC,gBAAgB,GAAG,QAAQ,CAAC;QACzC,CAAC;QAED,MAAM,CAAC,IAAI,GAAG,UAAU,CAAC;IAC3B,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;GAKG;AACH,SAAS,SAAS,CAChB,MAA+B,EAC/B,MAA+B;IAE/B,MAAM,MAAM,GAAG,EAAE,GAAG,MAAM,EAAE,CAAC;IAE7B,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;QACtC,MAAM,WAAW,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;QAChC,MAAM,WAAW,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;QAEhC,IACE,WAAW,KAAK,IAAI;YACpB,OAAO,WAAW,KAAK,QAAQ;YAC/B,CAAC,KAAK,CAAC,OAAO,CAAC,WAAW,CAAC;YAC3B,WAAW,KAAK,IAAI;YACpB,OAAO,WAAW,KAAK,QAAQ;YAC/B,CAAC,KAAK,CAAC,OAAO,CAAC,WAAW,CAAC,EAC3B,CAAC;YACD,MAAM,CAAC,GAAG,CAAC,GAAG,SAAS,CACrB,WAAsC,EACtC,WAAsC,CACvC,CAAC;QACJ,CAAC;aAAM,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;YACrC,MAAM,CAAC,GAAG,CAAC,GAAG,WAAW,CAAC;QAC5B,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAID,OAAO,EACL,4BAA4B,EAC5B,gBAAgB,EAChB,kBAAkB,GACnB,MAAM,aAAa,CAAC"}
|