mursa-mcp 0.4.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 +125 -0
- package/package.json +45 -0
- package/server.js +432 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Murali Gurajapu
|
|
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,125 @@
|
|
|
1
|
+
# Mursa MCP
|
|
2
|
+
|
|
3
|
+
Connect Claude Code, Claude Desktop, Cursor, or any MCP-compatible client to
|
|
4
|
+
your Mursa tasks, calendar, goals, notes, habits, projects, and Gmail.
|
|
5
|
+
|
|
6
|
+
📖 **Full guide with screenshots:** https://mursa.me/mcp
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 60-second install
|
|
11
|
+
|
|
12
|
+
### 1. Get an API key
|
|
13
|
+
|
|
14
|
+
1. Open [dashboard.mursa.me](https://dashboard.mursa.me) and sign in.
|
|
15
|
+
2. Go to **Settings → API keys**.
|
|
16
|
+
3. Click **New key** → pick a label, expiry, and scopes → **Create**.
|
|
17
|
+
4. Copy the key (starts with `mursa_mcp_…`). You'll only see it once.
|
|
18
|
+
|
|
19
|
+
### 2. Add to your client
|
|
20
|
+
|
|
21
|
+
#### Claude Code (`~/.claude.json`)
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"mcpServers": {
|
|
26
|
+
"mursa": {
|
|
27
|
+
"command": "npx",
|
|
28
|
+
"args": ["-y", "mursa-mcp"],
|
|
29
|
+
"env": { "MURSA_API_KEY": "mursa_mcp_…" }
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Restart Claude Code, run `/mcp` to confirm, call `whoami` to verify.
|
|
36
|
+
|
|
37
|
+
#### Claude Desktop
|
|
38
|
+
|
|
39
|
+
Edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS)
|
|
40
|
+
or `%APPDATA%/Claude/claude_desktop_config.json` (Windows) with the same block,
|
|
41
|
+
then restart the app.
|
|
42
|
+
|
|
43
|
+
#### Cursor
|
|
44
|
+
|
|
45
|
+
Settings → MCP → Add new MCP server, same JSON block.
|
|
46
|
+
|
|
47
|
+
### 3. That's it
|
|
48
|
+
|
|
49
|
+
Your agent now has access to whatever scopes your key has.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Tools (28)
|
|
54
|
+
|
|
55
|
+
| Group | Tools | Scope |
|
|
56
|
+
|---|---|---|
|
|
57
|
+
| Meta | `whoami` | (any) |
|
|
58
|
+
| Tasks (read) | `list_inbox`, `list_myday`, `list_schedule`, `search_tasks` | `tasks:read` |
|
|
59
|
+
| Tasks (write) | `create_task`, `update_task`, `complete_task`, `defer_task`, `schedule_task` | `tasks:write` |
|
|
60
|
+
| Calendar | `list_calendar`, `create_calendar_event` | `calendar:*` |
|
|
61
|
+
| Goals | `list_goals`, `create_goal`, `update_goal`, `delete_goal` | `goals:*` |
|
|
62
|
+
| Notes | `list_notes`, `search_notes`, `create_note`, `update_note` | `notes:*` |
|
|
63
|
+
| Habits | `list_habits` | `habits:read` |
|
|
64
|
+
| Projects | `list_projects` | `projects:read` |
|
|
65
|
+
| Email | `list_emails`, `get_email`, `get_attachment`, `search_emails` | `email:read` |
|
|
66
|
+
| Email | `send_email`, `reply_email` | `email:send` |
|
|
67
|
+
|
|
68
|
+
`*` as a scope grants everything.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Security
|
|
73
|
+
|
|
74
|
+
- API keys are sha256-hashed at rest — the raw value is never stored.
|
|
75
|
+
- Per-key scopes + expiry. Revoke any key instantly from the dashboard.
|
|
76
|
+
- Per-action rate limits (60/min reads, 30/min writes, **5/min email send**).
|
|
77
|
+
- Every call audit-logged for 90 days (action, status, latency, IP — no payload).
|
|
78
|
+
- Email attachments capped at 3 MB each / 10 MB total per email.
|
|
79
|
+
- All traffic goes through `mursa.me/api/mcp`; the Supabase function is gated
|
|
80
|
+
by a shared proxy secret so direct hits return 403.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Run from source (advanced)
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
git clone https://github.com/Murali1889/Prod-Mursa.git
|
|
88
|
+
cd Prod-Mursa/mcp-servers/mursa
|
|
89
|
+
npm install
|
|
90
|
+
echo 'MURSA_API_KEY=mursa_mcp_…' > .env
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Then point your client at the absolute path:
|
|
94
|
+
|
|
95
|
+
```json
|
|
96
|
+
{
|
|
97
|
+
"mcpServers": {
|
|
98
|
+
"mursa": {
|
|
99
|
+
"command": "node",
|
|
100
|
+
"args": ["/absolute/path/to/mcp-servers/mursa/server.js"]
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Override the endpoint for preview deployments or local dev:
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
MURSA_API_URL=https://mursa-preview.vercel.app/api/mcp
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
(Default is `https://mursa.me/api/mcp`.)
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Troubleshooting
|
|
117
|
+
|
|
118
|
+
| Symptom | Fix |
|
|
119
|
+
|---|---|
|
|
120
|
+
| `Invalid, expired, or revoked API key` | Mint a fresh key in the dashboard, swap `MURSA_API_KEY`, restart your client. |
|
|
121
|
+
| `This API key is missing the required scope: …` | Your key wasn't granted that scope at mint time. Revoke and mint a new one with the scope checked. |
|
|
122
|
+
| `Gmail is not connected. Connect Gmail in Mursa Settings first.` | Connect Gmail from the Mursa app first. |
|
|
123
|
+
| `Rate limit exceeded (5/min for send_email)` | Slow down — strict throttle on outbound mail. |
|
|
124
|
+
|
|
125
|
+
For server-side deployment (running your own Mursa instance), see `DEPLOY.md`.
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mursa-mcp",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Mursa MCP server — connect Claude, Cursor, and other MCP clients to your Mursa tasks, calendar, goals, notes, habits, projects, and Gmail.",
|
|
5
|
+
"main": "server.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"mursa-mcp": "server.js"
|
|
8
|
+
},
|
|
9
|
+
"type": "commonjs",
|
|
10
|
+
"files": [
|
|
11
|
+
"server.js",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"start": "node server.js"
|
|
17
|
+
},
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=18"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"mcp",
|
|
23
|
+
"modelcontextprotocol",
|
|
24
|
+
"mursa",
|
|
25
|
+
"claude",
|
|
26
|
+
"claude-code",
|
|
27
|
+
"cursor",
|
|
28
|
+
"tasks",
|
|
29
|
+
"productivity"
|
|
30
|
+
],
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"homepage": "https://mursa.me/mcp",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/Murali1889/MCP-mursa.git"
|
|
36
|
+
},
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/Murali1889/MCP-mursa/issues"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
42
|
+
"dotenv": "^16.4.5",
|
|
43
|
+
"zod": "^4.3.6"
|
|
44
|
+
}
|
|
45
|
+
}
|
package/server.js
ADDED
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Mursa MCP server (stdio).
|
|
4
|
+
*
|
|
5
|
+
* Architecture:
|
|
6
|
+
* Claude/Cursor/etc. ─stdio─> this Node process
|
|
7
|
+
* │
|
|
8
|
+
* │ fetch() with
|
|
9
|
+
* │ Authorization: Bearer <api key>
|
|
10
|
+
* â–¼
|
|
11
|
+
* Supabase Edge Function: mcp
|
|
12
|
+
* │ resolves key -> user_id + scopes
|
|
13
|
+
* â–¼
|
|
14
|
+
* Supabase Postgres
|
|
15
|
+
*
|
|
16
|
+
* The MCP server holds NO Supabase secrets. It only knows:
|
|
17
|
+
* - SUPABASE_URL (public)
|
|
18
|
+
* - MURSA_API_KEY (opaque per-user key with scopes + expiry)
|
|
19
|
+
*
|
|
20
|
+
* Every tool below just maps to one edge-function action.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
require("dotenv").config({ path: require("path").join(__dirname, ".env") });
|
|
24
|
+
|
|
25
|
+
const { McpServer } = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
26
|
+
const {
|
|
27
|
+
StdioServerTransport,
|
|
28
|
+
} = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
29
|
+
const { z } = require("zod");
|
|
30
|
+
|
|
31
|
+
// Default to the public Mursa proxy. Override only if you're self-hosting or
|
|
32
|
+
// pointing at a preview deployment.
|
|
33
|
+
const MURSA_API_URL = process.env.MURSA_API_URL || "https://mursa.me/api/mcp";
|
|
34
|
+
const MURSA_API_KEY = process.env.MURSA_API_KEY;
|
|
35
|
+
|
|
36
|
+
if (!MURSA_API_KEY) {
|
|
37
|
+
console.error("[mursa-mcp] MURSA_API_KEY is not set in .env");
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const ENDPOINT = MURSA_API_URL.replace(/\/$/, "");
|
|
42
|
+
|
|
43
|
+
// ───────────────────────────── call() ─────────────────────────────
|
|
44
|
+
|
|
45
|
+
async function call(action, args = {}) {
|
|
46
|
+
let res;
|
|
47
|
+
try {
|
|
48
|
+
res = await fetch(ENDPOINT, {
|
|
49
|
+
method: "POST",
|
|
50
|
+
headers: {
|
|
51
|
+
Authorization: `Bearer ${MURSA_API_KEY}`,
|
|
52
|
+
"Content-Type": "application/json",
|
|
53
|
+
},
|
|
54
|
+
body: JSON.stringify({ action, args }),
|
|
55
|
+
});
|
|
56
|
+
} catch (e) {
|
|
57
|
+
throw new Error(`Network error calling ${ENDPOINT}: ${e.message}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let body;
|
|
61
|
+
try {
|
|
62
|
+
body = await res.json();
|
|
63
|
+
} catch {
|
|
64
|
+
throw new Error(`Non-JSON response (HTTP ${res.status})`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!res.ok || body.error) {
|
|
68
|
+
throw new Error(body.error || `HTTP ${res.status}`);
|
|
69
|
+
}
|
|
70
|
+
return body.data;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function jsonContent(payload) {
|
|
74
|
+
return {
|
|
75
|
+
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function errorContent(err) {
|
|
80
|
+
return {
|
|
81
|
+
isError: true,
|
|
82
|
+
content: [{ type: "text", text: `Error: ${err.message || String(err)}` }],
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const server = new McpServer({ name: "mursa", version: "0.3.0" });
|
|
87
|
+
|
|
88
|
+
function tool(name, description, schema, action) {
|
|
89
|
+
server.tool(name, description, schema, async (args) => {
|
|
90
|
+
try {
|
|
91
|
+
const data = await call(action, args ?? {});
|
|
92
|
+
return jsonContent(data);
|
|
93
|
+
} catch (err) {
|
|
94
|
+
return errorContent(err);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ───────────────────────────── tools ─────────────────────────────
|
|
100
|
+
|
|
101
|
+
// Meta
|
|
102
|
+
tool("whoami", "Show which Mursa user this API key belongs to.", {}, "whoami");
|
|
103
|
+
|
|
104
|
+
// Tasks: read
|
|
105
|
+
tool(
|
|
106
|
+
"list_inbox",
|
|
107
|
+
"List Inbox tasks (unscheduled, personal, not completed by default).",
|
|
108
|
+
{
|
|
109
|
+
status: z.string().optional(),
|
|
110
|
+
limit: z.number().int().positive().max(200).optional(),
|
|
111
|
+
includeScheduled: z.boolean().optional(),
|
|
112
|
+
},
|
|
113
|
+
"list_inbox"
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
tool(
|
|
117
|
+
"list_myday",
|
|
118
|
+
"List tasks scheduled for a specific day (default today). Date: YYYY-MM-DD.",
|
|
119
|
+
{ date: z.string().optional() },
|
|
120
|
+
"list_myday"
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
tool(
|
|
124
|
+
"list_schedule",
|
|
125
|
+
"List all scheduled tasks between startDate and endDate inclusive. Dates: YYYY-MM-DD.",
|
|
126
|
+
{ startDate: z.string(), endDate: z.string() },
|
|
127
|
+
"list_schedule"
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
tool(
|
|
131
|
+
"search_tasks",
|
|
132
|
+
"Search task titles by ILIKE match. Returns most recently updated first.",
|
|
133
|
+
{ query: z.string().min(1), limit: z.number().int().positive().max(100).optional() },
|
|
134
|
+
"search_tasks"
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
// Calendar (= time-blocked scheduled tasks)
|
|
138
|
+
tool(
|
|
139
|
+
"list_calendar",
|
|
140
|
+
"List calendar events (scheduled tasks with start_time) between two dates. Dates: YYYY-MM-DD.",
|
|
141
|
+
{ startDate: z.string(), endDate: z.string() },
|
|
142
|
+
"list_calendar"
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
tool(
|
|
146
|
+
"create_calendar_event",
|
|
147
|
+
"Create a calendar event (a task with scheduled_date + start_time/end_time, task_type='meeting' by default).",
|
|
148
|
+
{
|
|
149
|
+
title: z.string().min(1),
|
|
150
|
+
scheduled_date: z.string(),
|
|
151
|
+
start_time: z.string().optional(),
|
|
152
|
+
end_time: z.string().optional(),
|
|
153
|
+
duration_minutes: z.number().int().positive().optional(),
|
|
154
|
+
description: z.string().optional(),
|
|
155
|
+
why: z.string().optional(),
|
|
156
|
+
task_type: z.enum(["deep", "shallow", "admin", "meeting"]).optional(),
|
|
157
|
+
priority: z.enum(["low", "medium", "high", "urgent"]).optional(),
|
|
158
|
+
},
|
|
159
|
+
"create_calendar_event"
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
// Tasks: write
|
|
163
|
+
tool(
|
|
164
|
+
"create_task",
|
|
165
|
+
"Create a task. With scheduled_date -> MyDay; without -> Inbox.",
|
|
166
|
+
{
|
|
167
|
+
title: z.string().min(1),
|
|
168
|
+
description: z.string().optional(),
|
|
169
|
+
priority: z.enum(["low", "medium", "high", "urgent"]).optional(),
|
|
170
|
+
due_date: z.string().optional(),
|
|
171
|
+
labels: z.array(z.string()).optional(),
|
|
172
|
+
category: z.string().optional(),
|
|
173
|
+
estimated_hours: z.number().nonnegative().optional(),
|
|
174
|
+
goal_id: z.string().optional(),
|
|
175
|
+
project_id: z.string().optional(),
|
|
176
|
+
scheduled_date: z.string().optional(),
|
|
177
|
+
start_time: z.string().optional(),
|
|
178
|
+
end_time: z.string().optional(),
|
|
179
|
+
duration_minutes: z.number().int().positive().optional(),
|
|
180
|
+
task_type: z.enum(["deep", "shallow", "admin", "meeting"]).optional(),
|
|
181
|
+
why: z.string().optional(),
|
|
182
|
+
},
|
|
183
|
+
"create_task"
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
tool(
|
|
187
|
+
"update_task",
|
|
188
|
+
"Patch any subset of editable fields on a task.",
|
|
189
|
+
{
|
|
190
|
+
task_id: z.string(),
|
|
191
|
+
title: z.string().optional(),
|
|
192
|
+
description: z.string().optional(),
|
|
193
|
+
status: z.string().optional(),
|
|
194
|
+
priority: z.string().optional(),
|
|
195
|
+
due_date: z.string().nullable().optional(),
|
|
196
|
+
labels: z.array(z.string()).optional(),
|
|
197
|
+
category: z.string().optional(),
|
|
198
|
+
estimated_hours: z.number().nonnegative().optional(),
|
|
199
|
+
goal_id: z.string().nullable().optional(),
|
|
200
|
+
project_id: z.string().nullable().optional(),
|
|
201
|
+
scheduled_date: z.string().nullable().optional(),
|
|
202
|
+
start_time: z.string().nullable().optional(),
|
|
203
|
+
end_time: z.string().nullable().optional(),
|
|
204
|
+
duration_minutes: z.number().int().positive().optional(),
|
|
205
|
+
task_type: z.string().optional(),
|
|
206
|
+
why: z.string().optional(),
|
|
207
|
+
sort_order: z.number().int().optional(),
|
|
208
|
+
},
|
|
209
|
+
"update_task"
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
tool(
|
|
213
|
+
"complete_task",
|
|
214
|
+
"Mark a task as completed.",
|
|
215
|
+
{ task_id: z.string() },
|
|
216
|
+
"complete_task"
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
tool(
|
|
220
|
+
"defer_task",
|
|
221
|
+
"Set a task's due_date to a new date (does not change scheduled_date).",
|
|
222
|
+
{ task_id: z.string(), new_date: z.string() },
|
|
223
|
+
"defer_task"
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
tool(
|
|
227
|
+
"schedule_task",
|
|
228
|
+
"Move a task onto a specific date (MyDay). Optionally set start/end time and duration.",
|
|
229
|
+
{
|
|
230
|
+
task_id: z.string(),
|
|
231
|
+
scheduled_date: z.string(),
|
|
232
|
+
start_time: z.string().optional(),
|
|
233
|
+
end_time: z.string().optional(),
|
|
234
|
+
duration_minutes: z.number().int().positive().optional(),
|
|
235
|
+
task_type: z.enum(["deep", "shallow", "admin", "meeting"]).optional(),
|
|
236
|
+
},
|
|
237
|
+
"schedule_task"
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
// Goals
|
|
241
|
+
tool(
|
|
242
|
+
"list_goals",
|
|
243
|
+
"List your goals, optionally filtered by status ('active', 'completed', ...).",
|
|
244
|
+
{ status: z.string().optional() },
|
|
245
|
+
"list_goals"
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
tool(
|
|
249
|
+
"create_goal",
|
|
250
|
+
"Create a goal.",
|
|
251
|
+
{
|
|
252
|
+
title: z.string().min(1),
|
|
253
|
+
description: z.string().optional(),
|
|
254
|
+
color: z.string().optional(),
|
|
255
|
+
target_date: z.string().optional(),
|
|
256
|
+
timeline_days: z.number().int().positive().optional(),
|
|
257
|
+
project_id: z.string().optional(),
|
|
258
|
+
status: z.string().optional(),
|
|
259
|
+
priority: z.number().int().min(1).max(5).optional(),
|
|
260
|
+
tags: z.array(z.string()).optional(),
|
|
261
|
+
horizon: z.string().optional(),
|
|
262
|
+
area: z.string().optional(),
|
|
263
|
+
},
|
|
264
|
+
"create_goal"
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
tool(
|
|
268
|
+
"update_goal",
|
|
269
|
+
"Patch a goal.",
|
|
270
|
+
{
|
|
271
|
+
goal_id: z.string(),
|
|
272
|
+
title: z.string().optional(),
|
|
273
|
+
description: z.string().optional(),
|
|
274
|
+
color: z.string().optional(),
|
|
275
|
+
progress: z.number().min(0).max(100).optional(),
|
|
276
|
+
target_date: z.string().nullable().optional(),
|
|
277
|
+
timeline_days: z.number().int().positive().optional(),
|
|
278
|
+
status: z.string().optional(),
|
|
279
|
+
priority: z.number().int().min(1).max(5).optional(),
|
|
280
|
+
is_archived: z.boolean().optional(),
|
|
281
|
+
tags: z.array(z.string()).optional(),
|
|
282
|
+
sort_order: z.number().int().optional(),
|
|
283
|
+
horizon: z.string().nullable().optional(),
|
|
284
|
+
area: z.string().nullable().optional(),
|
|
285
|
+
},
|
|
286
|
+
"update_goal"
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
tool(
|
|
290
|
+
"delete_goal",
|
|
291
|
+
"Delete a goal. Tasks that reference it are unlinked first (goal_id set to null).",
|
|
292
|
+
{ goal_id: z.string() },
|
|
293
|
+
"delete_goal"
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
// Notes
|
|
297
|
+
tool(
|
|
298
|
+
"list_notes",
|
|
299
|
+
"List notes, pinned first, then most recently updated.",
|
|
300
|
+
{ limit: z.number().int().positive().max(200).optional() },
|
|
301
|
+
"list_notes"
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
tool(
|
|
305
|
+
"search_notes",
|
|
306
|
+
"Search note titles and content by ILIKE.",
|
|
307
|
+
{ query: z.string().min(1), limit: z.number().int().positive().max(100).optional() },
|
|
308
|
+
"search_notes"
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
tool(
|
|
312
|
+
"create_note",
|
|
313
|
+
"Create a note.",
|
|
314
|
+
{
|
|
315
|
+
title: z.string().optional(),
|
|
316
|
+
content: z.string().optional(),
|
|
317
|
+
tags: z.array(z.string()).optional(),
|
|
318
|
+
color: z.string().optional(),
|
|
319
|
+
},
|
|
320
|
+
"create_note"
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
tool(
|
|
324
|
+
"update_note",
|
|
325
|
+
"Patch a note.",
|
|
326
|
+
{
|
|
327
|
+
note_id: z.string(),
|
|
328
|
+
title: z.string().optional(),
|
|
329
|
+
content: z.string().optional(),
|
|
330
|
+
tags: z.array(z.string()).optional(),
|
|
331
|
+
color: z.string().optional(),
|
|
332
|
+
isPinned: z.boolean().optional(),
|
|
333
|
+
},
|
|
334
|
+
"update_note"
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
// Habits & projects (read-only for v1)
|
|
338
|
+
tool("list_habits", "List your active (non-archived) habits.", {}, "list_habits");
|
|
339
|
+
tool("list_projects", "List your projects, most recently updated first.", {}, "list_projects");
|
|
340
|
+
|
|
341
|
+
// ───────────────────────────── EMAIL (Gmail) ─────────────────────────────
|
|
342
|
+
|
|
343
|
+
const sendAttachmentSchema = z.object({
|
|
344
|
+
filename: z.string().min(1),
|
|
345
|
+
mimeType: z.string().optional(),
|
|
346
|
+
contentBase64: z.string().min(1),
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
tool(
|
|
350
|
+
"list_emails",
|
|
351
|
+
"List Gmail threads in a label (default INBOX). Returns thread previews (subject/from/snippet/labels/unread).",
|
|
352
|
+
{
|
|
353
|
+
maxResults: z.number().int().min(1).max(50).optional(),
|
|
354
|
+
pageToken: z.string().optional(),
|
|
355
|
+
q: z.string().optional(),
|
|
356
|
+
label: z.string().optional(),
|
|
357
|
+
},
|
|
358
|
+
"list_emails"
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
tool(
|
|
362
|
+
"get_email",
|
|
363
|
+
"Get the full content of a thread or single message. Returns body text + HTML + attachment metadata (use get_attachment to download).",
|
|
364
|
+
{
|
|
365
|
+
threadId: z.string().optional(),
|
|
366
|
+
messageId: z.string().optional(),
|
|
367
|
+
},
|
|
368
|
+
"get_email"
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
tool(
|
|
372
|
+
"get_attachment",
|
|
373
|
+
"Download a single attachment from an email. Returns { size, contentBase64 } — decode the base64 to get the bytes.",
|
|
374
|
+
{
|
|
375
|
+
messageId: z.string(),
|
|
376
|
+
attachmentId: z.string(),
|
|
377
|
+
},
|
|
378
|
+
"get_attachment"
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
tool(
|
|
382
|
+
"search_emails",
|
|
383
|
+
"Search Gmail using Gmail's query syntax (e.g. 'from:alice has:attachment newer_than:7d'). Returns message ids; use get_email to fetch.",
|
|
384
|
+
{
|
|
385
|
+
query: z.string().min(1),
|
|
386
|
+
maxResults: z.number().int().min(1).max(50).optional(),
|
|
387
|
+
},
|
|
388
|
+
"search_emails"
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
tool(
|
|
392
|
+
"send_email",
|
|
393
|
+
"Send a new email. Attachments are base64-encoded; ≤3MB each, ≤10MB total. Rate-limited to 5/min.",
|
|
394
|
+
{
|
|
395
|
+
to: z.union([z.string(), z.array(z.string())]),
|
|
396
|
+
cc: z.union([z.string(), z.array(z.string())]).optional(),
|
|
397
|
+
bcc: z.union([z.string(), z.array(z.string())]).optional(),
|
|
398
|
+
subject: z.string().min(1),
|
|
399
|
+
bodyText: z.string().optional(),
|
|
400
|
+
bodyHtml: z.string().optional(),
|
|
401
|
+
attachments: z.array(sendAttachmentSchema).optional(),
|
|
402
|
+
},
|
|
403
|
+
"send_email"
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
tool(
|
|
407
|
+
"reply_email",
|
|
408
|
+
"Reply to an existing thread. Subject and recipient default to the last message's. Rate-limited to 5/min.",
|
|
409
|
+
{
|
|
410
|
+
threadId: z.string(),
|
|
411
|
+
to: z.union([z.string(), z.array(z.string())]).optional(),
|
|
412
|
+
cc: z.union([z.string(), z.array(z.string())]).optional(),
|
|
413
|
+
bcc: z.union([z.string(), z.array(z.string())]).optional(),
|
|
414
|
+
subject: z.string().optional(),
|
|
415
|
+
bodyText: z.string().optional(),
|
|
416
|
+
bodyHtml: z.string().optional(),
|
|
417
|
+
attachments: z.array(sendAttachmentSchema).optional(),
|
|
418
|
+
},
|
|
419
|
+
"reply_email"
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
// ───────────────────────────── main ─────────────────────────────
|
|
423
|
+
|
|
424
|
+
async function main() {
|
|
425
|
+
const transport = new StdioServerTransport();
|
|
426
|
+
await server.connect(transport);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
main().catch((err) => {
|
|
430
|
+
console.error("[mursa-mcp] fatal:", err);
|
|
431
|
+
process.exit(1);
|
|
432
|
+
});
|