@critical-path/mcp 0.18.1 → 0.20.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.
Files changed (51) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +70 -25
  3. package/dist/bin/args.d.ts +8 -0
  4. package/dist/bin/args.d.ts.map +1 -1
  5. package/dist/bin/args.js +20 -0
  6. package/dist/bin/args.js.map +1 -1
  7. package/dist/bin/cli.js +37 -3
  8. package/dist/bin/cli.js.map +1 -1
  9. package/dist/bin/skills.d.ts +15 -0
  10. package/dist/bin/skills.d.ts.map +1 -0
  11. package/dist/bin/skills.js +28 -0
  12. package/dist/bin/skills.js.map +1 -0
  13. package/dist/index.d.ts +2 -0
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +2 -0
  16. package/dist/index.js.map +1 -1
  17. package/dist/server/index.d.ts +6 -0
  18. package/dist/server/index.d.ts.map +1 -1
  19. package/dist/server/index.js +27 -13
  20. package/dist/server/index.js.map +1 -1
  21. package/dist/skills/catalog.generated.d.ts +3 -0
  22. package/dist/skills/catalog.generated.d.ts.map +1 -0
  23. package/dist/skills/catalog.generated.js +33 -0
  24. package/dist/skills/catalog.generated.js.map +1 -0
  25. package/dist/skills/index.d.ts +13 -0
  26. package/dist/skills/index.d.ts.map +1 -0
  27. package/dist/skills/index.js +7 -0
  28. package/dist/skills/index.js.map +1 -0
  29. package/dist/tools/definitions.d.ts +86 -8
  30. package/dist/tools/definitions.d.ts.map +1 -1
  31. package/dist/tools/definitions.js +302 -52
  32. package/dist/tools/definitions.js.map +1 -1
  33. package/dist/tools/instructions.d.ts +10 -0
  34. package/dist/tools/instructions.d.ts.map +1 -0
  35. package/dist/tools/instructions.js +48 -0
  36. package/dist/tools/instructions.js.map +1 -0
  37. package/dist/tools/outputs.d.ts +2443 -0
  38. package/dist/tools/outputs.d.ts.map +1 -0
  39. package/dist/tools/outputs.js +190 -0
  40. package/dist/tools/outputs.js.map +1 -0
  41. package/dist/web/index.d.ts +16 -0
  42. package/dist/web/index.d.ts.map +1 -1
  43. package/dist/web/index.js +13 -12
  44. package/dist/web/index.js.map +1 -1
  45. package/package.json +13 -8
  46. package/skills/analyze-schedule/SKILL.md +44 -0
  47. package/skills/investigate-task/SKILL.md +41 -0
  48. package/skills/plan-capacity/SKILL.md +32 -0
  49. package/skills/plan-project/SKILL.md +46 -0
  50. package/skills/report-status/SKILL.md +46 -0
  51. package/skills/triage-tasks/SKILL.md +41 -0
@@ -1,12 +1,39 @@
1
1
  import { z } from 'zod';
2
2
  import { zodToJsonSchema } from 'zod-to-json-schema';
3
+ import { ProjectOutput, TaskOutput, TaskWithDependenciesOutput, DependencyOutput, DeliverableOutput, CommentOutput, CriticalPathOutput, PortfolioCriticalPathOutput, TimelineLadderOutput, TaskLadderOutput, TaskMetricsOutput, TaskProgressHistoryOutput, WorkloadDistributionOutput, SkillOutput } from './outputs.js';
4
+ import { SKILLS, getSkill } from '../skills/index.js';
3
5
  /**
4
- * Declares a tool, generating its JSON Schema `inputSchema` from `zodSchema` so the schema the
5
- * model sees and the one arguments are validated against cannot drift apart.
6
+ * Groups of related tools. Hosts expose only the groups an agent needs (`toolsets` option), which
7
+ * keeps the tool list short and tool choice accurate.
8
+ */
9
+ export const TOOLSETS = {
10
+ core: 'Projects, tasks and task dependencies: find, read, create, update and delete',
11
+ collaboration: 'Deliverables and task comments',
12
+ scheduling: 'Critical path, portfolio scheduling and workload/capacity',
13
+ analytics: 'Project status ladder, task metrics, earned value and progress history',
14
+ guidance: 'Skills: step-by-step playbooks for multi-step jobs'
15
+ };
16
+ function toJsonSchema(schema) {
17
+ const { $schema: _schema, ...json } = zodToJsonSchema(schema, { $refStrategy: 'none' });
18
+ return json;
19
+ }
20
+ /**
21
+ * Declares a tool, generating its JSON Schema `inputSchema` and `outputSchema` from the zod schemas
22
+ * so the schemas the model sees and the ones values are checked against cannot drift apart.
6
23
  */
