@j0hanz/todokit-mcp 1.1.0 → 1.2.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/README.md +2 -2
- package/dist/diagnostics.d.ts +2 -0
- package/dist/diagnostics.js +10 -0
- package/dist/index.js +34 -28
- package/dist/requestContext.d.ts +6 -0
- package/dist/requestContext.js +8 -0
- package/dist/schema.js +1 -1
- package/dist/storage.d.ts +1 -0
- package/dist/storage.js +127 -25
- package/dist/tools.d.ts +1 -1
- package/dist/tools.js +43 -16
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -64,7 +64,7 @@ npm start
|
|
|
64
64
|
|
|
65
65
|
### Storage path
|
|
66
66
|
|
|
67
|
-
By default, todos are stored in `todos.json`
|
|
67
|
+
By default, todos are stored in `todos.json` in the current working directory. To control where data is written, set the `TODOKIT_TODO_FILE` environment variable to an absolute or relative path ending with `.json`. Relative paths resolve from the current working directory. The directory is created as needed; if the file does not exist, the server starts with an empty list.
|
|
68
68
|
|
|
69
69
|
Examples:
|
|
70
70
|
|
|
@@ -227,7 +227,7 @@ Result fields:
|
|
|
227
227
|
- `summary`
|
|
228
228
|
- `nextActions`
|
|
229
229
|
|
|
230
|
-
###
|
|
230
|
+
### clear_todos
|
|
231
231
|
|
|
232
232
|
Delete all todos from the list.
|
|
233
233
|
|
package/dist/diagnostics.d.ts
CHANGED
package/dist/diagnostics.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { channel, subscribe, unsubscribe, } from 'node:diagnostics_channel';
|
|
2
2
|
import { performance } from 'node:perf_hooks';
|
|
3
|
+
import { getRequestContext } from './requestContext.js';
|
|
3
4
|
const toolDiagnosticsChannel = channel('todokit:tool');
|
|
4
5
|
const storageDiagnosticsChannel = channel('todokit:storage');
|
|
5
6
|
const lifecycleDiagnosticsChannel = channel('todokit:lifecycle');
|
|
@@ -54,6 +55,15 @@ export function publishToolResult(event) {
|
|
|
54
55
|
safePublish(toolDiagnosticsChannel, event);
|
|
55
56
|
}
|
|
56
57
|
export function publishStorageEvent(event) {
|
|
58
|
+
const context = getRequestContext();
|
|
59
|
+
if (context) {
|
|
60
|
+
safePublish(storageDiagnosticsChannel, {
|
|
61
|
+
...event,
|
|
62
|
+
requestId: event.requestId ?? context.requestId,
|
|
63
|
+
tool: event.tool ?? context.tool,
|
|
64
|
+
});
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
57
67
|
safePublish(storageDiagnosticsChannel, event);
|
|
58
68
|
}
|
|
59
69
|
export function publishLifecycleEvent(event) {
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { pathToFileURL } from 'node:url';
|
|
|
3
3
|
import { parseArgs } from 'node:util';
|
|
4
4
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
5
5
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
6
|
-
import {
|
|
6
|
+
import {} from '@modelcontextprotocol/sdk/types.js';
|
|
7
7
|
import packageJson from '../package.json' with { type: 'json' };
|
|
8
8
|
import { enableDefaultDiagnosticsSubscribers, publishLifecycleEvent, } from './diagnostics.js';
|
|
9
9
|
import { closeDb } from './storage.js';
|
|
@@ -99,9 +99,13 @@ let disableDiagnostics = null;
|
|
|
99
99
|
function mapToolErrorCode(message) {
|
|
100
100
|
if (message.startsWith('Input validation error'))
|
|
101
101
|
return 'E_INVALID_PARAMS';
|
|
102
|
-
if (message.
|
|
102
|
+
if (message.startsWith('Invalid tools/call request'))
|
|
103
|
+
return 'E_INVALID_PARAMS';
|
|
104
|
+
if (message.startsWith('Invalid task creation result'))
|
|
105
|
+
return 'E_OUTPUT_INVALID';
|
|
106
|
+
if (message.startsWith('Tool ') && message.includes(' not found'))
|
|
103
107
|
return 'E_TOOL_NOT_FOUND';
|
|
104
|
-
if (message.includes('disabled'))
|
|
108
|
+
if (message.startsWith('Tool ') && message.includes(' disabled'))
|
|
105
109
|
return 'E_TOOL_DISABLED';
|
|
106
110
|
if (message.startsWith('Output validation error'))
|
|
107
111
|
return 'E_OUTPUT_INVALID';
|
|
@@ -109,38 +113,40 @@ function mapToolErrorCode(message) {
|
|
|
109
113
|
}
|
|
110
114
|
function patchToolErrorResponses(server) {
|
|
111
115
|
const target = server;
|
|
112
|
-
target.createToolError
|
|
113
|
-
const structured = {
|
|
114
|
-
ok: false,
|
|
115
|
-
error: { code: mapToolErrorCode(message), message },
|
|
116
|
-
};
|
|
117
|
-
return {
|
|
118
|
-
content: [{ type: 'text', text: JSON.stringify(structured) }],
|
|
119
|
-
structuredContent: structured,
|
|
120
|
-
isError: true,
|
|
121
|
-
};
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
function enforceProtocolVersion(server) {
|
|
125
|
-
const internalServer = server.server;
|
|
126
|
-
const onInitialize = internalServer._oninitialize;
|
|
127
|
-
if (!onInitialize)
|
|
116
|
+
if (typeof target.createToolError !== 'function') {
|
|
128
117
|
return;
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (
|
|
133
|
-
|
|
118
|
+
}
|
|
119
|
+
const descriptor = Object.getOwnPropertyDescriptor(target, 'createToolError');
|
|
120
|
+
if (descriptor) {
|
|
121
|
+
if (descriptor.writable === false && descriptor.set === undefined) {
|
|
122
|
+
return;
|
|
134
123
|
}
|
|
135
|
-
|
|
136
|
-
|
|
124
|
+
}
|
|
125
|
+
if (!Object.isExtensible(target)) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
target.createToolError = (message) => {
|
|
130
|
+
const structured = {
|
|
131
|
+
ok: false,
|
|
132
|
+
error: { code: mapToolErrorCode(message), message },
|
|
133
|
+
};
|
|
134
|
+
return {
|
|
135
|
+
content: [{ type: 'text', text: JSON.stringify(structured) }],
|
|
136
|
+
structuredContent: structured,
|
|
137
|
+
isError: true,
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// Best-effort override; fall back to SDK defaults if not writable.
|
|
143
|
+
}
|
|
137
144
|
}
|
|
138
145
|
export function createServer() {
|
|
139
146
|
const server = new McpServer({ name: 'todokit', version: SERVER_VERSION }, {
|
|
140
147
|
instructions: 'Todokit to-do list manager',
|
|
141
148
|
capabilities: { logging: {} },
|
|
142
149
|
});
|
|
143
|
-
enforceProtocolVersion(server);
|
|
144
150
|
patchToolErrorResponses(server);
|
|
145
151
|
registerAllTools(server);
|
|
146
152
|
return server;
|
|
@@ -214,7 +220,7 @@ if (entrypoint && import.meta.url === pathToFileURL(entrypoint).href) {
|
|
|
214
220
|
const logger = createStderrLogger(cli.logLevel);
|
|
215
221
|
disableDiagnostics = enableDefaultDiagnosticsSubscribers({
|
|
216
222
|
logger: (line) => {
|
|
217
|
-
logger.
|
|
223
|
+
logger.info(line);
|
|
218
224
|
},
|
|
219
225
|
});
|
|
220
226
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
const requestContextStorage = new AsyncLocalStorage();
|
|
3
|
+
export function runWithRequestContext(context, fn) {
|
|
4
|
+
return requestContextStorage.run(context, fn);
|
|
5
|
+
}
|
|
6
|
+
export function getRequestContext() {
|
|
7
|
+
return requestContextStorage.getStore();
|
|
8
|
+
}
|
package/dist/schema.js
CHANGED
|
@@ -80,5 +80,5 @@ export const ListTodosFilterSchema = z.strictObject({
|
|
|
80
80
|
export const DefaultOutputSchema = z.strictObject({
|
|
81
81
|
ok: z.boolean(),
|
|
82
82
|
result: z.unknown().optional(),
|
|
83
|
-
error: z.
|
|
83
|
+
error: z.strictObject({ code: z.string(), message: z.string() }).optional(),
|
|
84
84
|
});
|
package/dist/storage.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export declare function isAbortError(error: unknown): boolean;
|
|
|
5
5
|
export declare function getFileMtime(path: string, timeoutMs: number): Promise<number | null>;
|
|
6
6
|
export declare function readFileIfExists(path: string, timeoutMs: number, signal?: AbortSignal): Promise<string | null>;
|
|
7
7
|
export declare function writeFileAtomic(path: string, contents: string, timeoutMs: number): Promise<number>;
|
|
8
|
+
export declare function getCodedErrorCode(error: unknown): string | undefined;
|
|
8
9
|
export declare function readTodos(): Promise<readonly Todo[]>;
|
|
9
10
|
export declare function withTodos<T>(mutate: (todos: Todo[]) => {
|
|
10
11
|
todos: Todo[];
|
package/dist/storage.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { mkdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
3
|
-
import { dirname,
|
|
4
|
-
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { dirname, resolve } from 'node:path';
|
|
5
4
|
import { nowMs, publishStorageEvent } from './diagnostics.js';
|
|
6
5
|
import { createErrorResponse } from './responses.js';
|
|
7
6
|
import { TodosSchema } from './schema.js';
|
|
@@ -60,6 +59,17 @@ export async function getFileMtime(path, timeoutMs) {
|
|
|
60
59
|
throw error;
|
|
61
60
|
}
|
|
62
61
|
}
|
|
62
|
+
async function getFileSize(path, timeoutMs) {
|
|
63
|
+
try {
|
|
64
|
+
const stats = await withTimeout(stat(path), timeoutMs, 'File stat timed out');
|
|
65
|
+
return stats.size;
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
if (isNotFoundError(error))
|
|
69
|
+
return null;
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
63
73
|
export async function readFileIfExists(path, timeoutMs, signal) {
|
|
64
74
|
try {
|
|
65
75
|
return await readFile(path, {
|
|
@@ -121,10 +131,42 @@ export async function writeFileAtomic(path, contents, timeoutMs) {
|
|
|
121
131
|
await rm(tempPath, { force: true }).catch(() => undefined);
|
|
122
132
|
}
|
|
123
133
|
}
|
|
124
|
-
const
|
|
125
|
-
const DEFAULT_TODO_FILE = join(moduleDir, '../todos.json');
|
|
134
|
+
const DEFAULT_TODO_FILE = resolve(process.cwd(), 'todos.json');
|
|
126
135
|
const IO_TIMEOUT_MS = 10_000;
|
|
127
136
|
const WRITE_TIMEOUT_MS = 30_000;
|
|
137
|
+
const LOCK_TIMEOUT_MS = 5_000;
|
|
138
|
+
const DEFAULT_MAX_TODO_FILE_BYTES = 5 * 1024 * 1024;
|
|
139
|
+
const MAX_CONFLICT_RETRIES = 3;
|
|
140
|
+
class StorageError extends Error {
|
|
141
|
+
code;
|
|
142
|
+
constructor(code, message) {
|
|
143
|
+
super(message);
|
|
144
|
+
this.name = 'StorageError';
|
|
145
|
+
this.code = code;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function createCodedError(code, message) {
|
|
149
|
+
return new StorageError(code, message);
|
|
150
|
+
}
|
|
151
|
+
export function getCodedErrorCode(error) {
|
|
152
|
+
return error instanceof StorageError ? error.code : undefined;
|
|
153
|
+
}
|
|
154
|
+
function getEnvInt(name) {
|
|
155
|
+
const raw = process.env[name]?.trim();
|
|
156
|
+
if (!raw)
|
|
157
|
+
return null;
|
|
158
|
+
const value = Number(raw);
|
|
159
|
+
if (!Number.isFinite(value) || !Number.isInteger(value) || value < 0) {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
return value;
|
|
163
|
+
}
|
|
164
|
+
function getLockTimeoutMs() {
|
|
165
|
+
return getEnvInt('TODOKIT_LOCK_TIMEOUT_MS') ?? LOCK_TIMEOUT_MS;
|
|
166
|
+
}
|
|
167
|
+
function getMaxTodoFileBytes() {
|
|
168
|
+
return (getEnvInt('TODOKIT_MAX_TODO_FILE_BYTES') ?? DEFAULT_MAX_TODO_FILE_BYTES);
|
|
169
|
+
}
|
|
128
170
|
function getJsonIndentation() {
|
|
129
171
|
const raw = process.env.TODOKIT_JSON_PRETTY?.trim().toLowerCase();
|
|
130
172
|
if (!raw)
|
|
@@ -147,7 +189,38 @@ function enqueueWrite(task) {
|
|
|
147
189
|
writeQueue = run.then(() => undefined, () => undefined);
|
|
148
190
|
return run;
|
|
149
191
|
}
|
|
192
|
+
async function acquireWriteLock(todoPath, timeoutMs) {
|
|
193
|
+
const lockPath = `${todoPath}.lock`;
|
|
194
|
+
const started = nowMs();
|
|
195
|
+
await mkdir(dirname(todoPath), { recursive: true });
|
|
196
|
+
for (;;) {
|
|
197
|
+
try {
|
|
198
|
+
await writeFile(lockPath, `${String(process.pid)} ${new Date().toISOString()}\n`, { encoding: 'utf8', flag: 'wx' });
|
|
199
|
+
return async () => {
|
|
200
|
+
await rm(lockPath, { force: true }).catch(() => undefined);
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
const code = getErrorCode(error);
|
|
205
|
+
if (code !== 'EEXIST') {
|
|
206
|
+
throw error;
|
|
207
|
+
}
|
|
208
|
+
const elapsedMs = Math.max(0, nowMs() - started);
|
|
209
|
+
if (elapsedMs >= timeoutMs) {
|
|
210
|
+
throw createCodedError('E_STORAGE_LOCK_TIMEOUT', 'Todo storage is busy; please retry.');
|
|
211
|
+
}
|
|
212
|
+
await delay(25);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
150
216
|
async function loadTodos(path) {
|
|
217
|
+
const size = await getFileSize(path, IO_TIMEOUT_MS);
|
|
218
|
+
if (size !== null) {
|
|
219
|
+
const maxBytes = getMaxTodoFileBytes();
|
|
220
|
+
if (size > maxBytes) {
|
|
221
|
+
throw createCodedError('E_STORAGE_TOO_LARGE', `Todo storage file is too large (${String(size)} bytes; max ${String(maxBytes)}).`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
151
224
|
const raw = await readFileIfExists(path, IO_TIMEOUT_MS);
|
|
152
225
|
if (!raw)
|
|
153
226
|
return [];
|
|
@@ -207,14 +280,31 @@ export async function readTodos() {
|
|
|
207
280
|
export async function withTodos(mutate) {
|
|
208
281
|
return enqueueWrite(async () => {
|
|
209
282
|
const path = getTodoFilePath();
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
283
|
+
for (let attempt = 0; attempt <= MAX_CONFLICT_RETRIES; attempt += 1) {
|
|
284
|
+
const mtimeMs = await getFileMtime(path, IO_TIMEOUT_MS);
|
|
285
|
+
const current = cache?.mtimeMs === mtimeMs ? cache.todos : await loadTodos(path);
|
|
286
|
+
cache = { todos: current, mtimeMs };
|
|
287
|
+
const { todos, result } = mutate(current);
|
|
288
|
+
if (todos !== current) {
|
|
289
|
+
const release = await acquireWriteLock(path, getLockTimeoutMs());
|
|
290
|
+
try {
|
|
291
|
+
const latestMtime = await getFileMtime(path, IO_TIMEOUT_MS);
|
|
292
|
+
if (latestMtime !== mtimeMs) {
|
|
293
|
+
if (attempt >= MAX_CONFLICT_RETRIES) {
|
|
294
|
+
throw createCodedError('E_STORAGE_CONFLICT', 'Todo storage changed during update; please retry.');
|
|
295
|
+
}
|
|
296
|
+
await delay(25 * (attempt + 1));
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
await saveTodos(path, todos);
|
|
300
|
+
}
|
|
301
|
+
finally {
|
|
302
|
+
await release();
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return result;
|
|
216
306
|
}
|
|
217
|
-
|
|
307
|
+
throw createCodedError('E_STORAGE_CONFLICT', 'Todo storage update failed due to concurrent modifications');
|
|
218
308
|
});
|
|
219
309
|
}
|
|
220
310
|
export async function closeDb() {
|
|
@@ -230,17 +320,23 @@ export async function closeDb() {
|
|
|
230
320
|
durationMs: Math.max(0, nowMs() - start),
|
|
231
321
|
});
|
|
232
322
|
}
|
|
233
|
-
function matchesCompleted(todo, completed) {
|
|
234
|
-
return completed === undefined || todo.completed === completed;
|
|
235
|
-
}
|
|
236
|
-
function matchesQuery(todo, query) {
|
|
237
|
-
if (!query)
|
|
238
|
-
return true;
|
|
239
|
-
return todo.description.toLowerCase().includes(query);
|
|
240
|
-
}
|
|
241
323
|
export function filterTodos(todos, filters) {
|
|
242
|
-
const query = filters
|
|
243
|
-
|
|
324
|
+
const { completed, query: rawQuery } = filters;
|
|
325
|
+
const hasCompletedFilter = completed !== undefined;
|
|
326
|
+
const trimmedQuery = rawQuery?.trim();
|
|
327
|
+
const query = trimmedQuery?.toLowerCase();
|
|
328
|
+
if (!hasCompletedFilter && !query) {
|
|
329
|
+
return todos;
|
|
330
|
+
}
|
|
331
|
+
const matches = [];
|
|
332
|
+
for (const todo of todos) {
|
|
333
|
+
if (hasCompletedFilter && todo.completed !== completed)
|
|
334
|
+
continue;
|
|
335
|
+
if (query && !todo.description.toLowerCase().includes(query))
|
|
336
|
+
continue;
|
|
337
|
+
matches.push(todo);
|
|
338
|
+
}
|
|
339
|
+
return matches;
|
|
244
340
|
}
|
|
245
341
|
const PREVIEW_LIMIT = 5;
|
|
246
342
|
function buildMatchPreviews(todos, limit = PREVIEW_LIMIT) {
|
|
@@ -455,10 +551,16 @@ export async function completeTodoBySelector(input, completed) {
|
|
|
455
551
|
export function deleteTodosByIds(ids) {
|
|
456
552
|
const idsToDelete = new Set(ids);
|
|
457
553
|
return withTodos((todos) => {
|
|
458
|
-
const remaining =
|
|
459
|
-
const deletedIds =
|
|
460
|
-
|
|
461
|
-
.
|
|
554
|
+
const remaining = [];
|
|
555
|
+
const deletedIds = [];
|
|
556
|
+
for (const todo of todos) {
|
|
557
|
+
if (idsToDelete.has(todo.id)) {
|
|
558
|
+
deletedIds.push(todo.id);
|
|
559
|
+
}
|
|
560
|
+
else {
|
|
561
|
+
remaining.push(todo);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
462
564
|
return { todos: remaining, result: deletedIds };
|
|
463
565
|
});
|
|
464
566
|
}
|
package/dist/tools.d.ts
CHANGED
|
@@ -16,6 +16,6 @@ export declare function registerListTodos(server: McpServer): void;
|
|
|
16
16
|
export declare function registerUpdateTodo(server: McpServer): void;
|
|
17
17
|
export declare function registerCompleteTodo(server: McpServer): void;
|
|
18
18
|
export declare function registerDeleteTodo(server: McpServer): void;
|
|
19
|
-
export declare function
|
|
19
|
+
export declare function registerClearTodos(server: McpServer): void;
|
|
20
20
|
export declare function registerAllTools(server: McpServer): void;
|
|
21
21
|
export {};
|
package/dist/tools.js
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { nowMs, publishToolCallWithId, publishToolResult, } from './diagnostics.js';
|
|
4
|
+
import { runWithRequestContext } from './requestContext.js';
|
|
4
5
|
import { createErrorResponse, createToolResponse, getErrorMessage, } from './responses.js';
|
|
5
6
|
import { AddTodoSchema, AddTodosSchema, CompleteTodoSchema, DefaultOutputSchema, DeleteTodoSchema, ListTodosFilterSchema, UpdateTodoSchema, } from './schema.js';
|
|
6
|
-
import { addTodos, completeTodoBySelector, deleteAllTodos, deleteTodoBySelector, getTodos, toResolveInput, updateTodoBySelector, } from './storage.js';
|
|
7
|
+
import { addTodos, completeTodoBySelector, deleteAllTodos, deleteTodoBySelector, getCodedErrorCode, getTodos, toResolveInput, updateTodoBySelector, } from './storage.js';
|
|
8
|
+
function mapExecutionError(error, fallbackCode) {
|
|
9
|
+
const coded = getCodedErrorCode(error);
|
|
10
|
+
if (coded) {
|
|
11
|
+
return { code: coded, message: getErrorMessage(error) };
|
|
12
|
+
}
|
|
13
|
+
return { code: fallbackCode, message: getErrorMessage(error) };
|
|
14
|
+
}
|
|
7
15
|
function isRecord(value) {
|
|
8
16
|
return typeof value === 'object' && value !== null;
|
|
9
17
|
}
|
|
@@ -49,7 +57,15 @@ function createWrappedHandler(tool, handler) {
|
|
|
49
57
|
const requestId = randomUUID();
|
|
50
58
|
publishToolCallWithId(tool, input, requestId);
|
|
51
59
|
const start = nowMs();
|
|
52
|
-
|
|
60
|
+
let result;
|
|
61
|
+
try {
|
|
62
|
+
result = runWithRequestContext({ requestId, tool }, () => handler(input, extra));
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
publishFailureResult(tool, requestId, start);
|
|
66
|
+
const rejection = error instanceof Error ? error : new Error(String(error));
|
|
67
|
+
return Promise.reject(rejection);
|
|
68
|
+
}
|
|
53
69
|
return Promise.resolve(result)
|
|
54
70
|
.then((resolved) => {
|
|
55
71
|
publishSuccessResult(tool, requestId, start, resolved);
|
|
@@ -114,7 +130,8 @@ async function handleAddTodo(input) {
|
|
|
114
130
|
return buildAddTodoResponse(requireSingleTodo(todos));
|
|
115
131
|
}
|
|
116
132
|
catch (err) {
|
|
117
|
-
|
|
133
|
+
const mapped = mapExecutionError(err, 'E_ADD_TODO');
|
|
134
|
+
return createErrorResponse(mapped.code, mapped.message);
|
|
118
135
|
}
|
|
119
136
|
}
|
|
120
137
|
export function registerAddTodo(server) {
|
|
@@ -136,14 +153,19 @@ async function handleAddTodos(input) {
|
|
|
136
153
|
return buildAddTodosResponse(todos);
|
|
137
154
|
}
|
|
138
155
|
catch (err) {
|
|
139
|
-
|
|
156
|
+
const mapped = mapExecutionError(err, 'E_ADD_TODOS');
|
|
157
|
+
return createErrorResponse(mapped.code, mapped.message);
|
|
140
158
|
}
|
|
141
159
|
}
|
|
142
160
|
export function registerAddTodos(server) {
|
|
143
161
|
registerToolWithDiagnostics(server, 'add_todos', addTodosToolConfig, handleAddTodos);
|
|
144
162
|
}
|
|
145
163
|
function computeCounts(todos) {
|
|
146
|
-
|
|
164
|
+
let completed = 0;
|
|
165
|
+
for (const todo of todos) {
|
|
166
|
+
if (todo.completed)
|
|
167
|
+
completed += 1;
|
|
168
|
+
}
|
|
147
169
|
return {
|
|
148
170
|
total: todos.length,
|
|
149
171
|
completed,
|
|
@@ -198,7 +220,8 @@ export function registerListTodos(server) {
|
|
|
198
220
|
return await handleListTodos(filters);
|
|
199
221
|
}
|
|
200
222
|
catch (err) {
|
|
201
|
-
|
|
223
|
+
const mapped = mapExecutionError(err, 'E_LIST_TODOS');
|
|
224
|
+
return createErrorResponse(mapped.code, mapped.message);
|
|
202
225
|
}
|
|
203
226
|
});
|
|
204
227
|
}
|
|
@@ -240,7 +263,8 @@ export function registerUpdateTodo(server) {
|
|
|
240
263
|
return await handleUpdateTodo(input);
|
|
241
264
|
}
|
|
242
265
|
catch (err) {
|
|
243
|
-
|
|
266
|
+
const mapped = mapExecutionError(err, 'E_UPDATE_TODO');
|
|
267
|
+
return createErrorResponse(mapped.code, mapped.message);
|
|
244
268
|
}
|
|
245
269
|
});
|
|
246
270
|
}
|
|
@@ -287,7 +311,8 @@ export function registerCompleteTodo(server) {
|
|
|
287
311
|
return await handleCompleteTodo(input);
|
|
288
312
|
}
|
|
289
313
|
catch (err) {
|
|
290
|
-
|
|
314
|
+
const mapped = mapExecutionError(err, 'E_COMPLETE_TODO');
|
|
315
|
+
return createErrorResponse(mapped.code, mapped.message);
|
|
291
316
|
}
|
|
292
317
|
});
|
|
293
318
|
}
|
|
@@ -316,7 +341,7 @@ export function registerDeleteTodo(server) {
|
|
|
316
341
|
outputSchema: DefaultOutputSchema,
|
|
317
342
|
annotations: {
|
|
318
343
|
readOnlyHint: false,
|
|
319
|
-
idempotentHint:
|
|
344
|
+
idempotentHint: false,
|
|
320
345
|
destructiveHint: true,
|
|
321
346
|
},
|
|
322
347
|
}, async (input) => {
|
|
@@ -324,7 +349,8 @@ export function registerDeleteTodo(server) {
|
|
|
324
349
|
return await handleDeleteTodo(input);
|
|
325
350
|
}
|
|
326
351
|
catch (err) {
|
|
327
|
-
|
|
352
|
+
const mapped = mapExecutionError(err, 'E_DELETE_TODO');
|
|
353
|
+
return createErrorResponse(mapped.code, mapped.message);
|
|
328
354
|
}
|
|
329
355
|
});
|
|
330
356
|
}
|
|
@@ -341,11 +367,11 @@ async function handleDeleteTodos() {
|
|
|
341
367
|
},
|
|
342
368
|
});
|
|
343
369
|
}
|
|
344
|
-
export function
|
|
345
|
-
registerToolWithDiagnostics(server, '
|
|
346
|
-
title: '
|
|
370
|
+
export function registerClearTodos(server) {
|
|
371
|
+
registerToolWithDiagnostics(server, 'clear_todos', {
|
|
372
|
+
title: 'Clear All Todos',
|
|
347
373
|
description: 'Delete all todos from the list',
|
|
348
|
-
inputSchema: z.
|
|
374
|
+
inputSchema: z.strictObject({}),
|
|
349
375
|
outputSchema: DefaultOutputSchema,
|
|
350
376
|
annotations: {
|
|
351
377
|
readOnlyHint: false,
|
|
@@ -357,7 +383,8 @@ export function registerDeleteTodos(server) {
|
|
|
357
383
|
return await handleDeleteTodos();
|
|
358
384
|
}
|
|
359
385
|
catch (err) {
|
|
360
|
-
|
|
386
|
+
const mapped = mapExecutionError(err, 'E_CLEAR_TODOS');
|
|
387
|
+
return createErrorResponse(mapped.code, mapped.message);
|
|
361
388
|
}
|
|
362
389
|
});
|
|
363
390
|
}
|
|
@@ -368,7 +395,7 @@ const TOOL_REGISTRATIONS = [
|
|
|
368
395
|
registerUpdateTodo,
|
|
369
396
|
registerCompleteTodo,
|
|
370
397
|
registerDeleteTodo,
|
|
371
|
-
|
|
398
|
+
registerClearTodos,
|
|
372
399
|
];
|
|
373
400
|
export function registerAllTools(server) {
|
|
374
401
|
TOOL_REGISTRATIONS.forEach((register) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@j0hanz/todokit-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"mcpName": "io.github.j0hanz/todokit",
|
|
5
5
|
"description": "A MCP server for Todokit, a task management and productivity tool with JSON storage.",
|
|
6
6
|
"type": "module",
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"eslint-plugin-sonarjs": "^3.0.5",
|
|
65
65
|
"eslint-plugin-unused-imports": "^4.3.0",
|
|
66
66
|
"knip": "^5.80.2",
|
|
67
|
-
"jscpd": "4.0.
|
|
67
|
+
"jscpd": "4.0.7",
|
|
68
68
|
"prettier": "^3.7.4",
|
|
69
69
|
"tsx": "^4.21.0",
|
|
70
70
|
"typescript": "^5.9.3",
|