todoai-mcp 0.1.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/.env.example +13 -0
- package/README.md +299 -0
- package/api/index.js +6 -0
- package/package.json +54 -0
- package/src/app.js +65 -0
- package/src/client.js +106 -0
- package/src/index.js +139 -0
- package/src/server.js +470 -0
package/.env.example
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# TodoAI MCP Server Configuration
|
|
2
|
+
# Copy this to .env and fill in your values.
|
|
3
|
+
|
|
4
|
+
# Required: API key generated from TodoAI Settings -> API Keys
|
|
5
|
+
TODOAI_API_KEY=tai_xxxxx
|
|
6
|
+
|
|
7
|
+
# Backend API URL (default: http://localhost:3000)
|
|
8
|
+
# Change to your deployed backend URL for remote/cloud usage
|
|
9
|
+
# TODOAI_API_URL=https://your-backend.vercel.app
|
|
10
|
+
|
|
11
|
+
# Set PORT for HTTP mode (Streamable HTTP transport)
|
|
12
|
+
# Leave unset for stdio mode (local AI agents)
|
|
13
|
+
# PORT=3001
|
package/README.md
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
# TodoAI MCP Server
|
|
2
|
+
|
|
3
|
+
Let any AI agent manage your todos through the Model Context Protocol — works with **every** MCP-compatible client.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# Run without installing (auto-cached by npm):
|
|
11
|
+
npx todoai-mcp
|
|
12
|
+
|
|
13
|
+
# Or install globally:
|
|
14
|
+
npm install -g todoai-mcp
|
|
15
|
+
todoai-mcp
|
|
16
|
+
|
|
17
|
+
# Or install locally:
|
|
18
|
+
npm install todoai-mcp
|
|
19
|
+
npx todoai-mcp
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
### One-command setup (interactive)
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npx todoai-mcp --init
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
This will ask for your API key, backend URL, and port — then write `.env` and print config snippets for every tool.
|
|
33
|
+
|
|
34
|
+
### Manual setup
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npx todoai-mcp
|
|
38
|
+
# If API key is missing, it shows instructions and exits
|
|
39
|
+
|
|
40
|
+
# Or with env vars inline:
|
|
41
|
+
TODOAI_API_KEY=tai_xxxxx npx todoai-mcp
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
That's it. Stdio transport is ready. Connect from any MCP client below.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Configuration
|
|
49
|
+
|
|
50
|
+
### 1. Generate an API Key
|
|
51
|
+
|
|
52
|
+
In the TodoAI app → **Settings → API Keys** → create a key (e.g. "MCP Server").
|
|
53
|
+
|
|
54
|
+
### 2. Start the server
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# Interactive setup (recommended):
|
|
58
|
+
npx todoai-mcp --init
|
|
59
|
+
|
|
60
|
+
# Or start directly with env vars:
|
|
61
|
+
TODOAI_API_KEY=tai_xxxxx npx todoai-mcp
|
|
62
|
+
|
|
63
|
+
# Or locally after checkout:
|
|
64
|
+
cp .env.example .env
|
|
65
|
+
nano .env
|
|
66
|
+
node src/index.js
|
|
67
|
+
|
|
68
|
+
# HTTP mode (for remote/cloud IDE agents):
|
|
69
|
+
PORT=3001 TODOAI_API_KEY=tai_xxxxx npx todoai-mcp
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Connecting Every MCP-Compatible Tool
|
|
75
|
+
|
|
76
|
+
### Claude Desktop (Anthropic)
|
|
77
|
+
|
|
78
|
+
Edit `claude_desktop_config.json` (File → Settings → Developer → Edit Config):
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
{
|
|
82
|
+
"mcpServers": {
|
|
83
|
+
"todoai": {
|
|
84
|
+
"command": "npx",
|
|
85
|
+
"args": ["-y", "todoai-mcp"],
|
|
86
|
+
"env": {
|
|
87
|
+
"TODOAI_API_KEY": "tai_xxxxx",
|
|
88
|
+
"TODOAI_API_URL": "http://localhost:3000"
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
> `-y` auto-downloads the package if not cached. No install needed.
|
|
96
|
+
|
|
97
|
+
### Claude Code CLI (Anthropic)
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
# One-shot with npx:
|
|
101
|
+
TODOAI_API_KEY=tai_xxxxx claude --mcp "todoai=npx -y todoai-mcp"
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
In `.claude/settings.json` or `~/.claude/settings.json`:
|
|
105
|
+
|
|
106
|
+
```json
|
|
107
|
+
{
|
|
108
|
+
"mcpServers": {
|
|
109
|
+
"todoai": {
|
|
110
|
+
"command": "npx",
|
|
111
|
+
"args": ["-y", "todoai-mcp"],
|
|
112
|
+
"env": {
|
|
113
|
+
"TODOAI_API_KEY": "tai_xxxxx"
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Cursor
|
|
121
|
+
|
|
122
|
+
Settings → Features → MCP Servers → Add Server:
|
|
123
|
+
|
|
124
|
+
| Field | Value |
|
|
125
|
+
|---|---|
|
|
126
|
+
| Name | `todoai` |
|
|
127
|
+
| Type | `command` |
|
|
128
|
+
| Command | `npx -y todoai-mcp` |
|
|
129
|
+
|
|
130
|
+
Then add `TODOAI_API_KEY=tai_xxxxx` in the server's environment variables.
|
|
131
|
+
|
|
132
|
+
### Windsurf
|
|
133
|
+
|
|
134
|
+
In `.windsurf/config.json` at the project root:
|
|
135
|
+
|
|
136
|
+
```json
|
|
137
|
+
{
|
|
138
|
+
"mcpServers": {
|
|
139
|
+
"todoai": {
|
|
140
|
+
"command": "npx",
|
|
141
|
+
"args": ["-y", "todoai-mcp"],
|
|
142
|
+
"env": {
|
|
143
|
+
"TODOAI_API_KEY": "tai_xxxxx"
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### GitHub Copilot (VS Code)
|
|
151
|
+
|
|
152
|
+
Via VS Code `settings.json`:
|
|
153
|
+
|
|
154
|
+
```json
|
|
155
|
+
{
|
|
156
|
+
"github.copilot.mcpServers": {
|
|
157
|
+
"todoai": {
|
|
158
|
+
"command": "npx",
|
|
159
|
+
"args": ["-y", "todoai-mcp"],
|
|
160
|
+
"env": {
|
|
161
|
+
"TODOAI_API_KEY": "tai_xxxxx"
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Any MCP-compatible CLI (`mcp-cli`, `mcp-gateway`, etc.)
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
# Via npx (auto-download):
|
|
172
|
+
TODOAI_API_KEY=tai_xxxxx npx todoai-mcp
|
|
173
|
+
|
|
174
|
+
# Via the binary (if installed globally):
|
|
175
|
+
npm install -g todoai-mcp
|
|
176
|
+
todoai-mcp --init
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Pipe / script usage (any shell)
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | \
|
|
183
|
+
TODOAI_API_KEY=tai_xxxxx npx todoai-mcp
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Generic MCP Host (any MCP-compatible tool)
|
|
187
|
+
|
|
188
|
+
The server complies with the MCP stdio protocol. Any tool that supports `command`-type MCP servers works with:
|
|
189
|
+
|
|
190
|
+
- **command**: `npx`
|
|
191
|
+
- **args**: `["-y", "todoai-mcp"]`
|
|
192
|
+
- **env**: `{ "TODOAI_API_KEY": "tai_xxxxx" }`
|
|
193
|
+
|
|
194
|
+
### Remote HTTP Mode (for cloud IDEs / agents that only speak HTTP)
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
PORT=3001 TODOAI_API_KEY=tai_xxxxx npx todoai-mcp
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Connect from any HTTP MCP client:
|
|
201
|
+
|
|
202
|
+
```json
|
|
203
|
+
{
|
|
204
|
+
"mcpServers": {
|
|
205
|
+
"todoai": {
|
|
206
|
+
"url": "http://localhost:3001/mcp",
|
|
207
|
+
"headers": {
|
|
208
|
+
"x-api-key": "tai_xxxxx"
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## Security
|
|
218
|
+
|
|
219
|
+
| Layer | Protection |
|
|
220
|
+
|---|---|
|
|
221
|
+
| **API Key** | All requests require the `x-api-key` header matching `TODOAI_API_KEY`. Reject with `401` if invalid. |
|
|
222
|
+
| **Rate Limit** | 100 requests/hour per IP (HTTP mode). Resets every hour. |
|
|
223
|
+
| **Backend Auth** | The MCP server authenticates with the backend using your API key. All operations run under that user's permissions and plan limits. |
|
|
224
|
+
|
|
225
|
+
### Security Best Practices
|
|
226
|
+
|
|
227
|
+
- Keep `TODOAI_API_KEY` in environment variables, never in code
|
|
228
|
+
- Do not commit `.env` to version control (`.env` is in `.gitignore`)
|
|
229
|
+
- For remote deployments, use a secrets manager instead of plain env vars
|
|
230
|
+
- Each MCP server instance uses one API key — generate a dedicated key per instance
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## Plan Limits (Free / Pro / Business)
|
|
235
|
+
|
|
236
|
+
The backend enforces these limits on every API call made through the MCP server:
|
|
237
|
+
|
|
238
|
+
| Tier | Rate Limit | Max Active Tasks | Features |
|
|
239
|
+
|---|---|---|---|
|
|
240
|
+
| **Free** | 100 req/hour | 50 | Basic CRUD, AI parse, daily summary |
|
|
241
|
+
| **Pro** ($) | 1000 req/hour | Unlimited | Priority suggestions, breakdown, reschedule |
|
|
242
|
+
| **Business** ($$) | 5000 req/hour | Unlimited | All features, team workspaces, audit logs |
|
|
243
|
+
|
|
244
|
+
When a limit is hit, the MCP tool returns a clear error message telling you which limit was exceeded and what to do.
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## Deployment
|
|
249
|
+
|
|
250
|
+
### Vercel (serverless)
|
|
251
|
+
|
|
252
|
+
Push `todo-mcp/` to a GitHub repo → Import to Vercel:
|
|
253
|
+
|
|
254
|
+
- **Root Directory**: `todo-mcp`
|
|
255
|
+
- **Build Command**: `npm install`
|
|
256
|
+
- **Start Command**: `node src/index.js`
|
|
257
|
+
- **Environment Variables**: `TODOAI_API_KEY`, `TODOAI_API_URL`, `PORT=3001`
|
|
258
|
+
- Endpoint: `https://your-app.vercel.app/mcp`
|
|
259
|
+
|
|
260
|
+
### Railway / Render / Fly.io (long-running)
|
|
261
|
+
|
|
262
|
+
```bash
|
|
263
|
+
# Set env vars: PORT=3001, TODOAI_API_KEY, TODOAI_API_URL
|
|
264
|
+
# Start command: node src/index.js
|
|
265
|
+
# Or if published: npx todoai-mcp
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Health check: `https://your-app.com/health`
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## Available Tools
|
|
273
|
+
|
|
274
|
+
| Tool | Description |
|
|
275
|
+
|---|---|
|
|
276
|
+
| `list_todos` | List, search, and filter todos |
|
|
277
|
+
| `create_todo` | Create a todo (supports natural language + workspace) |
|
|
278
|
+
| `complete_todo` | Mark a todo as complete |
|
|
279
|
+
| `delete_todo` | Delete a todo |
|
|
280
|
+
| `suggest_priority` | AI priority analysis |
|
|
281
|
+
| `summarize_day` | Daily task summary |
|
|
282
|
+
| `break_down_task` | Split a task into subtasks |
|
|
283
|
+
| `reschedule` | View schedule overview |
|
|
284
|
+
|
|
285
|
+
## Examples
|
|
286
|
+
|
|
287
|
+
- "What's on my todo list?"
|
|
288
|
+
- "Add 'Buy groceries tomorrow' high priority"
|
|
289
|
+
- "Complete the task about reviewing the PR"
|
|
290
|
+
- "Summarize my day"
|
|
291
|
+
- "Create a task in the Design workspace"
|
|
292
|
+
|
|
293
|
+
## Environment Variables
|
|
294
|
+
|
|
295
|
+
| Variable | Required | Default | Description |
|
|
296
|
+
|---|---|---|---|
|
|
297
|
+
| `TODOAI_API_KEY` | Yes | — | API key from TodoAI Settings → API Keys |
|
|
298
|
+
| `TODOAI_API_URL` | No | `http://localhost:3000` | Backend API URL |
|
|
299
|
+
| `PORT` | No | — | Set for HTTP mode; omit for stdio |
|
package/api/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "todoai-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "TodoAI MCP Server — let any AI agent manage your todos natively through the Model Context Protocol",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"todoai-mcp": "./src/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src/",
|
|
12
|
+
"api/",
|
|
13
|
+
".env.example",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"start": "node src/index.js",
|
|
18
|
+
"dev": "node --watch src/index.js",
|
|
19
|
+
"init": "node src/index.js --init",
|
|
20
|
+
"setup": "node src/index.js --init",
|
|
21
|
+
"prepublishOnly": "echo 'Publishing to npm...'",
|
|
22
|
+
"postinstall": "echo 'TodoAI MCP Server installed. Run: npx todoai-mcp --init to configure'"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"mcp",
|
|
26
|
+
"model-context-protocol",
|
|
27
|
+
"todo",
|
|
28
|
+
"task-management",
|
|
29
|
+
"ai",
|
|
30
|
+
"claude",
|
|
31
|
+
"cursor",
|
|
32
|
+
"windsurf",
|
|
33
|
+
"copilot"
|
|
34
|
+
],
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "https://github.com/Ashish-chanchal/todo-mcp.git"
|
|
39
|
+
},
|
|
40
|
+
"homepage": "https://github.com/Ashish-chanchal/todo-mcp#readme",
|
|
41
|
+
"bugs": {
|
|
42
|
+
"url": "https://github.com/Ashish-chanchal/todo-mcp/issues"
|
|
43
|
+
},
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=18"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
49
|
+
"cors": "^2.8.6",
|
|
50
|
+
"dotenv": "^16.4.5",
|
|
51
|
+
"express": "^5.2.1",
|
|
52
|
+
"zod": "^3.23.8"
|
|
53
|
+
}
|
|
54
|
+
}
|
package/src/app.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import cors from 'cors';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
5
|
+
import { server, API_KEY } from './server.js';
|
|
6
|
+
|
|
7
|
+
const requestCounts = new Map();
|
|
8
|
+
setInterval(() => requestCounts.clear(), 60 * 60 * 1000);
|
|
9
|
+
|
|
10
|
+
function rateLimiter(req, res, next) {
|
|
11
|
+
const key = req.ip || 'unknown';
|
|
12
|
+
const current = requestCounts.get(key) || { count: 0, firstRequest: Date.now() };
|
|
13
|
+
|
|
14
|
+
if (current.count >= 100) {
|
|
15
|
+
const resetTime = new Date(current.firstRequest + 60 * 60 * 1000);
|
|
16
|
+
return res.status(429).json({
|
|
17
|
+
msg: `Too many requests. Rate limit resets at ${resetTime.toLocaleTimeString()}.`,
|
|
18
|
+
resetAt: resetTime,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
current.count += 1;
|
|
23
|
+
requestCounts.set(key, current);
|
|
24
|
+
next();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function requireApiKey(req, res, next) {
|
|
28
|
+
if (!API_KEY) return next();
|
|
29
|
+
const key = req.headers['x-api-key'] || req.headers['api-key'];
|
|
30
|
+
if (!key || key !== API_KEY) {
|
|
31
|
+
return res.status(401).json({ msg: 'Invalid or missing x-api-key header. Must match TODOAI_API_KEY.' });
|
|
32
|
+
}
|
|
33
|
+
next();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let transport = null;
|
|
37
|
+
|
|
38
|
+
export function createApp() {
|
|
39
|
+
const app = express();
|
|
40
|
+
app.use(cors());
|
|
41
|
+
app.use(express.json());
|
|
42
|
+
app.use(rateLimiter);
|
|
43
|
+
|
|
44
|
+
app.all('/mcp', requireApiKey, (req, res) => {
|
|
45
|
+
if (!transport) {
|
|
46
|
+
transport = new StreamableHTTPServerTransport({
|
|
47
|
+
sessionIdGenerator: () => randomUUID(),
|
|
48
|
+
});
|
|
49
|
+
server.connect(transport);
|
|
50
|
+
}
|
|
51
|
+
transport.handleRequest(req, res, req.body);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
app.get('/health', (_req, res) => {
|
|
55
|
+
res.json({
|
|
56
|
+
status: 'ok',
|
|
57
|
+
server: 'todoai-mcp',
|
|
58
|
+
version: '0.1.0',
|
|
59
|
+
apiKeyConfigured: !!API_KEY,
|
|
60
|
+
sessionId: transport?.sessionId || null,
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
return app;
|
|
65
|
+
}
|
package/src/client.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
const TODOAI_API_URL = process.env.TODOAI_API_URL || 'http://127.0.0.1:3000';
|
|
2
|
+
|
|
3
|
+
const BASE_HEADERS = {
|
|
4
|
+
'Content-Type': 'application/json',
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export class TodoAiClient {
|
|
8
|
+
constructor(apiKey) {
|
|
9
|
+
this.apiKey = apiKey;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
headers(extra = {}) {
|
|
13
|
+
return {
|
|
14
|
+
...BASE_HEADERS,
|
|
15
|
+
'x-api-key': this.apiKey,
|
|
16
|
+
...extra,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async request(method, path, body) {
|
|
21
|
+
const url = `${TODOAI_API_URL}${path}`;
|
|
22
|
+
const options = {
|
|
23
|
+
method,
|
|
24
|
+
headers: this.headers(),
|
|
25
|
+
};
|
|
26
|
+
if (body) options.body = JSON.stringify(body);
|
|
27
|
+
|
|
28
|
+
const res = await fetch(url, options);
|
|
29
|
+
const data = await res.json();
|
|
30
|
+
|
|
31
|
+
if (!res.ok) {
|
|
32
|
+
throw new Error(data.msg || `API error: ${res.status}`);
|
|
33
|
+
}
|
|
34
|
+
return data;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get(path) {
|
|
38
|
+
return this.request('GET', path);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
post(path, body) {
|
|
42
|
+
return this.request('POST', path, body);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
put(path, body) {
|
|
46
|
+
return this.request('PUT', path, body);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
del(path, body) {
|
|
50
|
+
return this.request('DELETE', path, body);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// --- Tool-specific API calls ---
|
|
54
|
+
|
|
55
|
+
async listTodos(params = {}) {
|
|
56
|
+
const query = {};
|
|
57
|
+
if (params.filter === 'active') query.completed = 'false';
|
|
58
|
+
else if (params.filter === 'completed') query.completed = 'true';
|
|
59
|
+
if (params.priority) query.priority = params.priority;
|
|
60
|
+
if (params.tag) query.tag = params.tag;
|
|
61
|
+
if (params.q) query.q = params.q;
|
|
62
|
+
if (params.page) query.page = params.page;
|
|
63
|
+
if (params.limit) query.limit = params.limit;
|
|
64
|
+
if (params.teamId) query.teamId = params.teamId;
|
|
65
|
+
const qs = new URLSearchParams(query).toString();
|
|
66
|
+
return this.get(`/todos${qs ? `?${qs}` : ''}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async createTodo(title, description, priority, dueDate, tags, teamId) {
|
|
70
|
+
const body = { title, description, priority, dueDate, tags };
|
|
71
|
+
if (teamId) body.teamId = teamId;
|
|
72
|
+
return this.post('/todo', body);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async completeTodo(id) {
|
|
76
|
+
return this.put('/completed', { id });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async deleteTodo(id) {
|
|
80
|
+
return this.del('/delete', { id });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async searchTodos(query) {
|
|
84
|
+
return this.get(`/todos?q=${encodeURIComponent(query)}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async aiParse(text) {
|
|
88
|
+
let body;
|
|
89
|
+
if (typeof text === 'string') {
|
|
90
|
+
body = { text };
|
|
91
|
+
} else if (typeof text === 'object' && text !== null) {
|
|
92
|
+
body = text;
|
|
93
|
+
} else {
|
|
94
|
+
body = { text: String(text) };
|
|
95
|
+
}
|
|
96
|
+
return this.post('/ai/parse', body);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async aiAnalyze(text) {
|
|
100
|
+
return this.post('/ai/analyze', { text });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async aiPrioritize() {
|
|
104
|
+
return this.post('/ai/prioritize');
|
|
105
|
+
}
|
|
106
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import 'dotenv/config';
|
|
4
|
+
import { createInterface } from 'node:readline';
|
|
5
|
+
import { existsSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { join, dirname } from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { API_KEY, server } from './server.js';
|
|
9
|
+
import { createApp } from './app.js';
|
|
10
|
+
|
|
11
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
|
|
13
|
+
function ask(query) {
|
|
14
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
15
|
+
return new Promise(resolve => rl.question(query, answer => { rl.close(); resolve(answer.trim()); }));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function printConfig(key, backendUrl, port) {
|
|
19
|
+
const todoaiBackend = backendUrl || 'http://localhost:3000';
|
|
20
|
+
const isHttp = !!port;
|
|
21
|
+
const transport = isHttp ? 'http' : 'stdio';
|
|
22
|
+
const serverRef = isHttp ? `http://localhost:${port}/mcp` : 'node /path/to/todo-mcp/src/index.js';
|
|
23
|
+
|
|
24
|
+
console.error('');
|
|
25
|
+
console.error('═'.repeat(50));
|
|
26
|
+
console.error(' TodoAI MCP Server — Configuration');
|
|
27
|
+
console.error('═'.repeat(50));
|
|
28
|
+
console.error(` Transport : ${transport}`);
|
|
29
|
+
console.error(` Backend : ${todoaiBackend}`);
|
|
30
|
+
console.error(` API Key : ${key ? key.substring(0, 8) + '...' : 'NOT SET'}`);
|
|
31
|
+
console.error('');
|
|
32
|
+
|
|
33
|
+
const configs = {
|
|
34
|
+
'Claude Desktop (claude_desktop_config.json)': {
|
|
35
|
+
mcpServers: {
|
|
36
|
+
todoai: isHttp
|
|
37
|
+
? { url: `http://localhost:${port}/mcp`, headers: { 'x-api-key': key } }
|
|
38
|
+
: { command: 'node', args: ['/path/to/todo-mcp/src/index.js'], env: { TODOAI_API_KEY: key, TODOAI_API_URL: todoaiBackend } }
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
'Claude Code (.claude/settings.json)': isHttp
|
|
42
|
+
? { mcpServers: { todoai: { url: `http://localhost:${port}/mcp`, headers: { 'x-api-key': key } } } }
|
|
43
|
+
: { mcpServers: { todoai: { command: 'node', args: ['/path/to/todo-mcp/src/index.js'], env: { TODOAI_API_KEY: key } } } },
|
|
44
|
+
'MCP CLI': isHttp
|
|
45
|
+
? `npx @anthropic-ai/mcp-cli connect --url http://localhost:${port}/mcp --headers '{"x-api-key":"${key}"}'`
|
|
46
|
+
: `TODOAI_API_KEY=${key} npx @anthropic-ai/mcp-cli --server "node /path/to/todo-mcp/src/index.js"`,
|
|
47
|
+
'Any MCP tool (stdio)': `TODOAI_API_KEY=${key} node /path/to/todo-mcp/src/index.js`,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
for (const [label, cfg] of Object.entries(configs)) {
|
|
51
|
+
console.error(`── ${label}`);
|
|
52
|
+
console.error(JSON.stringify(cfg, null, 2));
|
|
53
|
+
console.error('');
|
|
54
|
+
}
|
|
55
|
+
console.error('═'.repeat(50));
|
|
56
|
+
console.error('');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function runSetup() {
|
|
60
|
+
console.error('');
|
|
61
|
+
console.error(' TodoAI MCP Server — Setup');
|
|
62
|
+
console.error('═'.repeat(50));
|
|
63
|
+
|
|
64
|
+
let key = API_KEY || process.env.TODOAI_API_KEY;
|
|
65
|
+
if (!key) {
|
|
66
|
+
console.error(' No TODOAI_API_KEY found. Generate one in TodoAI → Settings → API Keys.');
|
|
67
|
+
key = await ask(' Enter your API key (tai_...): ');
|
|
68
|
+
if (!key) {
|
|
69
|
+
console.error(' API key is required. Exiting.');
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const backendUrl = process.env.TODOAI_API_URL || (await ask(` Backend URL [http://localhost:3000]: `)) || 'http://localhost:3000';
|
|
75
|
+
const port = process.env.PORT || (await ask(` HTTP port (empty for stdio): `));
|
|
76
|
+
|
|
77
|
+
// Write .env
|
|
78
|
+
const envPath = join(process.cwd(), '.env');
|
|
79
|
+
const envContent = [
|
|
80
|
+
`TODOAI_API_KEY=${key}`,
|
|
81
|
+
`TODOAI_API_URL=${backendUrl}`,
|
|
82
|
+
port ? `PORT=${port}` : '',
|
|
83
|
+
].filter(Boolean).join('\n') + '\n';
|
|
84
|
+
|
|
85
|
+
writeFileSync(envPath, envContent);
|
|
86
|
+
console.error(`\n .env written to ${envPath}`);
|
|
87
|
+
|
|
88
|
+
printConfig(key, backendUrl, port);
|
|
89
|
+
console.error(' Run: node src/index.js (or just: npx todoai-mcp)');
|
|
90
|
+
console.error('');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function main() {
|
|
94
|
+
const args = process.argv.slice(2);
|
|
95
|
+
|
|
96
|
+
if (args.includes('--init') || args.includes('setup')) {
|
|
97
|
+
await runSetup();
|
|
98
|
+
process.exit(0);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!API_KEY) {
|
|
102
|
+
console.error('');
|
|
103
|
+
console.error(' ╔══════════════════════════════════════════════════╗');
|
|
104
|
+
console.error(' ║ TODOAI_API_KEY is not set. ║');
|
|
105
|
+
console.error(' ║ ║');
|
|
106
|
+
console.error(' ║ 1. Generate a key in TodoAI → Settings → ║');
|
|
107
|
+
console.error(' ║ API Keys ║');
|
|
108
|
+
console.error(' ║ 2. Set it in your environment or .env file ║');
|
|
109
|
+
console.error(' ║ export TODOAI_API_KEY=tai_xxxxx ║');
|
|
110
|
+
console.error(' ║ ║');
|
|
111
|
+
console.error(' ║ Run with --init for interactive setup: ║');
|
|
112
|
+
console.error(' ║ npx todoai-mcp --init ║');
|
|
113
|
+
console.error(' ╚══════════════════════════════════════════════════╝');
|
|
114
|
+
console.error('');
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const PORT = process.env.PORT;
|
|
119
|
+
|
|
120
|
+
if (PORT) {
|
|
121
|
+
const app = createApp();
|
|
122
|
+
app.listen(PORT, () => {
|
|
123
|
+
console.error(`TodoAI MCP Server running on http://localhost:${PORT}/mcp`);
|
|
124
|
+
printConfig(API_KEY, process.env.TODOAI_API_URL, PORT);
|
|
125
|
+
});
|
|
126
|
+
} else {
|
|
127
|
+
const { StdioServerTransport } = await import('@modelcontextprotocol/sdk/server/stdio.js');
|
|
128
|
+
const transport = new StdioServerTransport();
|
|
129
|
+
await server.connect(transport);
|
|
130
|
+
console.error('TodoAI MCP Server running on stdio');
|
|
131
|
+
console.error('');
|
|
132
|
+
printConfig(API_KEY, process.env.TODOAI_API_URL);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
main().catch((err) => {
|
|
137
|
+
console.error('Fatal error:', err);
|
|
138
|
+
process.exit(1);
|
|
139
|
+
});
|
package/src/server.js
ADDED
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { TodoAiClient } from './client.js';
|
|
4
|
+
|
|
5
|
+
export const API_KEY = process.env.TODOAI_API_KEY || process.env.TODOAI_DEV_KEY;
|
|
6
|
+
export const client = API_KEY ? new TodoAiClient(API_KEY) : null;
|
|
7
|
+
export const server = new McpServer({ name: 'todoai', version: '0.1.0' });
|
|
8
|
+
|
|
9
|
+
export function requireClient() {
|
|
10
|
+
if (!client) {
|
|
11
|
+
return {
|
|
12
|
+
content: [{ type: 'text', text: 'TODOAI_API_KEY not configured. Generate an API key in Settings → API Keys and set the TODOAI_API_KEY environment variable.' }],
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const PRIORITY_ORDER = { high: 0, medium: 1, low: 2 };
|
|
19
|
+
|
|
20
|
+
function today() {
|
|
21
|
+
const d = new Date();
|
|
22
|
+
d.setHours(0, 0, 0, 0);
|
|
23
|
+
return d;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function daysFromNow(n) {
|
|
27
|
+
const d = today();
|
|
28
|
+
d.setDate(d.getDate() + n);
|
|
29
|
+
return d;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isSameDay(a, b) {
|
|
33
|
+
return a.getFullYear() === b.getFullYear()
|
|
34
|
+
&& a.getMonth() === b.getMonth()
|
|
35
|
+
&& a.getDate() === b.getDate();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function generatePriorityReasoning(text, priority) {
|
|
39
|
+
const lower = text.toLowerCase();
|
|
40
|
+
const urgentWords = ['urgent', 'asap', 'immediately', 'critical', 'deadline', 'emergency', 'important', 'must'];
|
|
41
|
+
const mediumWords = ['soon', 'week', 'next', 'pending', 'upcoming', 'shortly'];
|
|
42
|
+
|
|
43
|
+
if (priority === 'high') {
|
|
44
|
+
const found = urgentWords.filter(w => lower.includes(w));
|
|
45
|
+
if (found.length > 0) return `Contains urgency signals: ${found.join(', ')}`;
|
|
46
|
+
return 'Task appears time-sensitive or critical based on analysis';
|
|
47
|
+
}
|
|
48
|
+
if (priority === 'medium') {
|
|
49
|
+
const found = mediumWords.filter(w => lower.includes(w));
|
|
50
|
+
if (found.length > 0) return `Contains moderate-timeline signals: ${found.join(', ')}`;
|
|
51
|
+
return 'Task has moderate importance but no immediate urgency';
|
|
52
|
+
}
|
|
53
|
+
return 'Task appears low-urgency or informational, no critical time pressure detected';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function breakDownTask(text) {
|
|
57
|
+
const transitionWords = ['first', 'then', 'next', 'finally', 'after', 'before', 'lastly', 'second', 'third', 'fourth'];
|
|
58
|
+
const lower = text.toLowerCase();
|
|
59
|
+
|
|
60
|
+
const foundTransitions = transitionWords.filter(t => lower.includes(t));
|
|
61
|
+
const hasStructure = foundTransitions.length >= 2;
|
|
62
|
+
|
|
63
|
+
if (hasStructure) {
|
|
64
|
+
const steps = [];
|
|
65
|
+
const parts = text.split(new RegExp(`\\b(${foundTransitions.join('|')})\\b`, 'gi')).filter(p => p.trim());
|
|
66
|
+
let stepNum = 1;
|
|
67
|
+
for (const part of parts) {
|
|
68
|
+
if (transitionWords.includes(part.toLowerCase())) continue;
|
|
69
|
+
const trimmed = part.trim().replace(/^[,\s]+|[,\s]+$/g, '');
|
|
70
|
+
if (trimmed && !steps.includes(trimmed)) {
|
|
71
|
+
steps.push(trimmed);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return steps.slice(0, 5);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const keywords = {
|
|
78
|
+
write: ['Outline key points', 'Draft the content', 'Review and revise', 'Format the final version'],
|
|
79
|
+
create: ['Research requirements', 'Draft initial version', 'Review and refine', 'Finalize deliverable'],
|
|
80
|
+
develop: ['Gather specifications', 'Set up environment', 'Implement core logic', 'Test and debug', 'Deploy'],
|
|
81
|
+
build: ['Plan the structure', 'Gather materials', 'Assemble components', 'Test and iterate', 'Complete'],
|
|
82
|
+
organize: ['List all items', 'Categorize by type', 'Sort and prioritize', 'Create storage system'],
|
|
83
|
+
plan: ['Define objectives', 'Identify resources', 'Create timeline', 'Assign responsibilities', 'Review plan'],
|
|
84
|
+
learn: ['Find learning resources', 'Set study schedule', 'Practice regularly', 'Test knowledge', 'Review gaps'],
|
|
85
|
+
fix: ['Reproduce the issue', 'Identify root cause', 'Implement fix', 'Test the solution', 'Verify in production'],
|
|
86
|
+
clean: ['Declutter the area', 'Sort items', 'Deep clean surfaces', 'Organize remaining items', 'Maintain regularly'],
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
for (const [keyword, steps] of Object.entries(keywords)) {
|
|
90
|
+
if (lower.includes(keyword)) {
|
|
91
|
+
return steps;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return [
|
|
96
|
+
'Research and gather information',
|
|
97
|
+
'Plan the approach',
|
|
98
|
+
'Execute the main work',
|
|
99
|
+
'Review and refine results',
|
|
100
|
+
'Complete and follow up',
|
|
101
|
+
];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ── Tool 1: list_todos ──
|
|
105
|
+
server.registerTool(
|
|
106
|
+
'list_todos',
|
|
107
|
+
{
|
|
108
|
+
inputSchema: {
|
|
109
|
+
filter: z.string().optional().describe('Filter: "all", "active", "completed"'),
|
|
110
|
+
priority: z.string().optional().describe('Filter by priority: "low", "medium", "high"'),
|
|
111
|
+
tag: z.string().optional().describe('Filter by tag name'),
|
|
112
|
+
q: z.string().optional().describe('Search query'),
|
|
113
|
+
page: z.number().optional().describe('Page number (1-based)'),
|
|
114
|
+
limit: z.number().optional().describe('Items per page (default 20)'),
|
|
115
|
+
teamId: z.string().optional().describe('Workspace/team ID (omit for personal workspace)'),
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
async (args) => {
|
|
119
|
+
const noClient = requireClient(); if (noClient) return noClient;
|
|
120
|
+
try {
|
|
121
|
+
const data = await client.listTodos(args);
|
|
122
|
+
const todos = data.todos || [];
|
|
123
|
+
if (todos.length === 0) {
|
|
124
|
+
return { content: [{ type: 'text', text: 'No todos found.' }] };
|
|
125
|
+
}
|
|
126
|
+
const lines = todos.map((t, i) => {
|
|
127
|
+
const status = t.completed ? '✅' : '⬜';
|
|
128
|
+
const due = t.dueDate ? ` (due: ${new Date(t.dueDate).toLocaleDateString()})` : '';
|
|
129
|
+
const priority = t.priority ? ` [${t.priority}]` : '';
|
|
130
|
+
const tags = t.tags?.length ? ` #${t.tags.join(', #')}` : '';
|
|
131
|
+
return `${i + 1}. ${status} ${t.title}${priority}${due}${tags}`;
|
|
132
|
+
});
|
|
133
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
134
|
+
} catch (err) {
|
|
135
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
// ── Tool 2: create_todo ──
|
|
141
|
+
server.registerTool(
|
|
142
|
+
'create_todo',
|
|
143
|
+
{
|
|
144
|
+
inputSchema: {
|
|
145
|
+
title: z.string().describe('Todo title or natural language description'),
|
|
146
|
+
description: z.string().optional().describe('Detailed description'),
|
|
147
|
+
priority: z.enum(['low', 'medium', 'high']).optional().describe('Priority level'),
|
|
148
|
+
dueDate: z.string().optional().describe('Due date (YYYY-MM-DD or natural language)'),
|
|
149
|
+
tags: z.array(z.string()).optional().describe('Tags (max 3)'),
|
|
150
|
+
teamId: z.string().optional().describe('Workspace/team ID to create the todo in (omit for personal workspace)'),
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
async (args) => {
|
|
154
|
+
const noClient = requireClient(); if (noClient) return noClient;
|
|
155
|
+
try {
|
|
156
|
+
let { title, description, priority, dueDate, tags, teamId } = args;
|
|
157
|
+
|
|
158
|
+
if (tags) {
|
|
159
|
+
const seen = new Set();
|
|
160
|
+
tags = tags.map(t => String(t).toLowerCase().trim()).filter(t => t && !seen.has(t) && seen.add(t)).slice(0, 3);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (title.split(' ').length > 4 || dueDate || priority) {
|
|
164
|
+
const aiResult = await client.aiParse({ text: title, teamId });
|
|
165
|
+
if (aiResult.todo) {
|
|
166
|
+
return {
|
|
167
|
+
content: [{
|
|
168
|
+
type: 'text',
|
|
169
|
+
text: `Created todo: "${aiResult.todo.title}"\nPriority: ${aiResult.todo.priority}\nDue: ${aiResult.todo.dueDate || 'No due date'}\nTags: ${(aiResult.todo.tags || []).join(', ') || 'None'}`,
|
|
170
|
+
}],
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const data = await client.createTodo(title, description || '', priority || 'medium', dueDate || null, tags || [], teamId);
|
|
176
|
+
return {
|
|
177
|
+
content: [{
|
|
178
|
+
type: 'text',
|
|
179
|
+
text: `Created todo: "${data.todo.title}" (${data.todo._id})`,
|
|
180
|
+
}],
|
|
181
|
+
};
|
|
182
|
+
} catch (err) {
|
|
183
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
// ── Tool 3: complete_todo ──
|
|
189
|
+
server.registerTool(
|
|
190
|
+
'complete_todo',
|
|
191
|
+
{
|
|
192
|
+
inputSchema: {
|
|
193
|
+
id: z.string().optional().describe('Todo ID to mark as complete'),
|
|
194
|
+
search: z.string().optional().describe('Search text to find and complete a todo'),
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
async (args) => {
|
|
198
|
+
const noClient = requireClient(); if (noClient) return noClient;
|
|
199
|
+
try {
|
|
200
|
+
let todoId = args.id;
|
|
201
|
+
|
|
202
|
+
if (!todoId && args.search) {
|
|
203
|
+
const data = await client.searchTodos(args.search);
|
|
204
|
+
const todos = data.todos || [];
|
|
205
|
+
if (todos.length === 0) {
|
|
206
|
+
return { content: [{ type: 'text', text: `No todos matching "${args.search}"` }] };
|
|
207
|
+
}
|
|
208
|
+
todoId = todos[0]._id;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (!todoId) {
|
|
212
|
+
return { content: [{ type: 'text', text: 'Provide an id or search text' }] };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
await client.completeTodo(todoId);
|
|
216
|
+
return { content: [{ type: 'text', text: `Todo ${todoId} marked as complete.` }] };
|
|
217
|
+
} catch (err) {
|
|
218
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
// ── Tool 4: delete_todo ──
|
|
224
|
+
server.registerTool(
|
|
225
|
+
'delete_todo',
|
|
226
|
+
{
|
|
227
|
+
inputSchema: {
|
|
228
|
+
id: z.string().optional().describe('Todo ID to delete'),
|
|
229
|
+
search: z.string().optional().describe('Search text to find and delete a todo'),
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
async (args) => {
|
|
233
|
+
const noClient = requireClient(); if (noClient) return noClient;
|
|
234
|
+
try {
|
|
235
|
+
let todoId = args.id;
|
|
236
|
+
|
|
237
|
+
if (!todoId && args.search) {
|
|
238
|
+
const data = await client.searchTodos(args.search);
|
|
239
|
+
const todos = data.todos || [];
|
|
240
|
+
if (todos.length === 0) {
|
|
241
|
+
return { content: [{ type: 'text', text: `No todos matching "${args.search}"` }] };
|
|
242
|
+
}
|
|
243
|
+
todoId = todos[0]._id;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (!todoId) {
|
|
247
|
+
return { content: [{ type: 'text', text: 'Provide an id or search text' }] };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
await client.deleteTodo(todoId);
|
|
251
|
+
return { content: [{ type: 'text', text: `Todo ${todoId} deleted.` }] };
|
|
252
|
+
} catch (err) {
|
|
253
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
// ── Tool 5: suggest_priority ──
|
|
259
|
+
server.registerTool(
|
|
260
|
+
'suggest_priority',
|
|
261
|
+
{
|
|
262
|
+
inputSchema: {
|
|
263
|
+
text: z.string().describe('Task description to analyze'),
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
async (args) => {
|
|
267
|
+
const noClient = requireClient(); if (noClient) return noClient;
|
|
268
|
+
try {
|
|
269
|
+
const data = await client.aiAnalyze(args.text);
|
|
270
|
+
const priority = data.parsed?.priority || 'medium';
|
|
271
|
+
const reasoning = generatePriorityReasoning(args.text, priority);
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
content: [{
|
|
275
|
+
type: 'text',
|
|
276
|
+
text: `Based on analysis: Priority — ${priority}. Reasoning: ${reasoning}`,
|
|
277
|
+
}],
|
|
278
|
+
};
|
|
279
|
+
} catch (err) {
|
|
280
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
// ── Tool 6: summarize_day ──
|
|
286
|
+
server.registerTool(
|
|
287
|
+
'summarize_day',
|
|
288
|
+
{
|
|
289
|
+
inputSchema: {
|
|
290
|
+
date: z.string().optional().describe('Date to summarize (YYYY-MM-DD, defaults to today)'),
|
|
291
|
+
teamId: z.string().optional().describe('Workspace/team ID (omit for personal workspace)'),
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
async (args) => {
|
|
295
|
+
const noClient = requireClient(); if (noClient) return noClient;
|
|
296
|
+
try {
|
|
297
|
+
const data = await client.listTodos({ teamId: args.teamId });
|
|
298
|
+
const todos = data.todos || [];
|
|
299
|
+
const now = today();
|
|
300
|
+
const active = todos.filter(t => !t.completed);
|
|
301
|
+
const completed = todos.filter(t => t.completed);
|
|
302
|
+
const highPriority = active.filter(t => t.priority === 'high');
|
|
303
|
+
const overdue = active.filter(t => t.dueDate && new Date(t.dueDate) < now);
|
|
304
|
+
const dueToday = active.filter(t => t.dueDate && isSameDay(new Date(t.dueDate), now));
|
|
305
|
+
const total = todos.length;
|
|
306
|
+
const activeCount = active.length;
|
|
307
|
+
const completedCount = completed.length;
|
|
308
|
+
|
|
309
|
+
const lines = [];
|
|
310
|
+
lines.push('# 📋 TodoAI Daily Summary');
|
|
311
|
+
lines.push('');
|
|
312
|
+
lines.push('## 📊 Overview');
|
|
313
|
+
lines.push(`| Metric | Count |`);
|
|
314
|
+
lines.push(`|--------|-------|`);
|
|
315
|
+
lines.push(`| **Total Tasks** | ${total} |`);
|
|
316
|
+
lines.push(`| **Active** | ${activeCount} |`);
|
|
317
|
+
lines.push(`| **Completed** | ${completedCount} |`);
|
|
318
|
+
lines.push(`| **Overdue** | ${overdue.length} |`);
|
|
319
|
+
lines.push(`| **High Priority** | ${highPriority.length} |`);
|
|
320
|
+
lines.push(`| **Due Today** | ${dueToday.length} |`);
|
|
321
|
+
lines.push('');
|
|
322
|
+
|
|
323
|
+
if (overdue.length > 0) {
|
|
324
|
+
lines.push('## ⚠️ Overdue');
|
|
325
|
+
overdue.forEach(t => {
|
|
326
|
+
const due = new Date(t.dueDate).toLocaleDateString();
|
|
327
|
+
lines.push(`- **${t.title}** — due ${due} [${t.priority || 'medium'}]`);
|
|
328
|
+
});
|
|
329
|
+
lines.push('');
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (highPriority.length > 0) {
|
|
333
|
+
lines.push('## 🔴 High Priority');
|
|
334
|
+
highPriority.forEach(t => {
|
|
335
|
+
const due = t.dueDate ? ` (due ${new Date(t.dueDate).toLocaleDateString()})` : '';
|
|
336
|
+
lines.push(`- **${t.title}**${due}`);
|
|
337
|
+
});
|
|
338
|
+
lines.push('');
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (dueToday.length > 0) {
|
|
342
|
+
lines.push('## ✨ Quick Wins (Due Today)');
|
|
343
|
+
dueToday.forEach(t => {
|
|
344
|
+
lines.push(`- [ ] ${t.title} [${t.priority || 'medium'}]`);
|
|
345
|
+
});
|
|
346
|
+
lines.push('');
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (activeCount === 0) {
|
|
350
|
+
lines.push('## 🎉 All Done!');
|
|
351
|
+
lines.push('Nothing pending. Enjoy your day!');
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
355
|
+
} catch (err) {
|
|
356
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
// ── Tool 7: break_down_task ──
|
|
362
|
+
server.registerTool(
|
|
363
|
+
'break_down_task',
|
|
364
|
+
{
|
|
365
|
+
inputSchema: {
|
|
366
|
+
text: z.string().describe('Task description to break into steps'),
|
|
367
|
+
},
|
|
368
|
+
},
|
|
369
|
+
async (args) => {
|
|
370
|
+
const noClient = requireClient(); if (noClient) return noClient;
|
|
371
|
+
try {
|
|
372
|
+
const steps = breakDownTask(args.text);
|
|
373
|
+
const formatted = steps.map((s, i) => `${i + 1}. ${s}`).join('\n');
|
|
374
|
+
const lines = [];
|
|
375
|
+
lines.push(`Breakdown for: ${args.text}`);
|
|
376
|
+
lines.push('');
|
|
377
|
+
lines.push(formatted);
|
|
378
|
+
|
|
379
|
+
return {
|
|
380
|
+
content: [{
|
|
381
|
+
type: 'text',
|
|
382
|
+
text: lines.join('\n'),
|
|
383
|
+
}],
|
|
384
|
+
};
|
|
385
|
+
} catch (err) {
|
|
386
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
// ── Tool 8: reschedule ──
|
|
392
|
+
server.registerTool(
|
|
393
|
+
'reschedule',
|
|
394
|
+
{
|
|
395
|
+
inputSchema: {},
|
|
396
|
+
},
|
|
397
|
+
async () => {
|
|
398
|
+
const noClient = requireClient(); if (noClient) return noClient;
|
|
399
|
+
try {
|
|
400
|
+
const data = await client.listTodos({});
|
|
401
|
+
const todos = data.todos || [];
|
|
402
|
+
const active = todos.filter(t => !t.completed && t.dueDate);
|
|
403
|
+
|
|
404
|
+
if (active.length === 0) {
|
|
405
|
+
return { content: [{ type: 'text', text: 'No scheduled tasks to reschedule.' }] };
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const now = today();
|
|
409
|
+
const weekEnd = daysFromNow(7);
|
|
410
|
+
const monthEnd = daysFromNow(30);
|
|
411
|
+
|
|
412
|
+
const groups = {
|
|
413
|
+
'Due Today': [],
|
|
414
|
+
'Due This Week': [],
|
|
415
|
+
'Due This Month': [],
|
|
416
|
+
'No Rush': [],
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
for (const t of active) {
|
|
420
|
+
const due = new Date(t.dueDate);
|
|
421
|
+
due.setHours(0, 0, 0, 0);
|
|
422
|
+
|
|
423
|
+
if (isSameDay(due, now)) {
|
|
424
|
+
groups['Due Today'].push(t);
|
|
425
|
+
} else if (due <= weekEnd) {
|
|
426
|
+
groups['Due This Week'].push(t);
|
|
427
|
+
} else if (due <= monthEnd) {
|
|
428
|
+
groups['Due This Month'].push(t);
|
|
429
|
+
} else {
|
|
430
|
+
groups['No Rush'].push(t);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const lines = [];
|
|
435
|
+
lines.push('# 📅 Rescheduled View');
|
|
436
|
+
lines.push('');
|
|
437
|
+
|
|
438
|
+
for (const [groupName, groupTodos] of Object.entries(groups)) {
|
|
439
|
+
if (groupTodos.length === 0) continue;
|
|
440
|
+
|
|
441
|
+
groupTodos.sort((a, b) => {
|
|
442
|
+
const pa = PRIORITY_ORDER[a.priority] ?? 1;
|
|
443
|
+
const pb = PRIORITY_ORDER[b.priority] ?? 1;
|
|
444
|
+
return pa - pb;
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
const icon = groupName === 'Due Today' ? '🔴'
|
|
448
|
+
: groupName === 'Due This Week' ? '🟡'
|
|
449
|
+
: groupName === 'Due This Month' ? '🟢'
|
|
450
|
+
: '⚪';
|
|
451
|
+
|
|
452
|
+
lines.push(`## ${icon} ${groupName} (${groupTodos.length})`);
|
|
453
|
+
groupTodos.forEach(t => {
|
|
454
|
+
const due = new Date(t.dueDate).toLocaleDateString();
|
|
455
|
+
lines.push(`- **${t.title}** — due ${due} [${t.priority || 'medium'}]`);
|
|
456
|
+
});
|
|
457
|
+
lines.push('');
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return {
|
|
461
|
+
content: [{
|
|
462
|
+
type: 'text',
|
|
463
|
+
text: lines.join('\n'),
|
|
464
|
+
}],
|
|
465
|
+
};
|
|
466
|
+
} catch (err) {
|
|
467
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
);
|