@j0hanz/cortex-mcp 1.5.0 → 1.7.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.
@@ -7,55 +7,55 @@ function buildToolReference() {
7
7
  .join('\n\n');
8
8
  }
9
9
  export function buildWorkflowGuide() {
10
- return `# THE "GOLDEN PATH" WORKFLOWS (CRITICAL)
10
+ return `<role>
11
+ You are an expert reasoning engine assistant. You decompose queries into structured thought chains at configurable depth levels (basic, normal, high).
12
+ </role>
11
13
 
14
+ <workflows>
12
15
  ### WORKFLOW A: Sequential Reasoning (Most Common)
13
-
14
- 1. Call \`reasoning_think\` with \`{ query: "...", level: "basic", thought: "Your detailed reasoning for step 1..." }\`.
15
- 2. Read the response note the \`sessionId\` and \`remainingThoughts\` fields.
16
- 3. **You MUST continue**: Call again with \`{ sessionId: "<from response>", thought: "Your next reasoning step..." }\`.
17
- 4. Repeat step 3 until the response shows \`status: "completed"\` or \`remainingThoughts: 0\`.
18
- NOTE: The \`summary\` field contains the exact continuation call you should make next.
19
-
20
- ### WORKFLOW B: Multi-Turn Reasoning (Session Continuation)
21
-
22
- 1. Call \`reasoning_think\` with \`{ query: "initial question", level: "normal", thought: "Your first reasoning step..." }\` note the returned \`sessionId\`.
23
- 2. Call \`reasoning_think\` with \`{ sessionId: "<id>", thought: "Your next reasoning step..." }\` (optional: add \`query\` for follow-up context).
24
- 3. Repeat until \`status: "completed"\` or \`remainingThoughts: 0\`, then read \`reasoning://sessions/{sessionId}\` for the full chain.
25
- NOTE: The \`level\` parameter is optional when continuing; if provided and mismatched, the session level is used.
26
-
27
- ### WORKFLOW C: Controlled Depth Reasoning
28
-
29
- 1. Call \`reasoning_think\` with \`{ query: "...", level: "normal", targetThoughts: 8, thought: "Your reasoning..." }\` to set the session's planned step count.
30
- 2. Repeat calls with the returned \`sessionId\` and your next \`thought\` until \`result.totalThoughts\` is reached.
31
- NOTE: \`targetThoughts\` must fall within the level range (basic: 3–5, normal: 6–10, high: 15–25). Out-of-range values return \`E_INVALID_THOUGHT_COUNT\`.
32
-
33
- ### WORKFLOW D: Async Task Execution
34
-
35
- 1. Call \`reasoning_think\` as a task (send \`tools/call\` with \`task\` field) for long-running \`high\`-level reasoning.
36
- 2. Poll \`tasks/get\` until status is \`completed\` or \`failed\`.
37
- 3. Retrieve the result via \`tasks/result\`.
38
- 4. Use \`tasks/cancel\` to abort if needed.
16
+ 1. Call \`reasoning_think\` with \`{ query: "...", level: "basic", thought: "..." }\`.
17
+ 2. Read response: note \`sessionId\` and \`remainingThoughts\`.
18
+ 3. **MUST continue**: Call again with \`{ sessionId: "<id>", thought: "..." }\`.
19
+ 4. Repeat until \`status: "completed"\` or \`remainingThoughts: 0\`.
20
+ NOTE: \`summary\` field contains the exact next call.
21
+
22
+ ### WORKFLOW B: Multi-Turn Reasoning
23
+ 1. Call \`reasoning_think\` with \`{ query: "...", level: "normal", thought: "..." }\`.
24
+ 2. Call \`reasoning_think\` with \`{ sessionId: "<id>", thought: "..." }\` (optional: add \`query\` for follow-up).
25
+ 3. Repeat until completed. Read \`reasoning://sessions/{sessionId}\` for full chain.
26
+ NOTE: \`level\` is optional when continuing; session level is used if omitted.
27
+
28
+ ### WORKFLOW C: Controlled Depth
29
+ 1. Call \`reasoning_think\` with \`{ query: "...", level: "normal", targetThoughts: 8, thought: "..." }\`.
30
+ 2. Repeat with \`sessionId\` and \`thought\` until \`totalThoughts\` reached.
31
+ NOTE: \`targetThoughts\` must fit level range (basic: 3-5, normal: 6-10, high: 15-25).
32
+
33
+ ### WORKFLOW D: Async Task
34
+ 1. Call \`reasoning_think\` as task (send \`task\` field) for long \`high\`-level reasoning.
35
+ 2. Poll \`tasks/get\` until \`completed\`/\`failed\`.
36
+ 3. Retrieve via \`tasks/result\`.
37
+ 4. Abort via \`tasks/cancel\`.
39
38
 
40
39
  ### WORKFLOW E: Batched Run-To-Completion
41
-
42
- 1. Start a new session with explicit \`targetThoughts\` and \`runMode: "run_to_completion"\`.
43
- 2. Provide one \`thought\` plus additional \`thoughts[]\` entries to cover the planned step count.
44
- 3. The server consumes thought inputs in order until completion, token budget exhaustion, or cancellation.
45
-
46
- ### WORKFLOW F: Structured Reasoning (Observation/Hypothesis/Evaluation)
47
-
48
- 1. Call \`reasoning_think\` with \`{ query: "...", level: "normal", observation: "facts...", hypothesis: "idea...", evaluation: "critique..." }\`.
49
- 2. The server formats these into a structured thought and stores it in the session trace.
50
- 3. Continue with \`sessionId\` using either \`thought\` or structured fields for subsequent steps.
51
- 4. Use \`is_conclusion: true\` to end early, or \`rollback_to_step\` to discard and redo from a specific step.
52
-
53
- ## Shared Constraints
40
+ 1. Start session with \`targetThoughts\` and \`runMode: "run_to_completion"\`.
41
+ 2. Provide \`thought\` as string array (e.g., \`["step1", "step2"]\`).
42
+ 3. Server consumes inputs until completion, token exhaustion, or cancellation.
43
+
44
+ ### WORKFLOW F: Structured Reasoning
45
+ 1. Call \`reasoning_think\` with \`{ query: "...", level: "normal", observation: "...", hypothesis: "...", evaluation: "..." }\`.
46
+ 2. Server formats into structured thought in trace.
47
+ 3. Continue with \`sessionId\` using \`thought\` or structured fields.
48
+ 4. Use \`is_conclusion: true\` to end early, or \`rollback_to_step\` to discard/redo.
49
+ </workflows>
50
+
51
+ <constraints>
54
52
  ${getSharedConstraints()
55
53
  .map((c) => `- ${c}`)
56
54
  .join('\n')}
55
+ </constraints>
57
56
 
58
- ## Tool Reference
57
+ <tool_reference>
59
58
  ${buildToolReference()}
59
+ </tool_reference>
60
60
  `;
61
61
  }
