@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 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 instructionsPath = resolve(dirname(fileURLToPath(import.meta.url)), 'instructions.md');
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
  }
@@ -1,105 +1,39 @@
1
- # Todokit MCP Server — AI Usage Instructions
1
+ # Todokit Instructions
2
2
 
3
- Use this server to manage a small, persistent todo list (JSON file storage). Prefer using these tools over "remembering" state in chat.
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
- ## Operating Rules
5
+ ## 1. Core Capability
6
6
 
7
- - Use tools only when it changes or verifies the todo list (don't call tools "just to check").
8
- - Prefer `list_todos` to establish state before updating/completing/deleting.
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
- ### Strategies
10
+ ## 2. The "Golden Path" Workflows (Critical)
15
11
 
16
- - **Discovery:** Call `list_todos` (default: pending) to see current tasks. Use `status='all'` only if looking for completed items.
17
- - **Action:** Chain tools efficiently: `list` → confirm ID → `update`/`complete`. Use `add_todos` for multiple items.
12
+ ### Workflow A: Daily Triage
18
13
 
19
- ## Data Model
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
- - **Todo:** `id` (string), `description` (1-2000 chars), `priority` (low|medium|high), `category` (work|bug|testing|docs), `completed` (boolean), `dueAt` (optional ISO 8601 offset).
18
+ > **Constraint:** Never guess IDs. Always list first.
22
19
 
23
- ## Workflows
20
+ ### Workflow B: Cleanup & Verification
24
21
 
25
- ### 1) Daily Triage
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
- ```text
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
- ## Tools
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
- ### add_todo
34
+ ## 4. Error Handling Strategy
36
35
 
37
- Create a new task.
38
-
39
- - **Use when:** User provides a single, clear task.
40
- - **Args:** `description` (req), `priority` (req), `category` (req), `dueAt` (opt).
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
@@ -1,2 +1,3 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function setInitializationGuard(fn: () => boolean): void;
2
3
  export declare function registerAllTools(server: McpServer): void;
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
- return Promise.resolve(result)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@j0hanz/todokit-mcp",
3
- "version": "1.3.3",
3
+ "version": "1.3.4",
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",