@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 CHANGED
@@ -64,7 +64,7 @@ npm start
64
64
 
65
65
  ### Storage path
66
66
 
67
- By default, todos are stored in `todos.json` next to the server package (the project root when running from source). 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.
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
- ### delete_todos
230
+ ### clear_todos
231
231
 
232
232
  Delete all todos from the list.
233
233
 
@@ -24,6 +24,8 @@ export interface StorageEvent {
24
24
  v: 1;
25
25
  kind: 'storage';
26
26
  op: 'read' | 'write' | 'close';
27
+ requestId?: string | undefined;
28
+ tool?: string | undefined;
27
29
  at: string;
28
30
  durationMs?: number | undefined;
29
31
  cacheHit?: boolean | undefined;
@@ -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 { ErrorCode, InitializeRequestSchema, McpError, SUPPORTED_PROTOCOL_VERSIONS, } from '@modelcontextprotocol/sdk/types.js';
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.includes('not found'))
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 = (message) => {
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
- const boundOnInitialize = onInitialize.bind(server.server);
130
- internalServer.setRequestHandler(InitializeRequestSchema, (request) => {
131
- const requested = request.params.protocolVersion;
132
- if (!SUPPORTED_PROTOCOL_VERSIONS.includes(requested)) {
133
- throw new McpError(ErrorCode.InvalidParams, `Unsupported protocol version: ${requested}`);
118
+ }
119
+ const descriptor = Object.getOwnPropertyDescriptor(target, 'createToolError');
120
+ if (descriptor) {
121
+ if (descriptor.writable === false && descriptor.set === undefined) {
122
+ return;
134
123
  }
135
- return boundOnInitialize(request);
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.debug(line);
223
+ logger.info(line);
218
224
  },
219
225
  });
220
226
  }
@@ -0,0 +1,6 @@
1
+ export interface RequestContext {
2
+ requestId: string;
3
+ tool: string;
4
+ }
5
+ export declare function runWithRequestContext<T>(context: RequestContext, fn: () => T): T;
6
+ export declare function getRequestContext(): RequestContext | undefined;
@@ -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.object({ code: z.string(), message: z.string() }).optional(),
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, join, resolve } from 'node:path';
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 moduleDir = dirname(fileURLToPath(import.meta.url));
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
- const mtimeMs = await getFileMtime(path, IO_TIMEOUT_MS);
211
- const current = cache?.mtimeMs === mtimeMs ? cache.todos : await loadTodos(path);
212
- cache = { todos: current, mtimeMs };
213
- const { todos, result } = mutate(current);
214
- if (todos !== current) {
215
- await saveTodos(path, todos);
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
- return result;
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.query?.trim().toLowerCase();
243
- return todos.filter((todo) => matchesCompleted(todo, filters.completed) && matchesQuery(todo, query));
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 = todos.filter((todo) => !idsToDelete.has(todo.id));
459
- const deletedIds = todos
460
- .filter((todo) => idsToDelete.has(todo.id))
461
- .map((todo) => todo.id);
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 registerDeleteTodos(server: McpServer): void;
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
- const result = handler(input, extra);
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
- return createErrorResponse('E_ADD_TODO', getErrorMessage(err));
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
- return createErrorResponse('E_ADD_TODOS', getErrorMessage(err));
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
- const completed = todos.filter((t) => t.completed).length;
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
- return createErrorResponse('E_LIST_TODOS', getErrorMessage(err));
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
- return createErrorResponse('E_UPDATE_TODO', getErrorMessage(err));
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
- return createErrorResponse('E_COMPLETE_TODO', getErrorMessage(err));
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: true,
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
- return createErrorResponse('E_DELETE_TODO', getErrorMessage(err));
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 registerDeleteTodos(server) {
345
- registerToolWithDiagnostics(server, 'delete_todos', {
346
- title: 'Delete All Todos',
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.object({}),
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
- return createErrorResponse('E_DELETE_TODOS', getErrorMessage(err));
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
- registerDeleteTodos,
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.1.0",
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.5",
67
+ "jscpd": "4.0.7",
68
68
  "prettier": "^3.7.4",
69
69
  "tsx": "^4.21.0",
70
70
  "typescript": "^5.9.3",