@@ -13,7 +13,6 @@ export declare const ReasoningThinkInputSchema: z.ZodObject<{
13
13
  run_to_completion: "run_to_completion";
14
14
  }>>;
15
15
  thought: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
16
- thoughts: z.ZodOptional<z.ZodArray<z.ZodString>>;
17
16
  is_conclusion: z.ZodOptional<z.ZodBoolean>;
18
17
  rollback_to_step: z.ZodOptional<z.ZodNumber>;
19
18
  step_summary: z.ZodOptional<z.ZodString>;
@@ -17,63 +17,49 @@ function addCustomIssue(ctx, path, message) {
17
17
  }
18
18
  export const ReasoningThinkInputSchema = z
19
19
  .strictObject({
20
- query: QUERY_TEXT_SCHEMA.optional().describe('The question or problem to reason about'),
21
- level: LEVEL_SCHEMA.optional().describe(`Reasoning depth level (required for new sessions, optional for continuing). ${getLevelDescriptionString()}.`),
20
+ query: QUERY_TEXT_SCHEMA.optional().describe('Question or problem to reason about.'),
21
+ level: LEVEL_SCHEMA.optional().describe(`Reasoning depth level (required for new sessions). ${getLevelDescriptionString()}.`),
22
22
  targetThoughts: z
23
23
  .number()
24
24
  .int()
25
25
  .min(1)
26
26
  .max(25)
27
27
  .optional()
28
- .describe('Optional explicit thought count. Must fit the level range: basic 3–5, normal 6–10, high 15–25.'),
28
+ .describe('Explicit thought count. Must fit level range.'),
29
29
  sessionId: z
30
30
  .string()
31
31
  .min(1)
32
32
  .max(128)
33
33
  .optional()
34
- .describe('Session ID to continue. The session level is used when continuing; provided level is optional.'),
34
+ .describe('Session ID to continue.'),
35
35
  runMode: z
36
36
  .enum(RUN_MODE_VALUES)
37
37
  .optional()
38
- .describe('Execution mode (default: "step"). "step" appends a single thought per call. "run_to_completion" consumes all supplied thought inputs in one request.'),
38
+ .describe('Execution mode. "step" (default) or "run_to_completion".'),
39
39
  thought: z
40
40
  .union([THOUGHT_TEXT_SCHEMA, THOUGHT_BATCH_SCHEMA])
41
41
  .optional()
42
- .describe('Your full reasoning content for this step. ' +
43
- 'The server stores this text verbatim as the thought in the session trace. ' +
44
- 'Write your complete analysis, observations, and conclusions here — this is what appears in trace.md. ' +
45
- 'Can be a single string or an array of strings (for batch execution).'),
46
- thoughts: z
47
- .array(THOUGHT_TEXT_SCHEMA)
48
- .max(25)
49
- .optional()
50
- .describe('(Deprecated) Optional additional thought inputs. Use "thought" as an array instead.'),
42
+ .describe('Full reasoning content for this step. Stored verbatim. String or string array.'),
51
43
  is_conclusion: z
52
44
  .boolean()
53
45
  .optional()
54
- .describe('Set to true if you have arrived at the final answer and wish to end the reasoning session early.'),
46
+ .describe('End session early if final answer reached.'),
55
47
  rollback_to_step: z
56
48
  .number()
57
49
  .int()
58
50
  .min(0)
59
51
  .optional()
60
- .describe('Set to a thought index (0-based) to rollback to. All thoughts after this index will be discarded.'),
52
+ .describe('0-based thought index to rollback to. Discards subsequent thoughts.'),
61
53
  step_summary: z
62
54
  .string()
63
55
  .optional()
64
- .describe('A 1-sentence summary of the conclusion reached in this specific step.'),
65
- observation: z
66
- .string()
67
- .optional()
68
- .describe('What facts are known at this step?'),
56
+ .describe('1-sentence summary of the conclusion reached.'),
57
+ observation: z.string().optional().describe('Facts known at this step.'),
69
58
  hypothesis: z
70
59
  .string()
71
60
  .optional()
72
- .describe('What is the proposed idea or next logical leap?'),
73
- evaluation: z
74
- .string()
75
- .optional()
76
- .describe('Critique the hypothesis. Are there flaws?'),
61
+ .describe('Proposed idea or next logical leap.'),
62
+ evaluation: z.string().optional().describe('Critique of the hypothesis.'),
77
63
  })
