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 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
@@ -0,0 +1,6 @@
1
+ import 'dotenv/config';
2
+ import { createApp } from '../src/app.js';
3
+
4
+ const app = createApp();
5
+
6
+ export default app;
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
+ );