7
24
  export function defineTool(spec) {
8
- const { $schema: _schema, ...inputSchema } = zodToJsonSchema(spec.zodSchema, { $refStrategy: 'none' });
9
- return { ...spec, inputSchema };
25
+ const readOnlyHint = spec.annotations?.readOnlyHint ?? false;
26
+ return {
27
+ ...spec,
28
+ inputSchema: toJsonSchema(spec.zodSchema),
29
+ outputSchema: toJsonSchema(spec.outputZodSchema),
30
+ annotations: {
31
+ readOnlyHint,
32
+ destructiveHint: spec.annotations?.destructiveHint ?? false,
33
+ idempotentHint: spec.annotations?.idempotentHint ?? readOnlyHint,
34
+ openWorldHint: spec.annotations?.openWorldHint ?? false
35
+ }
36
+ };
10
37
  }
11
38
  export class ToolArgumentError extends Error {
12
39
  constructor(toolName, details) {
@@ -37,17 +64,21 @@ export async function resolveBackend(target) {
37
64
  export const listProjectsTool = defineTool({
38
65
  name: 'list_projects',
39
66
  title: 'List Projects',
40
- description: 'Retrieve all projects in the workspace.',
67
+ description: 'List every project you can read, with its id, name and key. Start here to find a projectId; never guess ids. Returns { projects }.',
68
+ toolset: 'core',
69
+ outputZodSchema: z.object({ projects: z.array(ProjectOutput) }),
41
70
  zodSchema: z.object({}),
42
71
  annotations: { readOnlyHint: true },
43
72
  execute: async (_args, target) => {
44
- return target.getProjects();
73
+ return { projects: await target.getProjects() };
45
74
  }
46
75
  });
47
76
  export const getProjectTool = defineTool({
48
77
  name: 'get_project',
49
78
  title: 'Get Project',
50
- description: 'Get details of a specific project by its ID.',
79
+ description: 'Get one project by id: name, key, workflow statuses, custom field definitions and calendar. Use it before creating or moving tasks to learn the valid status keys and custom fields. Returns the project. For its tasks use `list_tasks`.',
80
+ toolset: 'core',
81
+ outputZodSchema: ProjectOutput,
51
82
  zodSchema: z.object({
52
83
  id: z.string().describe('The project ID')
53
84
  }),
@@ -59,7 +90,9 @@ export const getProjectTool = defineTool({
59
90
  export const createProjectTool = defineTool({
60
91
  name: 'create_project',
61
92
  title: 'Create Project',
62
- description: 'Create a new project workspace.',
93
+ description: 'Create a new, empty project. Use only when the user wants a new project; to add work to an existing one use `create_task`. Returns the created project with its id.',
94
+ toolset: 'core',
95
+ outputZodSchema: ProjectOutput,
63
96
  zodSchema: z.object({
64
97
  name: z.string().describe('Name of the project'),
65
98
  description: z.string().optional().describe('Description of the project'),
@@ -73,7 +106,9 @@ export const createProjectTool = defineTool({
73
106
  export const listTasksTool = defineTool({
74
107
  name: 'list_tasks',
75
108
  title: 'List Tasks',
76
- description: 'List tasks (oldest first), optionally filtered by project, status, priority, assignee, iteration, deliverable, container or parent task (\'none\' for top-level tasks). Returns { tasks, nextCursor }; pass nextCursor back as cursor to get the next page.',
109
+ description: 'Find tasks, oldest first, filtered by project, status, priority, assignee, iteration, deliverable, container or parent task (\'none\' for top-level tasks only). Use it for questions like "what is blocked / assigned to X / in progress" and to look up task ids. Returns { tasks, nextCursor }; pass nextCursor back as cursor for the next page. For one task\'s full record use `get_task`.',
110
+ toolset: 'core',
111
+ outputZodSchema: z.object({ tasks: z.array(TaskOutput), nextCursor: z.string().nullable().optional() }),
77
112
  zodSchema: z.object({
78
113
  projectId: z.string().optional().describe('Filter by project ID'),
79
114
  status: z.string().optional().describe('Filter by task status key'),
@@ -106,19 +141,60 @@ export const listTasksTool = defineTool({
106
141
  export const getTaskTool = defineTool({
107
142
  name: 'get_task',
108
143
  title: 'Get Task',
109
- description: 'Get task details by ID.',
144
+ description: 'Get one task by id: status, priority, assignee, estimate, logged hours, dates, blocked state and custom fields. Set includeDependencies to also see which tasks it waits for and which wait for it. Returns the task, with { dependencies: { dependsOn, dependents } } when asked. For its discussion use `list_comments`; for progress and variance use `get_task_metrics`.',
145
+ toolset: 'core',
146
+ outputZodSchema: TaskWithDependenciesOutput,
110
147
  zodSchema: z.object({
111
- id: z.string().describe('The task ID')
148
+ id: z.string().describe('The task ID'),
149
+ includeDependencies: z
150
+ .boolean()
151
+ .optional()
152
+ .describe('Also return the tasks this one waits for (dependsOn) and the tasks waiting for it (dependents)')
112
153
  }),
113
154
  annotations: { readOnlyHint: true },
114
155
  execute: async (args, target) => {
115
- return target.getTask(args.id);
156
+ const task = await target.getTask(args.id);
157
+ if (!task || !args.includeDependencies)
158
+ return task;
159
+ const graph = await getDependencyGraph(target, args.id);
160
+ const related = new Map([...graph.upstreamTasks, ...graph.downstreamTasks].map((t) => [t.id, t]));
161
+ const dependsOn = [];
162
+ const dependents = [];
163
+ for (const dependency of graph.dependencies) {
164
+ const { waiting, waitedFor } = dependencyDirection(dependency);
165
+ const otherId = waiting === args.id ? waitedFor : waiting;
166
+ const other = related.get(otherId);
167
+ // Tasks the caller cannot read are left out.
168
+ if (!other || otherId === args.id)
169
+ continue;
170
+ const link = { dependencyId: dependency.id, taskId: other.id, title: other.title, status: other.status, type: linkType(dependency) };
171
+ (waiting === args.id ? dependsOn : dependents).push(link);
172
+ }
173
+ return { ...task, dependencies: { dependsOn, dependents } };
116
174
  }
117
175
  });
176
+ /**
177
+ * Which task waits for which. A `blocked_by` record is stored the other way round: its
178
+ * `dependsOnTaskId` waits for its `taskId` (see the scheduler in `@critical-path/core`).
179
+ */
180
+ function dependencyDirection(dependency) {
181
+ return dependency.type === 'blocked_by'
182
+ ? { waiting: dependency.dependsOnTaskId, waitedFor: dependency.taskId }
183
+ : { waiting: dependency.taskId, waitedFor: dependency.dependsOnTaskId };
184
+ }
185
+ const linkType = (dependency) => (dependency.type === 'relates_to' ? 'relates_to' : 'blocking');
186
+ async function getDependencyGraph(target, taskId) {
187
+ const { isEngine } = await resolveBackend(target);
188
+ return isEngine
189
+ ? target.getTaskDependencyGraph(taskId)
190
+ : target.getTaskDependencies(taskId);
191
+ }
118
192
  export const createTaskTool = defineTool({
119
193
  name: 'create_task',
120
194
  title: 'Create Task',
121
- description: 'Create a new task within a project.',
195
+ description: 'Create a task in a project. Check with `list_tasks` first so you do not add a duplicate. Without a status the task starts in the project\'s default workflow status. Returns the created task with its id.',
196
+ toolset: 'core',
197
+ outputZodSchema: TaskOutput,
122
198
  zodSchema: z.object({
123
199
  projectId: z.string().optional().describe('Project ID (inferred from active view in browser if omitted)'),
124
200
  title: z.string().describe('Title of the task'),
@@ -127,6 +203,8 @@ export const createTaskTool = defineTool({
127
203
  priority: z.enum(['urgent', 'high', 'medium', 'low', 'none']).optional().default('none').describe('Task priority'),
128
204
  assigneeId: z.string().optional().describe('User ID to assign task to'),
129
205
  dueDate: z.string().optional().describe('Due date (ISO string)'),
206
+ deadline: z.string().optional().describe('Latest acceptable finish (YYYY-MM-DD for the end of that working day, or an ISO date-time). Critical path analysis reports a miss as negative slack'),
207
+ isMilestone: z.boolean().optional().describe('Mark the task as a milestone: a zero-duration checkpoint (release, sign-off) fed by dependencies; its estimate is ignored'),
130
208
  tags: z.array(z.string()).optional().describe('Tags for categorization'),
131
209
  estimatedHours: z.number().optional().describe('Estimated hours to complete'),
132
210
  allocation: z
@@ -142,7 +220,7 @@ export const createTaskTool = defineTool({
142
220
  execute: async (args, target, ambientContext) => {
143
221
  const projectId = args.projectId || ambientContext?.projectId;
144
222
  if (!projectId) {
145
- throw new Error('projectId is required to create a task, but none was provided or active in context.');
223
+ throw new Error('projectId is required to create a task, but none was provided or active in context. Find one with `list_projects`.');
146
224
  }
147
225
  const input = {
148
226
  projectId,
@@ -153,6 +231,8 @@ export const createTaskTool = defineTool({
153
231
  priority: args.priority || 'none',
154
232
  assigneeId: args.assigneeId,
155
233
  dueDate: args.dueDate,
234
+ deadline: args.deadline,
235
+ isMilestone: args.isMilestone,
156
236
  tags: args.tags || [],
157
237
  estimatedHours: args.estimatedHours,
158
238
  customFields: args.customFields,
@@ -166,28 +246,33 @@ export const createTaskTool = defineTool({
166
246
  export const updateTaskTool = defineTool({
167
247
  name: 'update_task',
168
248
  title: 'Update Task',
169
- description: 'Update fields of an existing task (e.g. title, status, priority, assignee).',
249
+ description: 'Change an existing task: title, description, status, priority, assignee, due date, deadline, milestone flag, estimate, logged hours, allocation, blocked state, tags or custom fields. Only the fields you pass change. Use it to move a task through its workflow or re-plan it. Returns the updated task.',
250
+ toolset: 'core',
251
+ outputZodSchema: TaskOutput,
170
252
  zodSchema: z.object({
171
253
  id: z.string().describe('The task ID'),
172
- title: z.string().optional(),
173
- description: z.string().optional(),
174
- status: z.string().optional(),
175
- priority: z.enum(['urgent', 'high', 'medium', 'low', 'none']).optional(),
176
- assigneeId: z.string().optional(),
177
- dueDate: z.string().optional(),
178
- tags: z.array(z.string()).optional(),
179
- estimatedHours: z.number().optional(),
254
+ title: z.string().optional().describe('New title'),
255
+ description: z.string().optional().describe('New description (markdown)'),
256
+ status: z.string().optional().describe("Status key from the project's workflow (see `get_project`)"),
257
+ priority: z.enum(['urgent', 'high', 'medium', 'low', 'none']).optional().describe('New priority'),
258
+ assigneeId: z.string().optional().describe('User ID to assign the task to'),
259
+ dueDate: z.string().optional().describe('Due date (ISO string)'),
260
+ deadline: z.string().optional().describe('Latest acceptable finish (YYYY-MM-DD for the end of that working day, or an ISO date-time). Critical path analysis reports a miss as negative slack'),
261
+ isMilestone: z.boolean().optional().describe('Mark the task as a milestone: a zero-duration checkpoint (release, sign-off) fed by dependencies; its estimate is ignored'),
262
+ tags: z.array(z.string()).optional().describe('Replaces all tags'),
263
+ estimatedHours: z.number().optional().describe('Estimated effort in hours'),
180
264
  allocation: z
181
265
  .number()
182
266
  .gt(0)
183
267
  .max(1)
184
268
  .optional()
185
269
  .describe("Share of the assignee's time the task takes, in (0, 1]; e.g. 0.5 for half-time. Estimates are effort, so a 0.5 task takes twice as long"),
186
- loggedHours: z.number().optional(),
270
+ loggedHours: z.number().optional().describe('Total hours worked so far'),
187
271
  isBlocked: z.boolean().optional().describe('Whether the task is blocked'),
188
272
  blockedReason: z.string().optional().describe('Reason why the task is blocked'),
189
273
  customFields: z.record(z.unknown()).optional().describe('Custom field values as a JSON object (validated against the project\'s field definitions for tasks)')
190
274
  }),
275
+ annotations: { idempotentHint: true },
191
276
  execute: async (args, target) => {
192
277
  const { id, ...updates } = args;
193
278
  return target.updateTask(id, updates);
@@ -196,50 +281,113 @@ export const updateTaskTool = defineTool({
196
281
  export const deleteTaskTool = defineTool({
197
282
  name: 'delete_task',
198
283
  title: 'Delete Task',
199
- description: 'Delete a task by ID.',
284
+ description: 'Permanently delete a task by id. This cannot be undone: confirm with the user first, and prefer `update_task` with a canceled status when the history should be kept. Returns { success }.',
285
+ toolset: 'core',
286
+ outputZodSchema: z.object({ success: z.boolean() }),
200
287
  zodSchema: z.object({
201
288
  id: z.string().describe('The task ID to delete')
202
289
  }),
203
- annotations: { requiresConfirmation: true },
290
+ annotations: { destructiveHint: true, idempotentHint: true },
204
291
  execute: async (args, target) => {
205
292
  const success = await target.deleteTask(args.id);
206
293
  return { success };
207
294
  }
208
295
  });
296
+ export const addDependencyTool = defineTool({
297
+ name: 'add_dependency',
298
+ title: 'Add Dependency',
299
+ description: 'Record that a task cannot start until another task finishes, so the critical path schedules them in order. Use it when planning or re-sequencing work. Adding a link that already exists returns it; a link that would make a loop is refused. Returns the dependency { id, taskId, dependsOnTaskId, type }. To undo use `remove_dependency`.',
300
+ toolset: 'core',
301
+ outputZodSchema: DependencyOutput,
302
+ zodSchema: z.object({
303
+ taskId: z.string().describe('The task that waits'),
304
+ dependsOnTaskId: z.string().describe('The task that must finish first'),
305
+ type: z
306
+ .enum(['blocking', 'relates_to'])
307
+ .optional()
308
+ .describe("'blocking' (default) affects the schedule; 'relates_to' only links the tasks for reference")
309
+ }),
310
+ annotations: { idempotentHint: true },
311
+ execute: async (args, target) => {
312
+ const type = args.type ?? 'blocking';
313
+ const { isEngine } = await resolveBackend(target);
314
+ if (isEngine) {
315
+ return target.addDependency({ taskId: args.taskId, dependsOnTaskId: args.dependsOnTaskId, type });
316
+ }
317
+ return target.addDependency(args.taskId, { dependsOnTaskId: args.dependsOnTaskId, type });
318
+ }
319
+ });
320
+ export const removeDependencyTool = defineTool({
321
+ name: 'remove_dependency',
322
+ title: 'Remove Dependency',
323
+ description: 'Stop a task waiting for another task, for example when the order was wrong or the work no longer depends on it. Identify the link by its two task ids, as given to `add_dependency`. Removing a link that does not exist is not an error. Returns { removed, dependencyIds } with the number of links removed.',
324
+ toolset: 'core',
325
+ outputZodSchema: z.object({
326
+ removed: z.number().describe('How many dependency records were removed (0 if none matched)'),
327
+ dependencyIds: z.array(z.string()).describe('Ids of the removed records')
328
+ }),
329
+ zodSchema: z.object({
330
+ taskId: z.string().describe('The task that waits'),
331
+ dependsOnTaskId: z.string().describe('The task it waits for'),
332
+ type: z.enum(['blocking', 'relates_to']).optional().describe('Remove only links of this type (default: both)')
333
+ }),
334
+ annotations: { idempotentHint: true },
335
+ execute: async (args, target) => {
336
+ const graph = await getDependencyGraph(target, args.taskId);
337
+ const matches = graph.dependencies.filter((dependency) => {
338
+ const { waiting, waitedFor } = dependencyDirection(dependency);
339
+ return waiting === args.taskId && waitedFor === args.dependsOnTaskId && (!args.type || linkType(dependency) === args.type);
340
+ });
341
+ const { isEngine } = await resolveBackend(target);
342
+ const dependencyIds = [];
343
+ for (const dependency of matches) {
344
+ const removed = isEngine
345
+ ? await target.removeDependency(dependency.id)
346
+ : await target.removeDependency(dependency.taskId, dependency.id);
347
+ if (removed)
348
+ dependencyIds.push(dependency.id);
349
+ }
350
+ return { removed: dependencyIds.length, dependencyIds };
351
+ }
352
+ });
209
353
  export const listDeliverablesTool = defineTool({
210
354
  name: 'list_deliverables',
211
355
  title: 'List Deliverables',
212
- description: 'List deliverables/milestones for a project.',
356
+ description: 'List a project\'s deliverables: outputs that group the tasks producing them. Use it to see what is due and which tasks feed each deliverable. For dated checkpoints, look for milestone tasks (`isMilestone`). Returns { deliverables }.',
357
+ toolset: 'collaboration',
358
+ outputZodSchema: z.object({ deliverables: z.array(DeliverableOutput) }),
213
359
  zodSchema: z.object({
214
- projectId: z.string().optional().describe('Project ID')
360
+ projectId: z.string().optional().describe('Project ID (falls back to ambient context if omitted)')
215
361
  }),
216
362
  annotations: { readOnlyHint: true },
217
363
  execute: async (args, target, ambientContext) => {
218
364
  const projectId = args.projectId || ambientContext?.projectId;
219
365
  if (!projectId)
220
- throw new Error('projectId is required to list deliverables.');
366
+ throw new Error('projectId is required to list deliverables. Find one with `list_projects`.');
221
367
  if ('getDeliverables' in target && typeof target.getDeliverables === 'function') {
222
- return target.getDeliverables(projectId);
368
+ return { deliverables: await target.getDeliverables(projectId) };
223
369
  }
224
- return [];
370
+ return { deliverables: [] };
225
371
  }
226
372
  });
227
373
  export const createDeliverableTool = defineTool({
228
374
  name: 'create_deliverable',
229
375
  title: 'Create Deliverable',
230
- description: 'Create a milestone deliverable within a project.',
376
+ description: 'Create a deliverable in a project, optionally linking existing tasks by id. Use it for an output (a document, a build, an asset) that several tasks produce. For a dated checkpoint the schedule should enforce, create a task with `isMilestone: true` and a `deadline` instead. Returns the created deliverable.',
377
+ toolset: 'collaboration',
378
+ outputZodSchema: DeliverableOutput,
231
379
  zodSchema: z.object({
232
- projectId: z.string().optional(),
233
- title: z.string(),
234
- description: z.string().optional(),
235
- dueDate: z.string().optional(),
236
- taskIds: z.array(z.string()).optional(),
380
+ projectId: z.string().optional().describe('Project ID (falls back to ambient context if omitted)'),
381
+ title: z.string().describe('Name of the deliverable'),
382
+ description: z.string().optional().describe('What the deliverable is and how it is accepted'),
383
+ dueDate: z.string().optional().describe('Due date (ISO string)'),
384
+ taskIds: z.array(z.string()).optional().describe('IDs of existing tasks that feed this deliverable'),
237
385
  customFields: z.record(z.unknown()).optional().describe('Custom field values as a JSON object (validated against the project\'s field definitions for tasks)')
238
386
  }),
239
387
  execute: async (args, target, ambientContext) => {
240
388
  const projectId = args.projectId || ambientContext?.projectId;
241
389
  if (!projectId)
242
- throw new Error('projectId is required to create a deliverable.');
390
+ throw new Error('projectId is required to create a deliverable. Find one with `list_projects`.');
243
391
  if ('createDeliverable' in target && typeof target.createDeliverable === 'function') {
244
392
  return target.createDeliverable({
245
393
  ...args,
@@ -252,22 +400,26 @@ export const createDeliverableTool = defineTool({
252
400
  export const listCommentsTool = defineTool({
253
401
  name: 'list_comments',
254
402
  title: 'List Comments',
255
- description: 'List comments associated with a task.',
403
+ description: 'List the comments on a task. Use it to read the discussion, decisions or blockers recorded there. Returns { comments }.',
404
+ toolset: 'collaboration',
405
+ outputZodSchema: z.object({ comments: z.array(CommentOutput) }),
256
406
  zodSchema: z.object({
257
407
  taskId: z.string().describe('The task ID')
258
408
  }),
259
409
  annotations: { readOnlyHint: true },
260
410
  execute: async (args, target) => {
261
411
  if ('getComments' in target && typeof target.getComments === 'function') {
262
- return target.getComments(args.taskId);
412
+ return { comments: await target.getComments(args.taskId) };
263
413
  }
264
- return [];
414
+ return { comments: [] };
265
415
  }
266
416
  });
267
417
  export const addCommentTool = defineTool({
268
418
  name: 'add_comment',
269
419
  title: 'Add Comment',
270
- description: 'Add a comment to a task. The comment is attributed to the identity this MCP server runs as.',
420
+ description: 'Post a comment on a task, attributed to the identity this server runs as. Use it to record a decision, status note, question or the reasoning behind a change you made. Returns the created comment.',
421
+ toolset: 'collaboration',
422
+ outputZodSchema: CommentOutput,
271
423
  zodSchema: z.object({
272
424
  taskId: z.string().describe('The task ID'),
273
425
  content: z.string().describe('Comment body/markdown'),
@@ -280,7 +432,9 @@ export const addCommentTool = defineTool({
280
432
  export const calculatePortfolioCriticalPathTool = defineTool({
281
433
  name: 'calculate_portfolio_critical_path',
282
434
  title: 'Calculate Portfolio Critical Path',
283
- description: 'Critical path analysis across several projects at once (or every readable project). With calendars "assignee" and levelResources, people and teams are shared across projects, so nobody is double-booked between them.',
435
+ description: 'Schedule several projects together (default: every project you can read) so people and teams shared between them are not double-booked. Use it for cross-project delivery dates and resource conflicts. Returns { projects, projectEndDate, overallocations } with one analysis per project. For a single project use `calculate_critical_path`.',
436
+ toolset: 'scheduling',
437
+ outputZodSchema: PortfolioCriticalPathOutput,
284
438
  zodSchema: z.object({
285
439
  projectIds: z.array(z.string()).optional().describe('Projects to include (default: every project you can read)'),
286
440
  projectOrder: z.array(z.string()).optional().describe('Project ids in priority order for levelling; unlisted projects come last'),
@@ -305,7 +459,9 @@ export const calculatePortfolioCriticalPathTool = defineTool({
305
459
  export const calculateCriticalPathTool = defineTool({
306
460
  name: 'calculate_critical_path',
307
461
  title: 'Calculate Critical Path',
308
- description: 'Calculate Critical Path Method (CPM) schedule, early/late start and finish, total slack, and critical bottlenecks for a project.',
462
+ description: 'Schedule one project with the Critical Path Method: which tasks drive the end date, each task\'s early and late start and finish, and how much it can slip (slack). Use it for "when will this finish?", "what is on the critical path?" or "can this task slip?". Set calendars \'assignee\' with levelResources so each person works on one task at a time. Returns { projectEndDate, criticalTaskIds, tasks, overallocations }. For several projects that share people use `calculate_portfolio_critical_path`.',
463
+ toolset: 'scheduling',
464
+ outputZodSchema: CriticalPathOutput,
309
465
  zodSchema: z.object({
310
466
  projectId: z.string().optional().describe('Project ID (falls back to ambient context if omitted)'),
311
467
  calendars: z
@@ -329,7 +485,7 @@ export const calculateCriticalPathTool = defineTool({
329
485
  execute: async (args, target, ambientContext) => {
330
486
  const projectId = args.projectId || ambientContext?.projectId;
331
487
  if (!projectId) {
332
- throw new Error('projectId is required to calculate critical path.');
488
+ throw new Error('projectId is required to calculate the critical path. Find one with `list_projects`.');
333
489
  }
334
490
  const { calendars, levelResources, levelingPriority, effort } = args;
335
491
  return target.calculateCriticalPath(projectId, { calendars, levelResources, levelingPriority, effort });
@@ -338,7 +494,9 @@ export const calculateCriticalPathTool = defineTool({
338
494
  export const getTimelineLadderTool = defineTool({
339
495
  name: 'get_timeline_ladder',
340
496
  title: 'Get Timeline Ladder of Abstraction',
341
- description: 'Retrieve Ladder of Abstraction for a project timeline across macro phase envelopes, standard Gantt tasks & CPM, and concrete deliverables/time/attachments.',
497
+ description: 'Project status at three zoom levels: \'macro\' phases with health (on_track, at_risk, blocked, overdue) and progress, \'standard\' task timeline with the critical path and dependencies, and \'concrete\' evidence of work (time logged, attachments) per task. Use it for project status reports; level \'macro\' gives a compact summary. Returns { macro, standard, concrete }. For a single task use `get_task_ladder`.',
498
+ toolset: 'analytics',
499
+ outputZodSchema: TimelineLadderOutput,
342
500
  zodSchema: z.object({
343
501
  projectId: z.string().optional().describe('Project ID (falls back to ambient context if omitted)'),
344
502
  level: z.enum(['macro', 'standard', 'concrete', 'all']).optional().describe('Abstraction level (macro, standard, concrete, or all)'),
@@ -349,7 +507,7 @@ export const getTimelineLadderTool = defineTool({
349
507
  execute: async (args, target, ambientContext) => {
350
508
  const projectId = args.projectId || ambientContext?.projectId;
351
509
  if (!projectId) {
352
- throw new Error('projectId is required to get timeline ladder.');
510
+ throw new Error('projectId is required to get the timeline ladder. Find one with `list_projects`.');
353
511
  }
354
512
  return target.getTimelineLadder(projectId, {
355
513
  level: args.level,
@@ -361,7 +519,9 @@ export const getTimelineLadderTool = defineTool({
361
519
  export const getTaskLadderTool = defineTool({
362
520
  name: 'get_task_ladder',
363
521
  title: 'Get Task Ladder View',
364
- description: 'Retrieve multi-scale ladder view for a single task, connecting its macro phase, standard CPM timeline position, and concrete work evidence.',
522
+ description: 'One task in context: the phase it belongs to and that phase\'s health, its place in the schedule (dates, slack, critical or not) and evidence of work done. Use it to explain where a task fits and whether it puts its phase at risk. Returns { macroPhase, standard, concrete, metrics }. For the numbers alone use `get_task_metrics`.',
523
+ toolset: 'analytics',
524
+ outputZodSchema: TaskLadderOutput,
365
525
  zodSchema: z.object({
366
526
  taskId: z.string().describe('The task ID')
367
527
  }),
@@ -373,7 +533,9 @@ export const getTaskLadderTool = defineTool({
373
533
  export const getTaskMetricsTool = defineTool({
374
534
  name: 'get_task_metrics',
375
535
  title: 'Get Task Metrics',
376
- description: 'Retrieve multi-dimensional task metrics including inferred actuals, active working duration, effort/duration/schedule variances, progress inference, and Earned Value Management (EVM) metrics.',
536
+ description: 'Is a task on track? Progress percentage, actual against estimated hours, schedule variance, overdue flags and earned value (CPI, SPI). Use it to diagnose a late or over-budget task. Returns { progress, realityDelta, evm, inferredActuals, progressHistory }.',
537
+ toolset: 'analytics',
538
+ outputZodSchema: TaskMetricsOutput,
377
539
  zodSchema: z.object({
378
540
  taskId: z.string().describe('The task ID')
379
541
  }),
@@ -385,7 +547,9 @@ export const getTaskMetricsTool = defineTool({
385
547
  export const getTaskProgressHistoryTool = defineTool({
386
548
  name: 'get_task_progress_history',
387
549
  title: 'Get Task Progress History',
388
- description: 'Retrieve time-series progress data points and curve shape classification (linear, s_curve, early_surge, late_rush, stalled) for a task.',
550
+ description: 'How a task\'s progress has moved over time: dated progress points and a curve shape (linear, s_curve, early_surge, late_rush, stalled). Use it to tell whether work is stalling or being rushed at the end. Returns { points, curveProfile }. `get_task_metrics` includes the same history alongside variance and earned value.',
551
+ toolset: 'analytics',
552
+ outputZodSchema: TaskProgressHistoryOutput,
389
553
  zodSchema: z.object({
390
554
  taskId: z.string().describe('The task ID')
391
555
  }),
@@ -397,7 +561,9 @@ export const getTaskProgressHistoryTool = defineTool({
397
561
  export const getWorkloadDistributionTool = defineTool({
398
562
  name: 'get_workload_distribution',
399
563
  title: 'Get Workload Distribution',
400
- description: 'Retrieve time-series workload and capacity distribution suitable for streamgraphs, stacked charts, and team capacity planning.',
564
+ description: 'Hours of work per person, team, task type, priority or status in each day, week or month, with capacity and utilization. Use it for "who is overloaded?" and capacity planning; a utilizationRatio above 1 means more work than capacity. Omit projectId for the whole workspace. Returns { buckets, seriesKeys, seriesLabels, totalHours, averageUtilization }.',
565
+ toolset: 'scheduling',
566
+ outputZodSchema: WorkloadDistributionOutput,
401
567
  zodSchema: z.object({
402
568
  projectId: z.string().optional().describe('Project ID (optional, defaults to workspace-wide or ambient project)'),
403
569
  startDate: z.string().optional().describe('Start date ISO (YYYY-MM-DD)'),
@@ -420,6 +586,27 @@ export const getWorkloadDistributionTool = defineTool({
420
586
  });
421
587
  }
422
588
  });
589
+ export const getSkillTool = defineTool({
590
+ name: 'get_skill',
591
+ title: 'Get Skill',
592
+ description: 'Load a step-by-step playbook for a multi-step project management job, such as planning a project, triaging tasks, finding schedule risk, writing a status report, checking capacity or investigating a late task. Call it before starting such a job and follow its steps. Omit name to list the skills. Returns { name, description, content }, or { skills } without a name.',
593
+ toolset: 'guidance',
594
+ outputZodSchema: SkillOutput,
595
+ zodSchema: z.object({
596
+ name: z.string().optional().describe('Skill name, e.g. "plan-project"; omit to list all skills')
597
+ }),
598
+ annotations: { readOnlyHint: true },
599
+ execute: async (args) => {
600
+ if (!args.name) {
601
+ return { skills: SKILLS.map(({ name, description }) => ({ name, description })) };
602
+ }
603
+ const skill = getSkill(args.name);
604
+ if (!skill) {
605
+ throw new Error(`Skill "${args.name}" not found. Available skills: ${SKILLS.map((s) => s.name).join(', ')}.`);
606
+ }
607
+ return skill;
608
+ }
609
+ });
423
610
  export const ALL_TOOLS = [
424
611
  listProjectsTool,
425
612
  getProjectTool,
@@ -429,6 +616,8 @@ export const ALL_TOOLS = [
429
616
  createTaskTool,
430
617
  updateTaskTool,
431
618
  deleteTaskTool,
619
+ addDependencyTool,
620
+ removeDependencyTool,
432
621
  listDeliverablesTool,
433
622
  createDeliverableTool,
434
623
  listCommentsTool,
@@ -439,7 +628,68 @@ export const ALL_TOOLS = [
439
628
  getTaskLadderTool,
440
629
  getTaskMetricsTool,
441
630
  getTaskProgressHistoryTool,
442
- getWorkloadDistributionTool
631
+ getWorkloadDistributionTool,
632
+ getSkillTool
443
633
  ];
444
634
  export const TOOL_MAP = new Map(ALL_TOOLS.map((tool) => [tool.name, tool]));
635
+ /**
636
+ * Picks the tools a host exposes. Unknown tool or toolset names throw, so a typo cannot silently
637
+ * leave an agent without the tools it was meant to have. `get_skill` follows the same rules as
638
+ * other tools: include the `guidance` toolset (or name it) to keep skills reachable.
639
+ */
640
+ export function selectTools(selection = {}) {
641
+ const { tools, toolsets, readOnly } = selection;
642
+ const unknownTools = (tools ?? []).filter((name) => !TOOL_MAP.has(name));
643
+ if (unknownTools.length > 0) {
644
+ throw new Error(`Unknown Critical Path tool(s): ${unknownTools.join(', ')}. Known tools: ${ALL_TOOLS.map((t) => t.name).join(', ')}.`);
645
+ }
646
+ const unknownSets = (toolsets ?? []).filter((name) => !(name in TOOLSETS));
647
+ if (unknownSets.length > 0) {
648
+ throw new Error(`Unknown Critical Path toolset(s): ${unknownSets.join(', ')}. Known toolsets: ${Object.keys(TOOLSETS).join(', ')}.`);
649
+ }
650
+ const filtered = !tools && !toolsets;
651
+ return ALL_TOOLS.filter((tool) => (filtered || tools?.includes(tool.name) || toolsets?.includes(tool.toolset)) &&
652
+ (!readOnly || tool.annotations.readOnlyHint));
653
+ }
654
+ export class ToolNotFoundError extends Error {
655
+ constructor(message) {
656
+ super(message);
657
+ this.name = 'ToolNotFoundError';
658
+ }
659
+ }
660
+ const ID_RECOVERY = 'Check the id: find valid project ids with `list_projects` and task ids with `list_tasks`. Ids are never guessable, and items you cannot read are reported as not found.';
661
+ /**
662
+ * Validates arguments, runs a tool and returns a JSON-safe object result. A missing record (a
663
+ * `null` result) becomes an error, and errors carry a hint for what to do next so the agent can
664
+ * recover on its own.
665
+ */
666
+ export async function runTool(tool, args, target, ambientContext) {
667
+ try {
668
+ const result = await tool.execute(parseToolArgs(tool, args), target, ambientContext);
669
+ if (result === null || result === undefined) {
670
+ throw new ToolNotFoundError(`Nothing found for ${JSON.stringify(args ?? {})}.`);
671
+ }
672
+ // Round-trip through JSON: drops undefined fields and serializes dates, as a transport would.
673
+ return JSON.parse(JSON.stringify(result));
674
+ }
675
+ catch (err) {
676
+ throw new Error(withRecoveryHint(tool, err));
677
+ }
678
+ }
679
+ function withRecoveryHint(tool, err) {
680
+ const message = err?.message || String(err);
681
+ if (err instanceof ToolArgumentError) {
682
+ return `${message}. Fix the arguments to match the input schema of "${tool.name}" and call it again.`;
683
+ }
684
+ if (err instanceof ToolNotFoundError || err?.name === 'NotFoundError' || err?.status === 404 || /not found/i.test(message)) {
685
+ return `${message} ${ID_RECOVERY}`;
686
+ }
687
+ if (err?.name === 'CircularDependencyError' || /circular/i.test(message)) {
688
+ return `${message} The tasks are already ordered the other way round. Check with \`get_task\` and includeDependencies, and ask the user which order is right rather than retrying.`;
689
+ }
690
+ if (err?.name === 'ForbiddenError' || err?.status === 403) {
691
+ return `${message} You do not have permission for this; tell the user rather than retrying.`;
692
+ }
693
+ return message;
694
+ }
445
695
  //# sourceMappingURL=definitions.js.map