78
64
  .superRefine((data, ctx) => {
79
65
  const runMode = data.runMode ?? DEFAULT_RUN_MODE;
@@ -91,9 +77,6 @@ export const ReasoningThinkInputSchema = z
91
77
  if (runMode === 'step' && Array.isArray(data.thought)) {
92
78
  addCustomIssue(ctx, ['thought'], 'thought must be a string when runMode is "step"');
93
79
  }
94
- if (runMode === 'step' && data.thoughts !== undefined) {
95
- addCustomIssue(ctx, ['thoughts'], 'thoughts is only allowed when runMode is "run_to_completion"');
96
- }
97
80
  const hasThought = data.thought !== undefined;
98
81
  const hasStructured = data.observation !== undefined &&
99
82
  data.hypothesis !== undefined &&
@@ -3,6 +3,7 @@ declare const ReasoningThinkSuccessSchema: z.ZodObject<{
3
3
  ok: z.ZodLiteral<true>;
4
4
  result: z.ZodObject<{
5
5
  sessionId: z.ZodString;
6
+ query: z.ZodOptional<z.ZodString>;
6
7
  level: z.ZodEnum<{
7
8
  basic: "basic";
8
9
  normal: "normal";
@@ -40,6 +41,7 @@ export declare const ReasoningThinkToolOutputSchema: z.ZodObject<{
40
41
  ok: z.ZodBoolean;
41
42
  result: z.ZodOptional<z.ZodObject<{
42
43
  sessionId: z.ZodString;
44
+ query: z.ZodOptional<z.ZodString>;
43
45
  level: z.ZodEnum<{
44
46
  basic: "basic";
45
47
  normal: "normal";
@@ -90,6 +92,7 @@ export declare const ReasoningThinkResultSchema: z.ZodUnion<readonly [z.ZodObjec
90
92
  ok: z.ZodLiteral<true>;
91
93
  result: z.ZodObject<{
92
94
  sessionId: z.ZodString;
95
+ query: z.ZodOptional<z.ZodString>;
93
96
  level: z.ZodEnum<{
94
97
  basic: "basic";
95
98
  normal: "normal";
@@ -13,34 +13,34 @@ const ThoughtSchema = z.strictObject({
13
13
  stepSummary: z
14
14
  .string()
15
15
  .optional()
16
- .describe('A 1-sentence summary of the conclusion reached in this step, if provided.'),
16
+ .describe('1-sentence summary of the conclusion reached.'),
17
17
  });
18
18
  const ReasoningThinkSuccessSchema = z.strictObject({
19
19
  ok: z.literal(true),
20
20
  result: z.strictObject({
21
21
  sessionId: z.string(),
22
+ query: z
23
+ .string()
24
+ .optional()
25
+ .describe('Original query text for this session.'),
22
26
  level: z.enum(REASONING_LEVELS),
23
27
  status: z.enum(SESSION_STATUSES),
24
28
  thoughts: z.array(ThoughtSchema),
25
29
  generatedThoughts: z.number(),
26
30
  requestedThoughts: z.number(),
27
31
  totalThoughts: z.number(),
28
- tokenBudget: z
29
- .number()
30
- .describe('Approximate token budget (UTF-8 bytes ÷ 4, not true tokenization)'),
31
- tokensUsed: z
32
- .number()
33
- .describe('Approximate tokens used (UTF-8 bytes ÷ 4, not true tokenization)'),
32
+ tokenBudget: z.number().describe('Approximate token budget.'),
33
+ tokensUsed: z.number().describe('Approximate tokens used.'),
34
34
  ttlMs: z.number(),
35
35
  expiresAt: z.number(),
36
36
  createdAt: z.number(),
37
37
  updatedAt: z.number(),
38
38
  remainingThoughts: z
39
39
  .number()
40
- .describe('Number of thoughts remaining before the session reaches totalThoughts'),
40
+ .describe('Thoughts remaining before reaching totalThoughts.'),
41
41
  summary: z
42
42
  .string()
43
- .describe('Actionable next-step instruction when active, or completion status when done'),
43
+ .describe('Actionable next-step instruction or completion status.'),
44
44
  }),
45
45
  });
46
46
  const ReasoningThinkErrorSchema = z.strictObject({
package/dist/server.js CHANGED
@@ -3,6 +3,7 @@ import { findPackageJSON } from 'node:module';
3
3
  import { InMemoryTaskStore } from '@modelcontextprotocol/sdk/experimental/tasks';
4
4
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
5
5
  import { engineEvents } from './engine/events.js';
6
+ import { sessionStore } from './engine/reasoner.js';
6
7
  import { getErrorMessage } from './lib/errors.js';
7
8
  import { registerAllTools } from './tools/index.js';
8
9
  import { registerAllPrompts } from './prompts/index.js';
@@ -20,6 +21,7 @@ const ICON_URL_CANDIDATES = [
20
21
  ];
21
22
  let cachedLocalIconData;
22
23
  let cachedVersion;
24
+ let activeServerCount = 0;
23
25
  function getLocalIconData() {
24
26
  if (cachedLocalIconData !== undefined) {
25
27
  return cachedLocalIconData ?? undefined;
@@ -119,9 +121,22 @@ function attachEngineEventHandlers(server) {
119
121
  process.stderr.write(`[cortex-mcp.server] Failed to log budget_exhausted: ${getErrorMessage(err)}\n`);
120
122
  });
121
123
  };
124
+ const onSessionLifecycle = (data) => {
125
+ void server.server.sendResourceListChanged().catch((err) => {
126
+ logNotificationFailure(RESOURCE_LIST_CHANGED_METHOD, err, {
127
+ sessionId: data.sessionId,
128
+ });
129
+ });
130
+ };
122
131
  engineEvents.on('resources:changed', onResourcesChanged);
123
132
  engineEvents.on('resource:updated', onResourceUpdated);
124
133
  engineEvents.on('thought:budget-exhausted', onBudgetExhausted);
134
+ engineEvents.on('session:created', onSessionLifecycle);
135
+ engineEvents.on('session:completed', onSessionLifecycle);
136
+ engineEvents.on('session:cancelled', onSessionLifecycle);
137
+ engineEvents.on('session:expired', onSessionLifecycle);
138
+ engineEvents.on('session:evicted', onSessionLifecycle);
139
+ engineEvents.on('session:deleted', onSessionLifecycle);
125
140
  let detached = false;
126
141
  return () => {
127
142
  if (detached) {
@@ -131,6 +146,12 @@ function attachEngineEventHandlers(server) {
131
146
  engineEvents.off('resources:changed', onResourcesChanged);
132
147
  engineEvents.off('resource:updated', onResourceUpdated);
133
148
  engineEvents.off('thought:budget-exhausted', onBudgetExhausted);
149
+ engineEvents.off('session:created', onSessionLifecycle);
150
+ engineEvents.off('session:completed', onSessionLifecycle);
151
+ engineEvents.off('session:cancelled', onSessionLifecycle);
152
+ engineEvents.off('session:expired', onSessionLifecycle);
153
+ engineEvents.off('session:evicted', onSessionLifecycle);
154
+ engineEvents.off('session:deleted', onSessionLifecycle);
134
155
  };
135
156
  }
136
157
  function installCloseCleanup(server, cleanup) {
@@ -142,10 +163,18 @@ function installCloseCleanup(server, cleanup) {
142
163
  }
143
164
  closed = true;
144
165
  cleanup();
166
+ activeServerCount = Math.max(0, activeServerCount - 1);
167
+ if (activeServerCount === 0) {
168
+ sessionStore.dispose();
169
+ }
145
170
  await originalClose();
146
171
  };
147
172
  }
148
173
  export function createServer() {
174
+ if (activeServerCount === 0) {
175
+ sessionStore.ensureCleanupTimer();
176
+ }
177
+ activeServerCount += 1;
149
178
  const version = loadVersion();
150
179
  const taskStore = new InMemoryTaskStore();
151
180
  const localIcon = getLocalIconData();
@@ -6,13 +6,30 @@ import { createTaskLimiter } from '../lib/concurrency.js';
6
6
  import { createErrorResponse, getErrorMessage, InsufficientThoughtsError, InvalidRunModeArgsError, isObjectRecord, ReasoningAbortedError, ReasoningError, ServerBusyError, SessionNotFoundError, } from '../lib/errors.js';
7
7
  import { formatProgressMessage, formatThoughtsToMarkdown, } from '../lib/formatting.js';
8
8
  import { createToolResponse, withIconMeta } from '../lib/tool-response.js';
9
- import { parsePositiveIntEnv } from '../lib/validators.js';
9
+ import { parseBooleanEnv, parsePositiveIntEnv } from '../lib/validators.js';
10
10
  const DEFAULT_MAX_ACTIVE_REASONING_TASKS = 32;
11
+ const REDACTED_THOUGHT_CONTENT = '[REDACTED]';
12
+ function shouldRedactTraceContent() {
13
+ return parseBooleanEnv('CORTEX_REDACT_TRACE_CONTENT', false);
14
+ }
11
15
  function buildTraceResource(session) {
16
+ const sessionView = shouldRedactTraceContent()
17
+ ? {
18
+ ...session,
19
+ thoughts: session.thoughts.map((thought) => ({
20
+ index: thought.index,
21
+ content: REDACTED_THOUGHT_CONTENT,
22
+ revision: thought.revision,
23
+ ...(thought.stepSummary !== undefined
24
+ ? { stepSummary: REDACTED_THOUGHT_CONTENT }
25
+ : {}),
26
+ })),
27
+ }
28
+ : session;
12
29
  return {
13
- uri: `file:///cortex/sessions/${session.id}/trace.md`,
30
+ uri: `reasoning://sessions/${session.id}/trace.md`,
14
31
  mimeType: 'text/markdown',
15
- text: formatThoughtsToMarkdown(session),
32
+ text: formatThoughtsToMarkdown(sessionView),
16
33
  };
17
34
  }
18
35
  const reasoningTaskLimiter = createTaskLimiter(parsePositiveIntEnv('CORTEX_MAX_ACTIVE_REASONING_TASKS', DEFAULT_MAX_ACTIVE_REASONING_TASKS));
@@ -69,7 +86,7 @@ function isReasoningTaskExtra(value) {
69
86
  }
70
87
  return true;
71
88
  }
72
- function parseReasoningTaskExtra(rawExtra) {
89
+ function assertReasoningTaskExtra(rawExtra) {
73
90
  if (!isReasoningTaskExtra(rawExtra)) {
74
91
  throw new Error('Invalid task context in request handler.');
75
92
  }
@@ -144,7 +161,7 @@ function buildThoughtInputs(params) {
144
161
  : params.thought
145
162
  ? [params.thought]
146
163
  : [];
147
- return [...primary, ...(params.thoughts ?? [])];
164
+ return primary;
148
165
  }
149
166
  function getStartingThoughtCount(sessionId) {
150
167
  if (sessionId === undefined) {
@@ -224,6 +241,7 @@ function buildStructuredResult(session, generatedThoughts, targetThoughts) {
224
241
  ok: true,
225
242
  result: {
226
243
  sessionId: session.id,
244
+ ...(session.query !== undefined ? { query: session.query } : {}),
227
245
  level: session.level,
228
246
  status: session.status,
229
247
  thoughts: [...session.thoughts],
@@ -243,7 +261,7 @@ function buildStructuredResult(session, generatedThoughts, targetThoughts) {
243
261
  }
244
262
  function buildSummary(session, remainingThoughts) {
245
263
  if (session.status === 'completed') {
246
- return `Reasoning complete — ${String(session.thoughts.length)} thoughts at [${session.level}] level. Session ${session.id}.`;
264
+ return `Reasoning complete — ${String(session.thoughts.length)} thought${session.thoughts.length === 1 ? '' : 's'} at [${session.level}] level. Session ${session.id}.`;
247
265
  }
248
266
  if (session.status === 'cancelled') {
249
267
  return `Reasoning cancelled at thought ${String(session.thoughts.length)}/${String(session.totalThoughts)}. Session ${session.id}.`;
@@ -283,16 +301,22 @@ function createCancellationController(signal) {
283
301
  const controller = new AbortController();
284
302
  if (signal.aborted) {
285
303
  controller.abort();
286
- return controller;
304
+ return {
305
+ controller,
306
+ cleanup: () => {
307
+ // No listener to clean up when already aborted.
308
+ },
309
+ };
287
310
  }
288
311
  const onAbort = () => {
289
312
  controller.abort();
290
313
  };
291
- signal.addEventListener('abort', onAbort, { once: true });
292
- controller.signal.addEventListener('abort', () => {
314
+ const cleanup = () => {
293
315
  signal.removeEventListener('abort', onAbort);
294
- }, { once: true });
295
- return controller;
316
+ };
317
+ signal.addEventListener('abort', onAbort, { once: true });
318
+ controller.signal.addEventListener('abort', cleanup, { once: true });
319
+ return { controller, cleanup };
296
320
  }
297
321
  async function isTaskCancelled(taskStore, taskId) {
298
322
  try {
@@ -311,7 +335,7 @@ async function ensureTaskIsActive(taskStore, taskId, controller) {
311
335
  }
312
336
  function createProgressHandler(args) {
313
337
  const { server, taskStore, taskId, level, progressToken, controller, startingCount, batchTotal, } = args;
314
- return async (progress) => {
338
+ return async (progress, _total, summary) => {
315
339
  await ensureTaskIsActive(taskStore, taskId, controller);
316
340
  if (progressToken === undefined) {
317
341
  return;
@@ -322,14 +346,16 @@ function createProgressHandler(args) {
322
346
  const isTerminal = displayProgress >= batchTotal;
323
347
  // We must emit if it's the terminal update for this batch,
324
348
  // otherwise we respect the session-level skipping rules.
349
+ // If a summary is provided, we force an emit to show the meaningful update.
325
350
  if (!isTerminal &&
351
+ !summary &&
326
352
  !shouldEmitProgress(displayProgress, batchTotal, level)) {
327
353
  return;
328
354
  }
329
355
  const message = formatProgressMessage({
330
- toolName: TOOL_NAME,
356
+ toolName: `꩜ ${TOOL_NAME}`,
331
357
  context: 'Thought',
332
- metadata: `[${String(displayProgress)}/${String(batchTotal)}]`,
358
+ ...(summary ? { metadata: summary } : {}),
333
359
  ...(isTerminal ? { outcome: 'complete' } : {}),
334
360
  });
335
361
  await notifyProgress({
@@ -457,9 +483,9 @@ async function runReasoningTask(args) {
457
483
  const normalizedBatchTotal = Math.max(1, batchTotal);
458
484
  if (progressToken !== undefined) {
459
485
  const message = formatProgressMessage({
460
- toolName: TOOL_NAME,
461
- context: 'reasoning',
462
- metadata: level ? `starting [${level}]` : 'continuing session',
486
+ toolName: `꩜ ${TOOL_NAME}`,
487
+ context: level ? 'starting' : 'continuing',
488
+ metadata: level ? `[${level}]` : 'session',
463
489
  });
464
490
  await notifyProgress({
465
491
  server,
@@ -544,7 +570,7 @@ async function runReasoningTask(args) {
544
570
  }
545
571
  function getTaskId(extra) {
546
572
  if (typeof extra.taskId !== 'string' || extra.taskId.length === 0) {
547
- throw new Error('Task ID missing in request context.');
573
+ throw new InvalidRunModeArgsError('Task ID missing in request context.');
548
574
  }
549
575
  return extra.taskId;
550
576
  }
@@ -565,7 +591,8 @@ Use step_summary for a 1-sentence conclusion per step — these accumulate in th
565
591
 
566
592
  Levels: ${getLevelDescriptionString()}.
567
593
  Alternatives: runMode="run_to_completion" (batch), or observation/hypothesis/evaluation fields (structured).
568
- Errors: E_SESSION_NOT_FOUND (expired — start new), E_INVALID_THOUGHT_COUNT (check level ranges).`,
594
+ Errors: E_SESSION_NOT_FOUND (expired — start new), E_INVALID_THOUGHT_COUNT (check level ranges).
595
+ Protocol validation: malformed task metadata/arguments fail at request level before task start; runtime reasoning failures return tool isError=true payloads.`,
569
596
  inputSchema: ReasoningThinkInputSchema,
570
597
  outputSchema: ReasoningThinkToolOutputSchema,
571
598
  annotations: {
@@ -586,7 +613,7 @@ Errors: E_SESSION_NOT_FOUND (expired — start new), E_INVALID_THOUGHT_COUNT (ch
586
613
  throw new Error(`Invalid reasoning_think params: ${parseResult.error.message}`);
587
614
  }
588
615
  const params = parseResult.data;
589
- const extra = parseReasoningTaskExtra(rawExtra);
616
+ const extra = assertReasoningTaskExtra(rawExtra);
590
617
  const progressToken = extra._meta?.progressToken;
591
618
  if (!reasoningTaskLimiter.tryAcquire()) {
592
619
  throw new ServerBusyError();
@@ -602,13 +629,13 @@ Errors: E_SESSION_NOT_FOUND (expired — start new), E_INVALID_THOUGHT_COUNT (ch
602
629
  reasoningTaskLimiter.release();
603
630
  throw error;
604
631
  }
605
- const controller = createCancellationController(extra.signal);
632
+ const cancellation = createCancellationController(extra.signal);
606
633
  const runReasoningArgs = {
607
634
  server,
608
635
  taskStore: extra.taskStore,
609
636
  taskId: task.taskId,
610
637
  params,
611
- controller,
638
+ controller: cancellation.controller,
612
639
  };
613
640
  if (progressToken !== undefined) {
614
641
  runReasoningArgs.progressToken = progressToken;
@@ -617,16 +644,17 @@ Errors: E_SESSION_NOT_FOUND (expired — start new), E_INVALID_THOUGHT_COUNT (ch
617
644
  runReasoningArgs.sessionId = extra.sessionId;
618
645
  }
619
646
  void runReasoningTask(runReasoningArgs).finally(() => {
647
+ cancellation.cleanup();
620
648
  reasoningTaskLimiter.release();
621
649
  });
622
650
  return { task };
623
651
  },
624
652
  getTask(_params, rawExtra) {
625
- const extra = parseReasoningTaskExtra(rawExtra);
653
+ const extra = assertReasoningTaskExtra(rawExtra);
626
654
  return extra.taskStore.getTask(getTaskId(extra));
627
655
  },
628
656
  async getTaskResult(_params, rawExtra) {
629
- const extra = parseReasoningTaskExtra(rawExtra);
657
+ const extra = assertReasoningTaskExtra(rawExtra);
630
658
  const result = await extra.taskStore.getTaskResult(getTaskId(extra));
631
659
  assertCallToolResult(result);
632
660
  return result;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@j0hanz/cortex-mcp",
3
- "version": "1.5.0",
3
+ "version": "1.7.0",
4
4
  "mcpName": "io.github.j0hanz/cortex-mcp",
5
5
  "author": "Johanz",
6
6
  "license": "MIT",