@j0hanz/todokit-mcp 1.3.3 → 1.3.4
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/dist/index.js +33 -4
- package/dist/instructions.md +26 -92
- package/dist/tools.d.ts +1 -0
- package/dist/tools.js +103 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3,13 +3,13 @@ import { readFileSync } from 'node:fs';
|
|
|
3
3
|
import { dirname, resolve } from 'node:path';
|
|
4
4
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
5
5
|
import { parseArgs } from 'node:util';
|
|
6
|
-
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
6
|
+
import { McpServer, ResourceTemplate, } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
7
7
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
8
8
|
import {} from '@modelcontextprotocol/sdk/types.js';
|
|
9
9
|
import packageJson from '../package.json' with { type: 'json' };
|
|
10
10
|
import { enableDefaultDiagnosticsSubscribers, publishLifecycleEvent, } from './diagnostics.js';
|
|
11
11
|
import { closeDb } from './storage.js';
|
|
12
|
-
import { registerAllTools } from './tools.js';
|
|
12
|
+
import { registerAllTools, setInitializationGuard } from './tools.js';
|
|
13
13
|
const LEVEL_RANKS = {
|
|
14
14
|
debug: 10,
|
|
15
15
|
info: 20,
|
|
@@ -96,10 +96,12 @@ const SERVER_VERSION = typeof packageJson.version === 'string' && packageJson.ve
|
|
|
96
96
|
? packageJson.version
|
|
97
97
|
: '0.0.0';
|
|
98
98
|
const DEFAULT_INSTRUCTIONS = 'Todokit to-do list manager';
|
|
99
|
+
function resolveInstructionsPath() {
|
|
100
|
+
return resolve(dirname(fileURLToPath(import.meta.url)), 'instructions.md');
|
|
101
|
+
}
|
|
99
102
|
function loadServerInstructions() {
|
|
100
103
|
try {
|
|
101
|
-
const
|
|
102
|
-
const raw = readFileSync(instructionsPath, { encoding: 'utf8' });
|
|
104
|
+
const raw = readFileSync(resolveInstructionsPath(), { encoding: 'utf8' });
|
|
103
105
|
const trimmed = raw.trim();
|
|
104
106
|
return trimmed.length > 0 ? trimmed : DEFAULT_INSTRUCTIONS;
|
|
105
107
|
}
|
|
@@ -107,6 +109,20 @@ function loadServerInstructions() {
|
|
|
107
109
|
return DEFAULT_INSTRUCTIONS;
|
|
108
110
|
}
|
|
109
111
|
}
|
|
112
|
+
function registerInstructionsResource(server) {
|
|
113
|
+
server.registerResource('internal://instructions', new ResourceTemplate('internal://instructions', { list: undefined }), { title: 'Todokit Instructions', mimeType: 'text/markdown' }, (uri) => {
|
|
114
|
+
const text = loadServerInstructions();
|
|
115
|
+
return {
|
|
116
|
+
contents: [
|
|
117
|
+
{
|
|
118
|
+
uri: uri.href,
|
|
119
|
+
text,
|
|
120
|
+
mimeType: 'text/markdown',
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
};
|
|
124
|
+
});
|
|
125
|
+
}
|
|
110
126
|
let shuttingDown = false;
|
|
111
127
|
let activeServer = null;
|
|
112
128
|
let disableDiagnostics = null;
|
|
@@ -161,6 +177,14 @@ export function createServer() {
|
|
|
161
177
|
instructions: loadServerInstructions(),
|
|
162
178
|
capabilities: { logging: {} },
|
|
163
179
|
});
|
|
180
|
+
registerInstructionsResource(server);
|
|
181
|
+
let initialized = false;
|
|
182
|
+
const previousInitialized = server.server.oninitialized;
|
|
183
|
+
server.server.oninitialized = () => {
|
|
184
|
+
initialized = true;
|
|
185
|
+
previousInitialized?.();
|
|
186
|
+
};
|
|
187
|
+
setInitializationGuard(() => initialized);
|
|
164
188
|
patchToolErrorResponses(server);
|
|
165
189
|
registerAllTools(server);
|
|
166
190
|
return server;
|
|
@@ -235,6 +259,11 @@ if (entrypoint && import.meta.url === pathToFileURL(entrypoint).href) {
|
|
|
235
259
|
disableDiagnostics = enableDefaultDiagnosticsSubscribers({
|
|
236
260
|
logger: (line) => {
|
|
237
261
|
logger.info(line);
|
|
262
|
+
if (activeServer?.isConnected()) {
|
|
263
|
+
void activeServer
|
|
264
|
+
.sendLoggingMessage({ level: 'info', data: { message: line } })
|
|
265
|
+
.catch(() => undefined);
|
|
266
|
+
}
|
|
238
267
|
},
|
|
239
268
|
});
|
|
240
269
|
}
|
package/dist/instructions.md
CHANGED
|
@@ -1,105 +1,39 @@
|
|
|
1
|
-
# Todokit
|
|
1
|
+
# Todokit Instructions
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> **Guidance for the Agent:** These instructions are available as a resource (`internal://instructions`) or prompt (`get-help`). Load them when you are confused about tool usage.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## 1. Core Capability
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
- Operate by `id` (all mutation tools require an exact `id`). If the user doesn't provide an id, list first and then ask which item to act on.
|
|
10
|
-
- Batch-create with `add_todos` when adding multiple items.
|
|
11
|
-
- Treat `delete_todo` as destructive: ask for explicit confirmation unless the user clearly requested deletion.
|
|
12
|
-
- If request is vague, ask clarifying questions.
|
|
7
|
+
- **Domain:** Manage a persistent local todo list stored in JSON.
|
|
8
|
+
- **Primary Resources:** `Todo` items, list `counts`, `summary`, and `hint` metadata.
|
|
13
9
|
|
|
14
|
-
|
|
10
|
+
## 2. The "Golden Path" Workflows (Critical)
|
|
15
11
|
|
|
16
|
-
|
|
17
|
-
- **Action:** Chain tools efficiently: `list` → confirm ID → `update`/`complete`. Use `add_todos` for multiple items.
|
|
12
|
+
### Workflow A: Daily Triage
|
|
18
13
|
|
|
19
|
-
|
|
14
|
+
1. Call `list_todos` (default `status='pending'`).
|
|
15
|
+
2. Call `add_todo` (single) or `add_todos` (batch) to capture new tasks.
|
|
16
|
+
3. Call `update_todo` or `complete_todo` to keep the list current.
|
|
20
17
|
|
|
21
|
-
|
|
18
|
+
> **Constraint:** Never guess IDs. Always list first.
|
|
22
19
|
|
|
23
|
-
|
|
20
|
+
### Workflow B: Cleanup & Verification
|
|
24
21
|
|
|
25
|
-
|
|
22
|
+
1. Call `list_todos` with `status='completed'` to review finished items.
|
|
23
|
+
2. Call `delete_todo` only when the user explicitly wants removal.
|
|
24
|
+
3. Call `list_todos` again to confirm remaining items.
|
|
26
25
|
|
|
27
|
-
|
|
28
|
-
list_todos(status='pending') → See what is open
|
|
29
|
-
add_todos(...) → Add new items in bulk
|
|
30
|
-
complete_todo(id=...) → Mark finished items
|
|
31
|
-
```
|
|
26
|
+
## 3. Tool Nuances & "Gotchas"
|
|
32
27
|
|
|
33
|
-
|
|
28
|
+
- **`list_todos`**: Defaults to `status='pending'` and returns max 50 items; use `status='all'` to include completed items.
|
|
29
|
+
- **`add_todos`**: Prefer for 2+ items to reduce calls.
|
|
30
|
+
- **`update_todo`**: Requires at least one field; otherwise returns `E_BAD_REQUEST`.
|
|
31
|
+
- **`delete_todo`**: Destructive and non-idempotent—confirm intent first.
|
|
32
|
+
- **Storage behavior**: The JSON file is auto-deleted when all todos are completed.
|
|
34
33
|
|
|
35
|
-
|
|
34
|
+
## 4. Error Handling Strategy
|
|
36
35
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
-
|
|
40
|
-
-
|
|
41
|
-
- **Returns:** `{ item, summary, nextActions }`
|
|
42
|
-
|
|
43
|
-
### add_todos
|
|
44
|
-
|
|
45
|
-
Create multiple tasks in one call.
|
|
46
|
-
|
|
47
|
-
- **Use when:** User provides 2+ tasks or a list.
|
|
48
|
-
- **Args:** `items` (array, 1-50 items).
|
|
49
|
-
- **Returns:** `{ items, summary, nextActions }`
|
|
50
|
-
|
|
51
|
-
### list_todos
|
|
52
|
-
|
|
53
|
-
List todos with an optional status filter.
|
|
54
|
-
|
|
55
|
-
- **Use when:** Checking potential duplicates, finding IDs, or reviewing workload.
|
|
56
|
-
- **Args:** `status` (pending|completed|all, default: pending).
|
|
57
|
-
- **Returns:** `{ items, summary, counts, truncated, hint }`
|
|
58
|
-
|
|
59
|
-
### update_todo
|
|
60
|
-
|
|
61
|
-
Update fields on a todo item.
|
|
62
|
-
|
|
63
|
-
- **Use when:** Renaming, rescheduling, or changing priority/category.
|
|
64
|
-
- **Args:** `id` (req), `description`, `priority`, `category`, `dueAt`.
|
|
65
|
-
- **Returns:** `{ item, summary, nextActions }`
|
|
66
|
-
|
|
67
|
-
### complete_todo
|
|
68
|
-
|
|
69
|
-
Mark a todo as completed.
|
|
70
|
-
|
|
71
|
-
- **Use when:** Task is done.
|
|
72
|
-
- **Args:** `id` (req).
|
|
73
|
-
- **Returns:** `{ item, summary, nextActions }`
|
|
74
|
-
|
|
75
|
-
### delete_todo
|
|
76
|
-
|
|
77
|
-
Delete a todo item by ID.
|
|
78
|
-
|
|
79
|
-
- **Use when:** Removing mistakes or duplicates (prefer completion for finished work).
|
|
80
|
-
- **Args:** `id` (req).
|
|
81
|
-
- **Returns:** `{ deletedIds, summary, nextActions }`
|
|
82
|
-
|
|
83
|
-
## Response Shape
|
|
84
|
-
|
|
85
|
-
Success: `{ "ok": true, "result": { ... } }`
|
|
86
|
-
Error: `{ "ok": false, "error": { "code": "...", "message": "..." } }`
|
|
87
|
-
|
|
88
|
-
### Common Errors
|
|
89
|
-
|
|
90
|
-
| Code | Meaning | Resolution |
|
|
91
|
-
| --------------------- | ------------------------- | ----------------------------------------- |
|
|
92
|
-
| `E_NOT_FOUND` | Todo ID does not exist | List todos to find correct ID |
|
|
93
|
-
| `E_INVALID_PARAMS` | Schema validation failed | Check enums (priority/category) and types |
|
|
94
|
-
| `E_STORAGE_CONFLICT` | File changed during write | Retry the operation |
|
|
95
|
-
| `E_STORAGE_TOO_LARGE` | File exceeds 5MB | Clean up old todos |
|
|
96
|
-
|
|
97
|
-
## Limits
|
|
98
|
-
|
|
99
|
-
- **Pagination:** `list_todos` returns max 50 items.
|
|
100
|
-
- **Batch Size:** `add_todos` accepts max 50 items.
|
|
101
|
-
- **Storage:** File is automatically deleted when all tasks are completed.
|
|
102
|
-
|
|
103
|
-
## Security
|
|
104
|
-
|
|
105
|
-
- This server writes to a local JSON file (`todos.json` by default). Do not store sensitive credentials or PII in todo descriptions.
|
|
36
|
+
- **`E_NOT_FOUND`**: Call `list_todos` with the right `status` to locate the ID.
|
|
37
|
+
- **`E_INVALID_PARAMS`**: Fix enum values or ISO-8601 date formats.
|
|
38
|
+
- **`E_STORAGE_CONFLICT`**: Re-list then retry the mutation once.
|
|
39
|
+
- **`E_STORAGE_TOO_LARGE`**: Complete/delete old items to reduce size.
|
package/dist/tools.d.ts
CHANGED
package/dist/tools.js
CHANGED
|
@@ -4,6 +4,81 @@ import { runWithRequestContext } from './requestContext.js';
|
|
|
4
4
|
import { createErrorResponse, createToolResponse, getErrorMessage, } from './responses.js';
|
|
5
5
|
import { AddTodoSchema, AddTodosSchema, CompleteTodoSchema, DefaultOutputSchema, DeleteTodoSchema, ListTodosFilterSchema, UpdateTodoSchema, } from './schema.js';
|
|
6
6
|
import { addTodos, completeTodoById, deleteTodoById, getCodedErrorCode, getTodos, updateTodoById, } from './storage.js';
|
|
7
|
+
const DEFAULT_TOOL_TIMEOUT_MS = 60_000;
|
|
8
|
+
const TOOL_ABORT_ERROR_NAME = 'TodokitToolAbort';
|
|
9
|
+
const TOOL_TIMEOUT_ERROR_NAME = 'TodokitToolTimeout';
|
|
10
|
+
let isInitialized = () => true;
|
|
11
|
+
export function setInitializationGuard(fn) {
|
|
12
|
+
isInitialized = fn;
|
|
13
|
+
}
|
|
14
|
+
function getToolTimeoutMs() {
|
|
15
|
+
const raw = process.env.TODOKIT_TOOL_TIMEOUT_MS?.trim();
|
|
16
|
+
if (!raw)
|
|
17
|
+
return DEFAULT_TOOL_TIMEOUT_MS;
|
|
18
|
+
const value = Number(raw);
|
|
19
|
+
if (!Number.isFinite(value) || !Number.isInteger(value)) {
|
|
20
|
+
return DEFAULT_TOOL_TIMEOUT_MS;
|
|
21
|
+
}
|
|
22
|
+
if (value <= 0)
|
|
23
|
+
return null;
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
function isDefined(value) {
|
|
27
|
+
return value !== null && value !== undefined;
|
|
28
|
+
}
|
|
29
|
+
function createAbortPromise(signal) {
|
|
30
|
+
if (!signal)
|
|
31
|
+
return null;
|
|
32
|
+
if (signal.aborted) {
|
|
33
|
+
const error = new Error('Tool cancelled');
|
|
34
|
+
error.name = TOOL_ABORT_ERROR_NAME;
|
|
35
|
+
return { promise: Promise.reject(error), cancel: () => undefined };
|
|
36
|
+
}
|
|
37
|
+
let listener = null;
|
|
38
|
+
const promise = new Promise((_, reject) => {
|
|
39
|
+
listener = () => {
|
|
40
|
+
const error = new Error('Tool cancelled');
|
|
41
|
+
error.name = TOOL_ABORT_ERROR_NAME;
|
|
42
|
+
reject(error);
|
|
43
|
+
};
|
|
44
|
+
signal.addEventListener('abort', listener, { once: true });
|
|
45
|
+
});
|
|
46
|
+
return {
|
|
47
|
+
promise,
|
|
48
|
+
cancel: () => {
|
|
49
|
+
if (listener) {
|
|
50
|
+
signal.removeEventListener('abort', listener);
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function createTimeoutPromise(ms, message) {
|
|
56
|
+
let timeoutId;
|
|
57
|
+
const promise = new Promise((_, reject) => {
|
|
58
|
+
timeoutId = setTimeout(() => {
|
|
59
|
+
const error = new Error(message);
|
|
60
|
+
error.name = TOOL_TIMEOUT_ERROR_NAME;
|
|
61
|
+
reject(error);
|
|
62
|
+
}, ms);
|
|
63
|
+
});
|
|
64
|
+
return {
|
|
65
|
+
promise,
|
|
66
|
+
cancel: () => {
|
|
67
|
+
if (timeoutId !== undefined) {
|
|
68
|
+
clearTimeout(timeoutId);
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function classifyInterruption(error) {
|
|
74
|
+
if (!(error instanceof Error))
|
|
75
|
+
return null;
|
|
76
|
+
if (error.name === TOOL_ABORT_ERROR_NAME)
|
|
77
|
+
return 'cancelled';
|
|
78
|
+
if (error.name === TOOL_TIMEOUT_ERROR_NAME)
|
|
79
|
+
return 'timeout';
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
7
82
|
function mapExecutionError(error, fallbackCode) {
|
|
8
83
|
const coded = getCodedErrorCode(error);
|
|
9
84
|
if (coded) {
|
|
@@ -56,6 +131,16 @@ function createWrappedHandler(tool, handler) {
|
|
|
56
131
|
const requestId = randomUUID();
|
|
57
132
|
publishToolCallWithId(tool, input, requestId);
|
|
58
133
|
const start = nowMs();
|
|
134
|
+
if (!isInitialized()) {
|
|
135
|
+
const response = createErrorResponse('E_NOT_INITIALIZED', 'Server not initialized');
|
|
136
|
+
publishSuccessResult(tool, requestId, start, response);
|
|
137
|
+
return Promise.resolve(response);
|
|
138
|
+
}
|
|
139
|
+
if (extra.signal.aborted) {
|
|
140
|
+
const response = createErrorResponse('E_CANCELLED', 'Tool cancelled');
|
|
141
|
+
publishSuccessResult(tool, requestId, start, response);
|
|
142
|
+
return Promise.resolve(response);
|
|
143
|
+
}
|
|
59
144
|
let result;
|
|
60
145
|
try {
|
|
61
146
|
result = runWithRequestContext({ requestId, tool }, () => handler(input, extra));
|
|
@@ -65,12 +150,29 @@ function createWrappedHandler(tool, handler) {
|
|
|
65
150
|
const rejection = error instanceof Error ? error : new Error(String(error));
|
|
66
151
|
return Promise.reject(rejection);
|
|
67
152
|
}
|
|
68
|
-
|
|
153
|
+
const timeoutMs = getToolTimeoutMs();
|
|
154
|
+
const timeout = timeoutMs
|
|
155
|
+
? createTimeoutPromise(timeoutMs, `Tool ${tool} timed out`)
|
|
156
|
+
: null;
|
|
157
|
+
const abort = createAbortPromise(extra.signal);
|
|
158
|
+
const race = Promise.race([Promise.resolve(result), timeout?.promise, abort?.promise].filter(isDefined));
|
|
159
|
+
return race
|
|
160
|
+
.finally(() => {
|
|
161
|
+
timeout?.cancel();
|
|
162
|
+
abort?.cancel();
|
|
163
|
+
})
|
|
69
164
|
.then((resolved) => {
|
|
70
165
|
publishSuccessResult(tool, requestId, start, resolved);
|
|
71
166
|
return resolved;
|
|
72
167
|
})
|
|
73
168
|
.catch((error) => {
|
|
169
|
+
const interruption = classifyInterruption(error);
|
|
170
|
+
if (interruption) {
|
|
171
|
+
const code = interruption === 'timeout' ? 'E_TIMEOUT' : 'E_CANCELLED';
|
|
172
|
+
const response = createErrorResponse(code, error instanceof Error ? error.message : String(error));
|
|
173
|
+
publishSuccessResult(tool, requestId, start, response);
|
|
174
|
+
return response;
|
|
175
|
+
}
|
|
74
176
|
publishFailureResult(tool, requestId, start);
|
|
75
177
|
throw error;
|
|
76
178
|
});
|
package/package.json
CHANGED