agent-planner-mcp 0.8.1 → 0.9.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/src/tools.js CHANGED
@@ -1,2659 +1,64 @@
1
1
  /**
2
- * MCP Tools Implementation
3
- *
4
- * Provides comprehensive planning tools for AI agents:
5
- * - Full CRUD operations on all entities
6
- * - Unified search across all scopes
7
- * - Batch operations for efficiency
8
- * - Rich context retrieval
9
- * - Text responses for Claude Desktop compatibility
2
+ * MCP Tools — v0.9.0 BDI-aligned surface.
3
+ *
4
+ * 15 tools across Belief / Desire / Intention / Utility namespaces. The legacy
5
+ * 63-tool CRUD-shaped surface was removed in v0.9.0 — see ../docs/MIGRATION_v0.9.md
6
+ * for the mapping from old tool names to new ones, and ../docs/MCP_REDESIGN_PLAN.md
7
+ * for the design rationale.
10
8
  */
11
9
 
12
10
  const { ListToolsRequestSchema, CallToolRequestSchema } = require('@modelcontextprotocol/sdk/types.js');
13
11
  const defaultApiClient = require('./api-client');
14
-
15
- const APP_URL = (process.env.APP_URL || 'https://agentplanner.io').replace(/\/$/, '');
16
- function buildPlanUrl(planId) { return `${APP_URL}/app/plans/${planId}`; }
17
- function buildTaskUrl(planId, nodeId) { return `${APP_URL}/app/plans/${planId}?node=${nodeId}`; }
12
+ const { bdiToolDefinitions, bdiToolHandler, bdiToolNames } = require('./tools/bdi');
18
13
 
19
14
  /**
20
- * Format JSON data as text for Claude Desktop
21
- */
22
- function formatResponse(data) {
23
- // If data is an error object with a message, return just the message
24
- if (data && data.error) {
25
- return {
26
- isError: true,
27
- content: [
28
- {
29
- type: "text",
30
- text: data.error
31
- }
32
- ]
33
- };
34
- }
35
-
36
- // For successful responses, stringify the data
37
- return {
38
- content: [
39
- {
40
- type: "text",
41
- text: JSON.stringify(data, null, 2)
42
- }
43
- ]
44
- };
45
- }
46
-
47
- /**
48
- * Setup tools for the MCP server
15
+ * Wire BDI tools into an MCP server.
49
16
  * @param {Server} server - MCP server instance
50
- * @param {Object} [apiClientOverride] - Per-session API client (HTTP mode). Falls back to default (stdio mode).
17
+ * @param {Object} [apiClientOverride] - Per-session API client (HTTP mode); falls back to default (stdio mode)
51
18
  */
52
19
  function setupTools(server, apiClientOverride) {
53
20
  const apiClient = apiClientOverride || defaultApiClient;
54
- // Suppress console logs when not in debug mode
55
- if (process.env.NODE_ENV !== 'development') {
56
- // Silent mode for production
57
- } else {
58
- console.error('Setting up MCP tools...');
59
- }
60
-
61
- // Handler for listing available tools
62
- server.setRequestHandler(ListToolsRequestSchema, async () => {
63
- return {
64
- tools: [
65
- // ========================================
66
- // QUICK ACTIONS - Low friction entry points
67
- // Use these for common operations
68
- // ========================================
69
- {
70
- name: "quick_plan",
71
- description: "Create a plan quickly from a title and list of tasks. Perfect for getting started fast - just provide a title and task names. Returns plan URL and task IDs for immediate use. Tip: provide a goal_id to automatically link this plan to a goal.",
72
- inputSchema: {
73
- type: "object",
74
- properties: {
75
- title: { type: "string", description: "Plan title" },
76
- description: { type: "string", description: "Optional plan description" },
77
- tasks: {
78
- type: "array",
79
- items: { type: "string" },
80
- description: "List of task titles (simple strings). A phase will be created automatically."
81
- },
82
- goal_id: { type: "string", description: "Optionally link this plan to a goal. Recommended: always link plans to goals for tracking." },
83
- organization_id: { type: "string", description: "Organization ID (uses default if not provided)" }
84
- },
85
- required: ["title", "tasks"]
86
- }
87
- },
88
- {
89
- name: "quick_task",
90
- description: "Add a single task to an existing plan. Minimal parameters - just plan_id and title. Automatically adds to the first phase or creates one.",
91
- inputSchema: {
92
- type: "object",
93
- properties: {
94
- plan_id: { type: "string", description: "Plan to add task to" },
95
- title: { type: "string", description: "Task title" },
96
- description: { type: "string", description: "Optional task description" },
97
- phase_id: { type: "string", description: "Specific phase to add to (optional - uses first phase if not provided)" },
98
- agent_instructions: { type: "string", description: "Instructions for AI agents working on this task" }
99
- },
100
- required: ["plan_id", "title"]
101
- }
102
- },
103
- {
104
- name: "quick_status",
105
- description: "Update a task's status. The most common operation - made simple. Returns what to do next.",
106
- inputSchema: {
107
- type: "object",
108
- properties: {
109
- task_id: { type: "string", description: "Task ID to update" },
110
- plan_id: { type: "string", description: "Plan ID (required for API)" },
111
- status: {
112
- type: "string",
113
- enum: ["not_started", "in_progress", "completed", "blocked", "plan_ready"],
114
- description: "New status"
115
- },
116
- note: { type: "string", description: "Optional note explaining the status change (especially useful for 'blocked')" }
117
- },
118
- required: ["task_id", "plan_id", "status"]
119
- }
120
- },
121
- {
122
- name: "quick_log",
123
- description: "Add a progress note to a task. Use this to document work as you go - helps humans follow along and other agents understand what happened.",
124
- inputSchema: {
125
- type: "object",
126
- properties: {
127
- task_id: { type: "string", description: "Task ID" },
128
- plan_id: { type: "string", description: "Plan ID" },
129
- message: { type: "string", description: "What you did or learned" },
130
- log_type: {
131
- type: "string",
132
- enum: ["progress", "decision", "blocker", "completion"],
133
- default: "progress",
134
- description: "Type of log entry"
135
- }
136
- },
137
- required: ["task_id", "plan_id", "message"]
138
- }
139
- },
140
-
141
- {
142
- name: "check_goals_health",
143
- description: "Check the health of all your goals. Returns per-goal health status (on_track/at_risk/stale), bottleneck summaries, knowledge gaps, and pending decisions. Call this FIRST in the autonomous loop to identify which goals need attention.",
144
- inputSchema: {
145
- type: "object",
146
- properties: {
147
- status_filter: { type: "string", description: "Filter by health status (e.g. 'on_track', 'at_risk', 'stale')" }
148
- }
149
- }
150
- },
151
-
152
- // ========================================
153
- // TASK CLAIMING - Prevent agent collisions
154
- // ========================================
155
- {
156
- name: "claim_task",
157
- description: "Claim exclusive ownership of a task before starting work. Prevents other agents from working on the same task. Claims expire after ttl_minutes (default 30). Always claim before starting work on a task.",
158
- inputSchema: {
159
- type: "object",
160
- properties: {
161
- task_id: { type: "string", description: "Task ID to claim" },
162
- plan_id: { type: "string", description: "Plan ID" },
163
- ttl_minutes: { type: "integer", description: "Claim duration in minutes (default 30)", default: 30 }
164
- },
165
- required: ["task_id", "plan_id"]
166
- }
167
- },
168
- {
169
- name: "release_task",
170
- description: "Release a previously claimed task. Called automatically when you complete a task, but use this if you need to abandon work early.",
171
- inputSchema: {
172
- type: "object",
173
- properties: {
174
- task_id: { type: "string", description: "Task ID to release" },
175
- plan_id: { type: "string", description: "Plan ID" }
176
- },
177
- required: ["task_id", "plan_id"]
178
- }
179
- },
180
-
181
- // ========================================
182
- // COHERENCE - Check alignment across goals/plans/knowledge
183
- // ========================================
184
- {
185
- name: "check_coherence_pending",
186
- description: "Check what needs coherence review. Returns stale plans and goals that have changed since their last coherence check. Call this at the start of a maintenance cycle to discover what needs attention. Uses timestamp comparison (updated_at vs coherence_checked_at) — no expensive processing.",
187
- inputSchema: {
188
- type: "object",
189
- properties: {}
190
- }
191
- },
192
- {
193
- name: "run_coherence_check",
194
- description: "Run a coherence check on a specific plan. Evaluates quality (coverage, specificity, ordering, knowledge completeness), flags contradictions, and stamps the plan as checked. Returns quality breakdown with sub-scores and rationale.",
195
- inputSchema: {
196
- type: "object",
197
- properties: {
198
- plan_id: { type: "string", description: "Plan ID to check" },
199
- goal_id: { type: "string", description: "Optional goal ID to evaluate coverage against" }
200
- },
201
- required: ["plan_id"]
202
- }
203
- },
204
- {
205
- name: "assess_goal_quality",
206
- description: "Assess how well-defined a goal is. Evaluates 5 dimensions: clarity (title+description), measurability (success criteria), actionability (linked plans), knowledge grounding (related facts), and commitment (desire vs intention, deadline). Returns score, dimension breakdown, and specific improvement suggestions. Use this when helping users define or refine goals.",
207
- inputSchema: {
208
- type: "object",
209
- properties: {
210
- goal_id: { type: "string", description: "Goal ID to assess" }
211
- },
212
- required: ["goal_id"]
213
- }
214
- },
215
-
216
- // ========================================
217
- // CONTEXT LOADING - Get everything you need
218
- // Use before starting work on a plan/goal
219
- // ========================================
220
- {
221
- name: "get_my_tasks",
222
- description: "Get tasks that need attention - blocked tasks, in-progress tasks, and next tasks to start. Perfect for status check-ins.",
223
- inputSchema: {
224
- type: "object",
225
- properties: {
226
- plan_id: { type: "string", description: "Specific plan to check (optional - checks all if not provided)" },
227
- status: {
228
- type: "array",
229
- items: { type: "string" },
230
- default: ["blocked", "in_progress"],
231
- description: "Task statuses to include"
232
- }
233
- }
234
- }
235
- },
236
-
237
- // ========================================
238
- // MARKDOWN EXPORT/IMPORT - Filesystem pattern
239
- // ========================================
240
- {
241
- name: "export_plan_markdown",
242
- description: "Export a plan as markdown text. Useful for reviewing plans in text format or saving to files.",
243
- inputSchema: {
244
- type: "object",
245
- properties: {
246
- plan_id: { type: "string", description: "Plan to export" },
247
- include_descriptions: { type: "boolean", default: true, description: "Include task descriptions" },
248
- include_status: { type: "boolean", default: true, description: "Include status indicators" }
249
- },
250
- required: ["plan_id"]
251
- }
252
- },
253
- {
254
- name: "import_plan_markdown",
255
- description: "Create a new plan from markdown text. Parses headings as phases and list items as tasks. Great for converting text plans into structured AgentPlanner plans.",
256
- inputSchema: {
257
- type: "object",
258
- properties: {
259
- markdown: {
260
- type: "string",
261
- description: "Markdown text to parse. Use # for title, ## for phases, - for tasks"
262
- },
263
- title: {
264
- type: "string",
265
- description: "Plan title (optional - extracted from first # heading if not provided)"
266
- },
267
- goal_id: { type: "string", description: "Optionally link to a goal" }
268
- },
269
- required: ["markdown"]
270
- }
271
- },
272
-
273
- // ===== UNIFIED SEARCH TOOL =====
274
- {
275
- name: "search",
276
- description: "Universal search tool for plans, nodes, and content",
277
- inputSchema: {
278
- type: "object",
279
- properties: {
280
- scope: {
281
- type: "string",
282
- description: "Search scope",
283
- enum: ["global", "plans", "plan", "node"],
284
- default: "global"
285
- },
286
- scope_id: {
287
- type: "string",
288
- description: "Plan ID (if scope is 'plan') or Node ID (if scope is 'node')"
289
- },
290
- query: {
291
- type: "string",
292
- description: "Search query"
293
- },
294
- filters: {
295
- type: "object",
296
- description: "Optional filters",
297
- properties: {
298
- status: {
299
- type: "string",
300
- description: "Filter by status",
301
- enum: ["draft", "active", "completed", "archived", "not_started", "in_progress", "blocked"]
302
- },
303
- type: {
304
- type: "string",
305
- description: "Filter by type",
306
- enum: ["plan", "node", "phase", "task", "milestone", "log"]
307
- },
308
- limit: {
309
- type: "integer",
310
- description: "Maximum number of results",
311
- default: 20
312
- }
313
- }
314
- }
315
- },
316
- required: ["query"]
317
- }
318
- },
319
-
320
- // ===== PLAN MANAGEMENT TOOLS =====
321
- {
322
- name: "list_plans",
323
- description: "List plans. By default excludes completed/archived plans — set include_completed to true to see all.",
324
- inputSchema: {
325
- type: "object",
326
- properties: {
327
- status: {
328
- type: "string",
329
- description: "Optional filter by plan status",
330
- enum: ["draft", "active", "completed", "archived"]
331
- },
332
- include_completed: {
333
- type: "boolean",
334
- description: "If true, include completed and archived plans (default: false)",
335
- default: false
336
- }
337
- }
338
- }
339
- },
340
- {
341
- name: "create_plan",
342
- description: "Create a new plan",
343
- inputSchema: {
344
- type: "object",
345
- properties: {
346
- title: { type: "string", description: "Plan title" },
347
- description: { type: "string", description: "Plan description" },
348
- status: {
349
- type: "string",
350
- description: "Plan status",
351
- enum: ["draft", "active", "completed", "archived"],
352
- default: "draft"
353
- }
354
- },
355
- required: ["title"]
356
- }
357
- },
358
- {
359
- name: "update_plan",
360
- description: "Update an existing plan",
361
- inputSchema: {
362
- type: "object",
363
- properties: {
364
- plan_id: { type: "string", description: "Plan ID" },
365
- title: { type: "string", description: "New plan title" },
366
- description: { type: "string", description: "New plan description" },
367
- status: {
368
- type: "string",
369
- description: "New plan status",
370
- enum: ["draft", "active", "completed", "archived"]
371
- }
372
- },
373
- required: ["plan_id"]
374
- }
375
- },
376
- {
377
- name: "delete_plan",
378
- description: "Delete a plan",
379
- inputSchema: {
380
- type: "object",
381
- properties: {
382
- plan_id: { type: "string", description: "Plan ID to delete" }
383
- },
384
- required: ["plan_id"]
385
- }
386
- },
387
- {
388
- name: "share_plan",
389
- description: "Share a plan by making it public or private. Public plans can be viewed by anyone with the link.",
390
- inputSchema: {
391
- type: "object",
392
- properties: {
393
- plan_id: { type: "string", description: "Plan ID to share" },
394
- visibility: {
395
- type: "string",
396
- description: "Plan visibility setting",
397
- enum: ["public", "private"],
398
- default: "public"
399
- },
400
- github_repo_owner: {
401
- type: "string",
402
- description: "GitHub repository owner (optional, for linking public plans to a repo)"
403
- },
404
- github_repo_name: {
405
- type: "string",
406
- description: "GitHub repository name (optional, for linking public plans to a repo)"
407
- }
408
- },
409
- required: ["plan_id"]
410
- }
411
- },
412
-
413
- // ===== NODE MANAGEMENT TOOLS =====
414
- {
415
- name: "create_node",
416
- description: "Create a new node in a plan",
417
- inputSchema: {
418
- type: "object",
419
- properties: {
420
- plan_id: { type: "string", description: "Plan ID" },
421
- parent_id: { type: "string", description: "Parent node ID (optional, defaults to root)" },
422
- node_type: {
423
- type: "string",
424
- description: "Node type",
425
- enum: ["phase", "task", "milestone"]
426
- },
427
- title: { type: "string", description: "Node title" },
428
- description: { type: "string", description: "Node description" },
429
- status: {
430
- type: "string",
431
- description: "Node status",
432
- enum: ["not_started", "in_progress", "completed", "blocked", "plan_ready"],
433
- default: "not_started"
434
- },
435
- context: { type: "string", description: "Additional context for the node" },
436
- agent_instructions: { type: "string", description: "Instructions for AI agents working on this node" },
437
- acceptance_criteria: { type: "string", description: "Criteria for node completion" },
438
- due_date: { type: "string", description: "Due date (ISO format)" },
439
- metadata: { type: "object", description: "Additional metadata" },
440
- task_mode: {
441
- type: "string",
442
- description: "RPI workflow mode for the node",
443
- enum: ["research", "plan", "implement", "free"],
444
- default: "free"
445
- }
446
- },
447
- required: ["plan_id", "node_type", "title"]
448
- }
449
- },
450
- {
451
- name: "update_node",
452
- description: "Update a node's properties",
453
- inputSchema: {
454
- type: "object",
455
- properties: {
456
- plan_id: { type: "string", description: "Plan ID" },
457
- node_id: { type: "string", description: "Node ID" },
458
- title: { type: "string", description: "New node title" },
459
- description: { type: "string", description: "New node description" },
460
- status: {
461
- type: "string",
462
- description: "New node status",
463
- enum: ["not_started", "in_progress", "completed", "blocked", "plan_ready"]
464
- },
465
- context: { type: "string", description: "New context" },
466
- agent_instructions: { type: "string", description: "New agent instructions" },
467
- acceptance_criteria: { type: "string", description: "New acceptance criteria" },
468
- due_date: { type: "string", description: "New due date (ISO format)" },
469
- metadata: { type: "object", description: "New metadata" },
470
- task_mode: {
471
- type: "string",
472
- description: "RPI workflow mode for the node",
473
- enum: ["research", "plan", "implement", "free"]
474
- }
475
- },
476
- required: ["plan_id", "node_id"]
477
- }
478
- },
479
- {
480
- name: "delete_node",
481
- description: "Delete a node and all its children",
482
- inputSchema: {
483
- type: "object",
484
- properties: {
485
- plan_id: { type: "string", description: "Plan ID" },
486
- node_id: { type: "string", description: "Node ID to delete" }
487
- },
488
- required: ["plan_id", "node_id"]
489
- }
490
- },
491
- {
492
- name: "move_node",
493
- description: "Move a node to a different parent or position",
494
- inputSchema: {
495
- type: "object",
496
- properties: {
497
- plan_id: { type: "string", description: "Plan ID" },
498
- node_id: { type: "string", description: "Node ID to move" },
499
- parent_id: { type: "string", description: "New parent node ID" },
500
- order_index: { type: "integer", description: "New position index" }
501
- },
502
- required: ["plan_id", "node_id"]
503
- }
504
- },
505
- {
506
- name: "get_node_ancestry",
507
- description: "Get the path from root to a specific node",
508
- inputSchema: {
509
- type: "object",
510
- properties: {
511
- plan_id: { type: "string", description: "Plan ID" },
512
- node_id: { type: "string", description: "Node ID" }
513
- },
514
- required: ["plan_id", "node_id"]
515
- }
516
- },
517
-
518
- // ===== DEPENDENCY TOOLS =====
519
- {
520
- name: "create_dependency",
521
- description: "Create a dependency edge between two nodes in a plan. Source 'blocks' target by default.",
522
- inputSchema: {
523
- type: "object",
524
- properties: {
525
- plan_id: { type: "string", description: "Plan ID" },
526
- source_node_id: { type: "string", description: "Source node ID (the blocker)" },
527
- target_node_id: { type: "string", description: "Target node ID (the blocked)" },
528
- dependency_type: {
529
- type: "string",
530
- description: "Type of dependency",
531
- enum: ["blocks", "requires", "relates_to"],
532
- default: "blocks"
533
- },
534
- weight: { type: "integer", description: "Edge weight (default 1)", default: 1 },
535
- metadata: { type: "object", description: "Additional metadata" }
536
- },
537
- required: ["plan_id", "source_node_id", "target_node_id"]
538
- }
539
- },
540
- {
541
- name: "delete_dependency",
542
- description: "Delete a dependency edge",
543
- inputSchema: {
544
- type: "object",
545
- properties: {
546
- plan_id: { type: "string", description: "Plan ID" },
547
- dependency_id: { type: "string", description: "Dependency edge ID" }
548
- },
549
- required: ["plan_id", "dependency_id"]
550
- }
551
- },
552
- {
553
- name: "list_dependencies",
554
- description: "List all dependency edges in a plan",
555
- inputSchema: {
556
- type: "object",
557
- properties: {
558
- plan_id: { type: "string", description: "Plan ID" }
559
- },
560
- required: ["plan_id"]
561
- }
562
- },
563
- {
564
- name: "get_node_dependencies",
565
- description: "Get upstream and downstream dependencies for a node",
566
- inputSchema: {
567
- type: "object",
568
- properties: {
569
- plan_id: { type: "string", description: "Plan ID" },
570
- node_id: { type: "string", description: "Node ID" },
571
- direction: {
572
- type: "string",
573
- description: "Direction to query",
574
- enum: ["upstream", "downstream", "both"],
575
- default: "both"
576
- }
577
- },
578
- required: ["plan_id", "node_id"]
579
- }
580
- },
581
-
582
- // ===== RPI WORKFLOW =====
583
- {
584
- name: "create_rpi_chain",
585
- description: "Create a Research→Plan→Implement task chain with automatic dependency edges. The three tasks are linked: Research blocks Plan, Plan blocks Implement.",
586
- inputSchema: {
587
- type: "object",
588
- properties: {
589
- plan_id: { type: "string", description: "Plan ID" },
590
- title: { type: "string", description: "Base title for the chain (e.g. 'Auth refactor')" },
591
- description: { type: "string", description: "Description for the research task" },
592
- parent_id: { type: "string", description: "Parent node ID (optional, defaults to root)" }
593
- },
594
- required: ["plan_id", "title"]
595
- }
596
- },
597
-
598
- // ===== ANALYSIS TOOLS =====
599
- {
600
- name: "analyze_impact",
601
- description: "Analyze what happens if a node is delayed, blocked, or removed. Shows directly and transitively affected nodes.",
602
- inputSchema: {
603
- type: "object",
604
- properties: {
605
- plan_id: { type: "string", description: "Plan ID" },
606
- node_id: { type: "string", description: "Node ID to analyze" },
607
- scenario: {
608
- type: "string",
609
- description: "Impact scenario",
610
- enum: ["delay", "block", "remove"],
611
- default: "block"
612
- }
613
- },
614
- required: ["plan_id", "node_id"]
615
- }
616
- },
617
- {
618
- name: "get_critical_path",
619
- description: "Find the critical path (longest dependency chain) through incomplete tasks in a plan",
620
- inputSchema: {
621
- type: "object",
622
- properties: {
623
- plan_id: { type: "string", description: "Plan ID" }
624
- },
625
- required: ["plan_id"]
626
- }
627
- },
628
-
629
- // ===== PROGRESSIVE CONTEXT TOOLS =====
630
- {
631
- name: "get_task_context",
632
- description: "Get progressive context for a task at adjustable depth. This is the PRIMARY way to load context before starting work on a task.\n\nDepth levels:\n- 1: Task focus — node details + recent logs\n- 2: Local neighborhood — adds parent, siblings, direct dependencies\n- 3: Knowledge — adds plan-scoped knowledge entries\n- 4: Extended — adds plan overview, ancestry, goals, transitive dependencies\n\nFor RPI implement tasks, automatically includes research/plan outputs from the chain.",
633
- inputSchema: {
634
- type: "object",
635
- properties: {
636
- node_id: { type: "string", description: "Task/node ID to get context for" },
637
- depth: {
638
- type: "integer",
639
- description: "Context depth 1-4 (default 2). Start with 2, go deeper if needed.",
640
- minimum: 1,
641
- maximum: 4,
642
- default: 2
643
- },
644
- token_budget: {
645
- type: "integer",
646
- description: "Max estimated tokens (0 = unlimited). Use to stay within context window limits.",
647
- default: 0
648
- },
649
- log_limit: {
650
- type: "integer",
651
- description: "Max recent logs to include per node",
652
- default: 10
653
- },
654
- include_research: {
655
- type: "boolean",
656
- description: "Include research outputs from RPI chain siblings (for implement tasks)",
657
- default: true
658
- }
659
- },
660
- required: ["node_id"]
661
- }
662
- },
663
- {
664
- name: "suggest_next_tasks",
665
- description: "Suggest the next actionable tasks for a plan based on dependency analysis. Returns tasks where all upstream blockers are completed, prioritized by: RPI research tasks first, then by how many downstream tasks each unblocks.",
666
- inputSchema: {
667
- type: "object",
668
- properties: {
669
- plan_id: { type: "string", description: "Plan ID" },
670
- limit: {
671
- type: "integer",
672
- description: "Maximum suggestions to return",
673
- default: 5
674
- }
675
- },
676
- required: ["plan_id"]
677
- }
678
- },
679
-
680
- // ===== LOGGING TOOLS (Replaces Comments) =====
681
- {
682
- name: "add_log",
683
- description: "Add a log entry to a node (replaces comments)",
684
- inputSchema: {
685
- type: "object",
686
- properties: {
687
- plan_id: { type: "string", description: "Plan ID" },
688
- node_id: { type: "string", description: "Node ID" },
689
- content: { type: "string", description: "Log content" },
690
- log_type: {
691
- type: "string",
692
- description: "Type of log entry",
693
- enum: ["progress", "reasoning", "challenge", "decision", "comment"],
694
- default: "comment"
695
- },
696
- tags: {
697
- type: "array",
698
- description: "Tags for categorizing the log entry",
699
- items: { type: "string" }
700
- }
701
- },
702
- required: ["plan_id", "node_id", "content"]
703
- }
704
- },
705
- {
706
- name: "get_logs",
707
- description: "Get log entries for a node",
708
- inputSchema: {
709
- type: "object",
710
- properties: {
711
- plan_id: { type: "string", description: "Plan ID" },
712
- node_id: { type: "string", description: "Node ID" },
713
- log_type: {
714
- type: "string",
715
- description: "Filter by log type",
716
- enum: ["progress", "reasoning", "challenge", "decision", "comment"]
717
- },
718
- limit: {
719
- type: "integer",
720
- description: "Maximum number of logs to return",
721
- default: 50
722
- }
723
- },
724
- required: ["plan_id", "node_id"]
725
- }
726
- },
727
-
728
- // ===== BATCH OPERATIONS =====
729
- {
730
- name: "batch_update_nodes",
731
- description: "Update multiple nodes at once",
732
- inputSchema: {
733
- type: "object",
734
- properties: {
735
- plan_id: { type: "string", description: "Plan ID" },
736
- updates: {
737
- type: "array",
738
- description: "List of node updates",
739
- items: {
740
- type: "object",
741
- properties: {
742
- node_id: { type: "string", description: "Node ID" },
743
- status: {
744
- type: "string",
745
- enum: ["not_started", "in_progress", "completed", "blocked", "plan_ready"]
746
- },
747
- title: { type: "string" },
748
- description: { type: "string" }
749
- },
750
- required: ["node_id"]
751
- }
752
- }
753
- },
754
- required: ["plan_id", "updates"]
755
- }
756
- },
757
-
758
- // ===== PLAN STRUCTURE & SUMMARY =====
759
- {
760
- name: "get_plan_structure",
761
- description: "Get the hierarchical structure of a plan with minimal fields (id, parent_id, node_type, title, status, order_index). Use get_task_context for detailed information about specific nodes.",
762
- inputSchema: {
763
- type: "object",
764
- properties: {
765
- plan_id: { type: "string", description: "Plan ID" },
766
- include_details: {
767
- type: "boolean",
768
- description: "Include full node details (description, context, agent_instructions, etc.). Default is false for efficient context usage.",
769
- default: false
770
- }
771
- },
772
- required: ["plan_id"]
773
- }
774
- },
775
- {
776
- name: "get_plan_summary",
777
- description: "Get a comprehensive summary with statistics",
778
- inputSchema: {
779
- type: "object",
780
- properties: {
781
- plan_id: { type: "string", description: "Plan ID" }
782
- },
783
- required: ["plan_id"]
784
- }
785
- },
786
-
787
- // ===== AGENT CONTEXT TOOLS (Leaf-up context loading) =====
788
- {
789
- name: "get_plan_context",
790
- description: "Get plan-level context overview. Returns plan details, phase summaries (not full tree), linked goals, and organization. Use get_task_context for task-focused work.",
791
- inputSchema: {
792
- type: "object",
793
- properties: {
794
- plan_id: {
795
- type: "string",
796
- description: "Plan ID"
797
- },
798
- include_knowledge: {
799
- type: "boolean",
800
- description: "Include knowledge entries",
801
- default: true
802
- }
803
- },
804
- required: ["plan_id"]
805
- }
806
- },
807
-
808
- // ===== ORGANIZATION TOOLS =====
809
- {
810
- name: "list_organizations",
811
- description: "List all organizations the user is a member of",
812
- inputSchema: {
813
- type: "object",
814
- properties: {}
815
- }
816
- },
817
- {
818
- name: "get_organization",
819
- description: "Get organization details including member count and plan count",
820
- inputSchema: {
821
- type: "object",
822
- properties: {
823
- organization_id: { type: "string", description: "Organization ID" }
824
- },
825
- required: ["organization_id"]
826
- }
827
- },
828
- {
829
- name: "create_organization",
830
- description: "Create a new organization. You become the owner.",
831
- inputSchema: {
832
- type: "object",
833
- properties: {
834
- name: { type: "string", description: "Organization name" },
835
- description: { type: "string", description: "Organization description" },
836
- slug: { type: "string", description: "URL-friendly slug (auto-generated if not provided)" }
837
- },
838
- required: ["name"]
839
- }
840
- },
841
- {
842
- name: "update_organization",
843
- description: "Update organization details (owner only)",
844
- inputSchema: {
845
- type: "object",
846
- properties: {
847
- organization_id: { type: "string", description: "Organization ID" },
848
- name: { type: "string", description: "New name" },
849
- description: { type: "string", description: "New description" }
850
- },
851
- required: ["organization_id"]
852
- }
853
- },
854
-
855
- // ===== GOAL TOOLS =====
856
- {
857
- name: "list_goals",
858
- description: "List goals. By default returns only active goals — set include_inactive to true to see all.",
859
- inputSchema: {
860
- type: "object",
861
- properties: {
862
- organization_id: { type: "string", description: "Filter by organization ID" },
863
- status: {
864
- type: "string",
865
- description: "Filter by status",
866
- enum: ["active", "achieved", "at_risk", "abandoned"]
867
- },
868
- include_inactive: {
869
- type: "boolean",
870
- description: "If true, include achieved/paused/abandoned goals (default: false)",
871
- default: false
872
- }
873
- }
874
- }
875
- },
876
- {
877
- name: "get_goal",
878
- description: "Get goal details including linked plans",
879
- inputSchema: {
880
- type: "object",
881
- properties: {
882
- goal_id: { type: "string", description: "Goal ID" }
883
- },
884
- required: ["goal_id"]
885
- }
886
- },
887
- {
888
- name: "create_goal",
889
- description: "Create a new goal within an organization",
890
- inputSchema: {
891
- type: "object",
892
- properties: {
893
- organization_id: { type: "string", description: "Organization ID" },
894
- title: { type: "string", description: "Goal title" },
895
- description: { type: "string", description: "Goal description" },
896
- type: { type: "string", enum: ["outcome", "constraint", "metric", "principle"], description: "Goal type (default: outcome)" },
897
- success_criteria: { type: "object", description: "Success criteria as JSON" },
898
- priority: { type: "number", description: "Priority (higher = more important, default: 0)" },
899
- parent_goal_id: { type: "string", description: "Parent goal ID for hierarchy" }
900
- },
901
- required: ["organization_id", "title"]
902
- }
903
- },
904
- {
905
- name: "update_goal",
906
- description: "Update goal details or status",
907
- inputSchema: {
908
- type: "object",
909
- properties: {
910
- goal_id: { type: "string", description: "Goal ID" },
911
- title: { type: "string", description: "New title" },
912
- description: { type: "string", description: "New description" },
913
- type: { type: "string", enum: ["outcome", "constraint", "metric", "principle"], description: "Goal type" },
914
- status: {
915
- type: "string",
916
- description: "New status",
917
- enum: ["active", "achieved", "paused", "abandoned"]
918
- },
919
- success_criteria: { type: "object", description: "Success criteria as JSON" },
920
- priority: { type: "number", description: "Priority (higher = more important)" },
921
- parent_goal_id: { type: "string", description: "Parent goal ID for hierarchy" }
922
- },
923
- required: ["goal_id"]
924
- }
925
- },
926
- {
927
- name: "link_plan_to_goal",
928
- description: "Link a plan to a goal (shows the plan contributes to this goal)",
929
- inputSchema: {
930
- type: "object",
931
- properties: {
932
- goal_id: { type: "string", description: "Goal ID" },
933
- plan_id: { type: "string", description: "Plan ID to link" }
934
- },
935
- required: ["goal_id", "plan_id"]
936
- }
937
- },
938
- {
939
- name: "unlink_plan_from_goal",
940
- description: "Remove a plan-goal link",
941
- inputSchema: {
942
- type: "object",
943
- properties: {
944
- goal_id: { type: "string", description: "Goal ID" },
945
- plan_id: { type: "string", description: "Plan ID to unlink" }
946
- },
947
- required: ["goal_id", "plan_id"]
948
- }
949
- },
950
-
951
- // ===== CROSS-PLAN & EXTERNAL DEPENDENCY TOOLS =====
952
- {
953
- name: "create_cross_plan_dependency",
954
- description: "Create a dependency edge between nodes in different plans. Use when a task in one plan blocks or requires a task in another plan.",
955
- inputSchema: {
956
- type: "object",
957
- properties: {
958
- source_node_id: { type: "string", description: "Source node ID (the blocker/prerequisite)" },
959
- target_node_id: { type: "string", description: "Target node ID (the blocked/dependent task)" },
960
- dependency_type: { type: "string", enum: ["blocks", "requires", "relates_to"], default: "blocks", description: "Edge type (default: blocks)" },
961
- weight: { type: "number", description: "Edge weight (default 1)" }
962
- },
963
- required: ["source_node_id", "target_node_id"]
964
- }
965
- },
966
- {
967
- name: "list_cross_plan_dependencies",
968
- description: "List all dependency edges that cross plan boundaries between specified plans.",
969
- inputSchema: {
970
- type: "object",
971
- properties: {
972
- plan_ids: {
973
- type: "array",
974
- items: { type: "string" },
975
- description: "Plan IDs to check for cross-plan edges (at least 2)"
976
- }
977
- },
978
- required: ["plan_ids"]
979
- }
980
- },
981
- {
982
- name: "create_external_dependency",
983
- description: "Create an external dependency node representing a blocker outside the system (vendor API, legal approval, etc.). Optionally blocks a target task.",
984
- inputSchema: {
985
- type: "object",
986
- properties: {
987
- plan_id: { type: "string", description: "Plan to add the external dependency to" },
988
- title: { type: "string", description: "External dependency title (e.g., 'Waiting for vendor API access')" },
989
- description: { type: "string", description: "Details about the external dependency" },
990
- url: { type: "string", description: "URL reference (ticket, docs, etc.)" },
991
- blocks_node_id: { type: "string", description: "Node ID that this external dep blocks" }
992
- },
993
- required: ["plan_id", "title"]
994
- }
995
- },
996
-
997
- // ===== GOAL-DEPENDENCY TOOLS =====
998
- {
999
- name: "goal_path",
1000
- description: "Get the full dependency path to a goal — all tasks that contribute to achieving it (direct achievers + their upstream blockers). Shows completion stats and which tasks are blocking progress.",
1001
- inputSchema: {
1002
- type: "object",
1003
- properties: {
1004
- goal_id: { type: "string", description: "Goal ID" },
1005
- max_depth: { type: "number", description: "Max traversal depth (default 20)" }
1006
- },
1007
- required: ["goal_id"]
1008
- }
1009
- },
1010
- {
1011
- name: "goal_progress",
1012
- description: "Get goal progress calculated from its dependency graph. Returns overall completion percentage and direct achiever progress.",
1013
- inputSchema: {
1014
- type: "object",
1015
- properties: {
1016
- goal_id: { type: "string", description: "Goal ID" }
1017
- },
1018
- required: ["goal_id"]
1019
- }
1020
- },
1021
- {
1022
- name: "add_achiever",
1023
- description: "Link a task to a goal via an 'achieves' dependency edge. This declares that completing this task contributes to achieving the goal.",
1024
- inputSchema: {
1025
- type: "object",
1026
- properties: {
1027
- goal_id: { type: "string", description: "Goal ID" },
1028
- node_id: { type: "string", description: "Task/node ID that achieves this goal" },
1029
- weight: { type: "number", description: "Edge weight for critical path (default 1)" }
1030
- },
1031
- required: ["goal_id", "node_id"]
1032
- }
1033
- },
1034
- {
1035
- name: "remove_achiever",
1036
- description: "Remove an achieves edge between a task and a goal",
1037
- inputSchema: {
1038
- type: "object",
1039
- properties: {
1040
- goal_id: { type: "string", description: "Goal ID" },
1041
- dependency_id: { type: "string", description: "Dependency edge ID to remove" }
1042
- },
1043
- required: ["goal_id", "dependency_id"]
1044
- }
1045
- },
1046
- {
1047
- name: "goal_knowledge_gaps",
1048
- description: "Detect knowledge gaps for a goal — checks which tasks on the goal's dependency path lack relevant knowledge in the temporal knowledge graph. Useful for identifying where research is needed before implementation.",
1049
- inputSchema: {
1050
- type: "object",
1051
- properties: {
1052
- goal_id: { type: "string", description: "Goal ID" }
1053
- },
1054
- required: ["goal_id"]
1055
- }
1056
- },
1057
-
1058
- // ===== GRAPHITI KNOWLEDGE GRAPH TOOLS =====
1059
- {
1060
- name: "add_learning",
1061
- description: "Record a knowledge episode to the temporal knowledge graph. Use this after research, when making decisions, or discovering important context. Graphiti automatically extracts entities and relationships. The knowledge persists across plans and sessions.",
1062
- inputSchema: {
1063
- type: "object",
1064
- properties: {
1065
- content: { type: "string", description: "The knowledge content — be detailed. Include context, reasoning, and conclusions." },
1066
- title: { type: "string", description: "Short title/name for the episode" },
1067
- entry_type: { type: "string", enum: ["decision", "learning", "context", "constraint"], description: "Type of knowledge" },
1068
- plan_id: { type: "string", description: "Plan ID this knowledge relates to (optional)" },
1069
- node_id: { type: "string", description: "Node/task ID this knowledge relates to (optional)" }
1070
- },
1071
- required: ["content"]
1072
- }
1073
- },
1074
- {
1075
- name: "recall_knowledge",
1076
- description: "Search the temporal knowledge graph for relevant facts, decisions, and learnings. Searches across ALL plans in the organization. Use before starting work or making decisions.",
1077
- inputSchema: {
1078
- type: "object",
1079
- properties: {
1080
- query: { type: "string", description: "What to search for — be specific" },
1081
- max_results: { type: "number", description: "Maximum results (default 10)", default: 10 }
1082
- },
1083
- required: ["query"]
1084
- }
1085
- },
1086
- {
1087
- name: "find_entities",
1088
- description: "Search for entities (technologies, people, patterns, constraints) in the knowledge graph. Returns entity nodes with their relationships.",
1089
- inputSchema: {
1090
- type: "object",
1091
- properties: {
1092
- query: { type: "string", description: "Entity search query" },
1093
- max_results: { type: "number", description: "Maximum results (default 10)", default: 10 }
1094
- },
1095
- required: ["query"]
1096
- }
1097
- },
1098
21
 
1099
- {
1100
- name: "check_contradictions",
1101
- description: "Check if knowledge about a topic has changed over time. Returns current facts and any superseded (outdated) facts. Useful before making decisions based on past knowledge — ensures you're working with the latest information.",
1102
- inputSchema: {
1103
- type: "object",
1104
- properties: {
1105
- query: { type: "string", description: "Topic to check for contradictions" },
1106
- max_results: { type: "number", description: "Maximum results (default 10)", default: 10 }
1107
- },
1108
- required: ["query"]
1109
- }
1110
- },
1111
- {
1112
- name: "get_recent_episodes",
1113
- description: "Get recent knowledge episodes from the temporal graph. Returns the latest episodes (learnings, decisions, context) across all plans. Useful to understand what has been learned recently or to review your own work session history.",
1114
- inputSchema: {
1115
- type: "object",
1116
- properties: {
1117
- max_episodes: { type: "number", description: "Maximum episodes to return (default 20)", default: 20 }
1118
- }
1119
- }
1120
- },
22
+ if (process.env.NODE_ENV === 'development') {
23
+ console.error(`Setting up MCP tools (${bdiToolDefinitions.length} BDI tools)`);
24
+ }
1121
25
 
1122
- // ===== HELPER / GUIDANCE TOOLS =====
1123
- {
1124
- name: "get_started",
1125
- description: "Get guidance on how to use AgentPlanner effectively. Returns an overview of the system and recommended workflows for common tasks. Call this when you're new to AgentPlanner or need to understand how to approach a task.",
1126
- inputSchema: {
1127
- type: "object",
1128
- properties: {
1129
- topic: {
1130
- type: "string",
1131
- enum: ["overview", "planning", "execution", "knowledge", "collaboration"],
1132
- description: "Specific topic to learn about: 'overview' (system intro), 'planning' (creating plans), 'execution' (working through tasks), 'knowledge' (storing decisions/learnings), 'collaboration' (working with others)",
1133
- default: "overview"
1134
- }
1135
- }
1136
- }
1137
- },
1138
- ]
1139
- };
26
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
27
+ return { tools: bdiToolDefinitions };
1140
28
  });
1141
-
1142
- // Handler for calling tools
29
+
1143
30
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
1144
31
  const { name, arguments: args } = request.params;
1145
-
1146
- // Only log in development mode
32
+
1147
33
  if (process.env.NODE_ENV === 'development') {
1148
- console.error(`Calling tool: ${name} with arguments:`, args);
34
+ console.error(`Calling tool: ${name}`);
1149
35
  }
1150
-
1151
- try {
1152
- // ========================================
1153
- // QUICK ACTIONS IMPLEMENTATIONS
1154
- // ========================================
1155
-
1156
- if (name === "quick_plan") {
1157
- const { title, description, tasks, goal_id, organization_id } = args;
1158
-
1159
- if (!tasks || tasks.length === 0) {
1160
- return formatResponse({
1161
- error: "At least one task is required",
1162
- suggestion: "Provide an array of task titles in the 'tasks' parameter"
1163
- });
1164
- }
1165
-
1166
- // Create the plan
1167
- const planData = { title };
1168
- if (description) planData.description = description;
1169
- if (organization_id) planData.organization_id = organization_id;
1170
-
1171
- const plan = await apiClient.plans.createPlan(planData);
1172
-
1173
- // Get the root node
1174
- const nodes = await apiClient.nodes.getNodes(plan.id);
1175
- const rootNode = nodes.find(n => n.node_type === 'root') || nodes[0];
1176
-
1177
- // Create a phase for the tasks
1178
- const phase = await apiClient.nodes.createNode(plan.id, {
1179
- parent_id: rootNode.id,
1180
- node_type: 'phase',
1181
- title: 'Tasks',
1182
- status: 'not_started',
1183
- order_index: 0
1184
- });
1185
-
1186
- // Create tasks
1187
- const createdTasks = [];
1188
- for (let i = 0; i < tasks.length; i++) {
1189
- const task = await apiClient.nodes.createNode(plan.id, {
1190
- parent_id: phase.id,
1191
- node_type: 'task',
1192
- title: tasks[i],
1193
- status: 'not_started',
1194
- order_index: i
1195
- });
1196
- createdTasks.push({ id: task.id, title: task.title });
1197
- }
1198
-
1199
- // Link to goal if provided
1200
- if (goal_id) {
1201
- try {
1202
- await apiClient.goals.linkPlan(goal_id, plan.id);
1203
- } catch (e) {
1204
- // Goal linking failed, continue anyway
1205
- }
1206
- }
1207
-
1208
- return formatResponse({
1209
- success: true,
1210
- message: `Plan "${title}" created with ${tasks.length} tasks`,
1211
- plan_id: plan.id,
1212
- plan_url: buildPlanUrl(plan.id),
1213
- phase_id: phase.id,
1214
- task_ids: createdTasks.map(t => t.id),
1215
- tasks: createdTasks,
1216
- next_steps: [
1217
- "Use quick_status to update task progress",
1218
- "Use quick_log to document your work",
1219
- "Use get_plan_context to load full plan details"
1220
- ]
1221
- });
1222
- }
1223
-
1224
- if (name === "quick_task") {
1225
- const { plan_id, title, description, phase_id, agent_instructions } = args;
1226
-
1227
- // Get plan structure to find the phase
1228
- const nodes = await apiClient.nodes.getNodes(plan_id);
1229
- const flatNodes = flattenNodes(nodes);
1230
-
1231
- // Find target phase
1232
- let targetPhaseId = phase_id;
1233
- if (!targetPhaseId) {
1234
- // Find first phase
1235
- const phases = flatNodes.filter(n => n.node_type === 'phase');
1236
- if (phases.length > 0) {
1237
- targetPhaseId = phases[0].id;
1238
- } else {
1239
- // No phase exists, create one
1240
- const rootNode = flatNodes.find(n => n.node_type === 'root') || nodes[0];
1241
- const newPhase = await apiClient.nodes.createNode(plan_id, {
1242
- parent_id: rootNode.id,
1243
- node_type: 'phase',
1244
- title: 'Tasks',
1245
- status: 'not_started',
1246
- order_index: 0
1247
- });
1248
- targetPhaseId = newPhase.id;
1249
- }
1250
- }
1251
-
1252
- // Get task count in phase for order_index
1253
- const phaseTasks = flatNodes.filter(n => n.parent_id === targetPhaseId && n.node_type === 'task');
1254
-
1255
- // Create the task
1256
- const taskData = {
1257
- parent_id: targetPhaseId,
1258
- node_type: 'task',
1259
- title,
1260
- status: 'not_started',
1261
- order_index: phaseTasks.length
1262
- };
1263
- if (description) taskData.description = description;
1264
- if (agent_instructions) taskData.agent_instructions = agent_instructions;
1265
-
1266
- const task = await apiClient.nodes.createNode(plan_id, taskData);
1267
-
1268
- return formatResponse({
1269
- success: true,
1270
- message: `Task "${title}" added to plan`,
1271
- task_id: task.id,
1272
- plan_id: plan_id,
1273
- phase_id: targetPhaseId,
1274
- task_url: buildTaskUrl(plan_id, task.id),
1275
- next_steps: [
1276
- "Use quick_status to mark as in_progress when you start",
1277
- "Use quick_log to document progress"
1278
- ]
1279
- });
1280
- }
1281
-
1282
- if (name === "quick_status") {
1283
- const { task_id, plan_id, status, note } = args;
1284
-
1285
- // Update the task status
1286
- const updateData = { status };
1287
- const updated = await apiClient.nodes.updateNode(plan_id, task_id, updateData);
1288
-
1289
- // Add a log entry if note provided or if blocking
1290
- if (note || status === 'blocked') {
1291
- const logMessage = note || (status === 'blocked' ? 'Task blocked - needs attention' : `Status changed to ${status}`);
1292
- try {
1293
- await apiClient.logs.addLogEntry(plan_id, task_id, {
1294
- type: status === 'blocked' ? 'blocker' : 'progress',
1295
- content: logMessage
1296
- });
1297
- } catch (e) {
1298
- // Log failed, continue
1299
- }
1300
- }
1301
-
1302
- // Get next tasks for suggestion
1303
- let nextTasks = [];
1304
- try {
1305
- const nodes = await apiClient.nodes.getNodes(plan_id);
1306
- const flatNodes = flattenNodes(nodes);
1307
- nextTasks = flatNodes
1308
- .filter(n => n.node_type === 'task' && n.status === 'not_started')
1309
- .slice(0, 3)
1310
- .map(n => ({ id: n.id, title: n.title }));
1311
- } catch (e) {
1312
- // Failed to get next tasks, continue
1313
- }
1314
-
1315
- const response = {
1316
- success: true,
1317
- message: `Task status updated to "${status}"`,
1318
- task_id,
1319
- plan_id,
1320
- new_status: status
1321
- };
1322
-
1323
- if (status === 'completed' && nextTasks.length > 0) {
1324
- response.next_tasks = nextTasks;
1325
- response.suggestion = "Here are the next tasks to work on";
1326
- } else if (status === 'blocked') {
1327
- response.suggestion = "Task marked as blocked. A human will be notified to help unblock.";
1328
- }
1329
-
1330
- return formatResponse(response);
1331
- }
1332
-
1333
- if (name === "quick_log") {
1334
- const { task_id, plan_id, message, log_type = 'progress' } = args;
1335
-
1336
- const logEntry = await apiClient.logs.addLogEntry(plan_id, task_id, {
1337
- type: log_type,
1338
- content: message
1339
- });
1340
-
1341
- return formatResponse({
1342
- success: true,
1343
- message: "Progress logged",
1344
- log_id: logEntry.id,
1345
- task_id,
1346
- plan_id,
1347
- logged: message,
1348
- tip: "Good practice! Logging helps humans follow your work."
1349
- });
1350
- }
1351
-
1352
- // ========================================
1353
- // CONTEXT LOADING IMPLEMENTATIONS
1354
- // ========================================
1355
-
1356
- if (name === "get_my_tasks") {
1357
- const { plan_id, status = ["blocked", "in_progress"] } = args;
1358
-
1359
- const tasks = {
1360
- retrieved_at: new Date().toISOString(),
1361
- needs_attention: [],
1362
- ready_to_start: []
1363
- };
1364
-
1365
- // Get plans to check
1366
- let plansToCheck = [];
1367
- if (plan_id) {
1368
- plansToCheck = [{ id: plan_id }];
1369
- } else {
1370
- plansToCheck = await apiClient.plans.getPlans();
1371
- }
1372
-
1373
- for (const plan of plansToCheck.slice(0, 10)) { // Limit to 10 plans
1374
- try {
1375
- const nodes = await apiClient.nodes.getNodes(plan.id);
1376
- const flatNodes = flattenNodes(nodes);
1377
-
1378
- const matchingTasks = flatNodes
1379
- .filter(n => n.node_type === 'task' && status.includes(n.status))
1380
- .map(n => ({
1381
- id: n.id,
1382
- title: n.title,
1383
- status: n.status,
1384
- plan_id: plan.id,
1385
- plan_title: plan.title
1386
- }));
1387
-
1388
- tasks.needs_attention.push(...matchingTasks);
1389
-
1390
- // Also get a few ready-to-start tasks
1391
- const readyTasks = flatNodes
1392
- .filter(n => n.node_type === 'task' && n.status === 'not_started')
1393
- .slice(0, 3)
1394
- .map(n => ({
1395
- id: n.id,
1396
- title: n.title,
1397
- plan_id: plan.id,
1398
- plan_title: plan.title
1399
- }));
1400
-
1401
- tasks.ready_to_start.push(...readyTasks);
1402
- } catch (e) {
1403
- // Skip this plan
1404
- }
1405
- }
1406
-
1407
- tasks.summary = {
1408
- blocked: tasks.needs_attention.filter(t => t.status === 'blocked').length,
1409
- in_progress: tasks.needs_attention.filter(t => t.status === 'in_progress').length,
1410
- ready: tasks.ready_to_start.length
1411
- };
1412
-
1413
- return formatResponse(tasks);
1414
- }
1415
-
1416
- // add_learning handled in GRAPHITI KNOWLEDGE GRAPH HANDLERS section below
1417
-
1418
- // ========================================
1419
- // MARKDOWN EXPORT
1420
- // ========================================
1421
-
1422
- if (name === "export_plan_markdown") {
1423
- const { plan_id, include_descriptions = true, include_status = true } = args;
1424
-
1425
- const plan = await apiClient.plans.getPlan(plan_id);
1426
- const nodes = await apiClient.nodes.getNodes(plan_id);
1427
-
1428
- let markdown = `# ${plan.title}\n\n`;
1429
- if (plan.description) {
1430
- markdown += `${plan.description}\n\n`;
1431
- }
1432
-
1433
- const statusEmoji = {
1434
- not_started: '⬜',
1435
- in_progress: '🔄',
1436
- completed: '✅',
1437
- blocked: '🚫',
1438
- cancelled: '❌'
1439
- };
1440
-
1441
- // Process nodes recursively
1442
- const processNode = (node, depth = 0) => {
1443
- const indent = ' '.repeat(depth);
1444
- const status = include_status ? (statusEmoji[node.status] || '⬜') + ' ' : '';
1445
-
1446
- if (node.node_type === 'phase') {
1447
- markdown += `\n${indent}## ${node.title}\n`;
1448
- if (include_descriptions && node.description) {
1449
- markdown += `${indent}${node.description}\n`;
1450
- }
1451
- } else if (node.node_type === 'task') {
1452
- markdown += `${indent}- ${status}${node.title}\n`;
1453
- if (include_descriptions && node.description) {
1454
- markdown += `${indent} _${node.description}_\n`;
1455
- }
1456
- } else if (node.node_type === 'milestone') {
1457
- markdown += `${indent}- 🎯 ${status}**${node.title}**\n`;
1458
- }
1459
-
1460
- if (node.children) {
1461
- node.children.forEach(child => processNode(child, depth + 1));
1462
- }
1463
- };
1464
-
1465
- // Start from root's children
1466
- if (nodes.length > 0 && nodes[0].children) {
1467
- nodes[0].children.forEach(child => processNode(child, 0));
1468
- }
1469
-
1470
- return formatResponse({
1471
- plan_id,
1472
- title: plan.title,
1473
- markdown,
1474
- tip: "You can save this to a file or share it as text"
1475
- });
1476
- }
1477
-
1478
- if (name === "import_plan_markdown") {
1479
- const { markdown, title: providedTitle, goal_id } = args;
1480
-
1481
- // Parse markdown
1482
- const lines = markdown.split('\n').map(l => l.trim()).filter(l => l);
1483
-
1484
- let planTitle = providedTitle;
1485
- let planDescription = '';
1486
- const phases = [];
1487
- let currentPhase = null;
1488
-
1489
- for (const line of lines) {
1490
- // H1 = Plan title
1491
- if (line.startsWith('# ')) {
1492
- if (!planTitle) {
1493
- planTitle = line.slice(2).trim();
1494
- }
1495
- }
1496
- // H2 = Phase
1497
- else if (line.startsWith('## ')) {
1498
- const phaseTitle = line.slice(3).trim();
1499
- currentPhase = { title: phaseTitle, tasks: [] };
1500
- phases.push(currentPhase);
1501
- }
1502
- // List item = Task (with optional status emoji)
1503
- else if (line.startsWith('- ') || line.startsWith('* ')) {
1504
- let taskTitle = line.slice(2).trim();
1505
- let taskStatus = 'not_started';
1506
-
1507
- // Parse status from emoji
1508
- if (taskTitle.startsWith('✅')) {
1509
- taskStatus = 'completed';
1510
- taskTitle = taskTitle.slice(1).trim();
1511
- } else if (taskTitle.startsWith('🔄')) {
1512
- taskStatus = 'in_progress';
1513
- taskTitle = taskTitle.slice(1).trim();
1514
- } else if (taskTitle.startsWith('🚫')) {
1515
- taskStatus = 'blocked';
1516
- taskTitle = taskTitle.slice(1).trim();
1517
- } else if (taskTitle.startsWith('⬜')) {
1518
- taskTitle = taskTitle.slice(1).trim();
1519
- }
1520
-
1521
- // Skip milestone markers for now
1522
- if (taskTitle.startsWith('🎯')) {
1523
- taskTitle = taskTitle.slice(1).trim().replace(/\*\*/g, '');
1524
- }
1525
-
1526
- if (currentPhase) {
1527
- currentPhase.tasks.push({ title: taskTitle, status: taskStatus });
1528
- } else {
1529
- // No phase yet, create a default one
1530
- currentPhase = { title: 'Tasks', tasks: [{ title: taskTitle, status: taskStatus }] };
1531
- phases.push(currentPhase);
1532
- }
1533
- }
1534
- // Regular text after title = description
1535
- else if (planTitle && !currentPhase && !line.startsWith('#')) {
1536
- planDescription += (planDescription ? ' ' : '') + line;
1537
- }
1538
- }
1539
-
1540
- if (!planTitle) {
1541
- return formatResponse({
1542
- error: "Could not extract plan title",
1543
- suggestion: "Add a '# Title' heading at the start, or provide the 'title' parameter"
1544
- });
1545
- }
1546
-
1547
- if (phases.length === 0) {
1548
- return formatResponse({
1549
- error: "No phases or tasks found",
1550
- suggestion: "Use '## Phase Name' for phases and '- Task name' for tasks"
1551
- });
1552
- }
1553
-
1554
- // Create the plan
1555
- const planData = { title: planTitle };
1556
- if (planDescription) planData.description = planDescription;
1557
-
1558
- const plan = await apiClient.plans.createPlan(planData);
1559
-
1560
- // Get root node
1561
- const nodes = await apiClient.nodes.getNodes(plan.id);
1562
- const rootNode = nodes.find(n => n.node_type === 'root') || nodes[0];
1563
-
1564
- // Create phases and tasks
1565
- const createdPhases = [];
1566
- const createdTasks = [];
1567
-
1568
- for (let i = 0; i < phases.length; i++) {
1569
- const phaseData = phases[i];
1570
-
1571
- const phase = await apiClient.nodes.createNode(plan.id, {
1572
- parent_id: rootNode.id,
1573
- node_type: 'phase',
1574
- title: phaseData.title,
1575
- status: 'not_started',
1576
- order_index: i
1577
- });
1578
- createdPhases.push({ id: phase.id, title: phase.title });
1579
-
1580
- for (let j = 0; j < phaseData.tasks.length; j++) {
1581
- const taskData = phaseData.tasks[j];
1582
-
1583
- const task = await apiClient.nodes.createNode(plan.id, {
1584
- parent_id: phase.id,
1585
- node_type: 'task',
1586
- title: taskData.title,
1587
- status: taskData.status,
1588
- order_index: j
1589
- });
1590
- createdTasks.push({ id: task.id, title: task.title, status: task.status });
1591
- }
1592
- }
1593
-
1594
- // Link to goal if provided
1595
- if (goal_id) {
1596
- try {
1597
- await apiClient.goals.linkPlan(goal_id, plan.id);
1598
- } catch (e) {}
1599
- }
1600
-
1601
- return formatResponse({
1602
- success: true,
1603
- message: `Plan "${planTitle}" created from markdown with ${phases.length} phases and ${createdTasks.length} tasks`,
1604
- plan_id: plan.id,
1605
- plan_url: buildPlanUrl(plan.id),
1606
- phases: createdPhases,
1607
- tasks: createdTasks,
1608
- next_steps: [
1609
- "Use get_plan_context to review the imported plan",
1610
- "Use quick_status to update task progress"
1611
- ]
1612
- });
1613
- }
1614
-
1615
- // ===== UNIFIED SEARCH TOOL =====
1616
- if (name === "search") {
1617
- const { scope, scope_id, query, filters = {} } = args;
1618
-
1619
- let results = [];
1620
-
1621
- switch (scope) {
1622
- case "global":
1623
- // Global search across all plans
1624
- const searchWrapper = require('./tools/search-wrapper');
1625
- results = await searchWrapper.globalSearch(query);
1626
- break;
1627
-
1628
- case "plans":
1629
- // Search only in plan titles/descriptions
1630
- const plans = await apiClient.plans.getPlans();
1631
-
1632
- // Handle wildcard queries
1633
- if (query === '*' || query === '' || !query) {
1634
- // Return all plans (with optional status filter)
1635
- results = plans.filter(plan =>
1636
- !filters.status || plan.status === filters.status
1637
- );
1638
- } else {
1639
- // Normal search
1640
- const queryLower = query.toLowerCase();
1641
- results = plans.filter(plan => {
1642
- const titleMatch = plan.title.toLowerCase().includes(queryLower);
1643
- const descMatch = plan.description?.toLowerCase().includes(queryLower);
1644
- const statusMatch = !filters.status || plan.status === filters.status;
1645
- return (titleMatch || descMatch) && statusMatch;
1646
- });
1647
- }
1648
- break;
1649
-
1650
- case "plan":
1651
- // Search within a specific plan
1652
- if (!scope_id) {
1653
- throw new Error("scope_id (plan_id) is required when scope is 'plan'");
1654
- }
1655
- const searchWrapperPlan = require('./tools/search-wrapper');
1656
- results = await searchWrapperPlan.searchPlan(scope_id, query);
1657
- break;
1658
-
1659
- case "node":
1660
- // Search within a specific node's children
1661
- if (!scope_id) {
1662
- throw new Error("scope_id (node_id) is required when scope is 'node'");
1663
- }
1664
- // This would need a specific implementation
1665
- results = [];
1666
- break;
1667
-
1668
- default:
1669
- // Default to global search
1670
- const searchWrapperDefault = require('./tools/search-wrapper');
1671
- results = await searchWrapperDefault.globalSearch(query);
1672
- }
1673
-
1674
- // Apply filters
1675
- if (filters.type) {
1676
- results = results.filter(item => item.type === filters.type);
1677
- }
1678
- if (filters.limit) {
1679
- results = results.slice(0, filters.limit);
1680
- }
1681
-
1682
- return formatResponse({
1683
- query,
1684
- scope,
1685
- scope_id,
1686
- filters,
1687
- count: results.length,
1688
- results
1689
- });
1690
- }
1691
-
1692
- // ===== PLAN MANAGEMENT =====
1693
- if (name === "list_plans") {
1694
- const { status, include_completed } = args;
1695
- const plans = await apiClient.plans.getPlans();
1696
- let filteredPlans;
1697
- if (status) {
1698
- filteredPlans = plans.filter(p => p.status === status);
1699
- } else if (!include_completed) {
1700
- filteredPlans = plans.filter(p => p.status !== 'completed' && p.status !== 'archived');
1701
- } else {
1702
- filteredPlans = plans;
1703
- }
1704
- return formatResponse(filteredPlans);
1705
- }
1706
-
1707
- if (name === "create_plan") {
1708
- const result = await apiClient.plans.createPlan(args);
1709
- return formatResponse(result);
1710
- }
1711
-
1712
- if (name === "update_plan") {
1713
- const { plan_id, ...planData } = args;
1714
- const result = await apiClient.plans.updatePlan(plan_id, planData);
1715
- return formatResponse(result);
1716
- }
1717
-
1718
- if (name === "delete_plan") {
1719
- const { plan_id } = args;
1720
- await apiClient.plans.deletePlan(plan_id);
1721
- return formatResponse({
1722
- success: true,
1723
- message: `Plan ${plan_id} deleted successfully`
1724
- });
1725
- }
1726
-
1727
- if (name === "share_plan") {
1728
- const { plan_id, visibility = "public", github_repo_owner, github_repo_name } = args;
1729
-
1730
- const visibilityData = { visibility };
1731
- if (github_repo_owner) visibilityData.github_repo_owner = github_repo_owner;
1732
- if (github_repo_name) visibilityData.github_repo_name = github_repo_name;
1733
-
1734
- const result = await apiClient.plans.updateVisibility(plan_id, visibilityData);
1735
-
1736
- const shareUrl = visibility === "public"
1737
- ? buildPlanUrl(plan_id)
1738
- : null;
1739
-
1740
- return formatResponse({
1741
- success: true,
1742
- plan_id: plan_id,
1743
- visibility: result.visibility,
1744
- is_public: result.is_public,
1745
- share_url: shareUrl,
1746
- github_repo_owner: result.github_repo_owner,
1747
- github_repo_name: result.github_repo_name,
1748
- message: visibility === "public"
1749
- ? `Plan is now public. Share URL: ${shareUrl}`
1750
- : `Plan is now private.`
1751
- });
1752
- }
1753
-
1754
- // ===== NODE MANAGEMENT =====
1755
- if (name === "create_node") {
1756
- const { plan_id, ...nodeData } = args;
1757
- const result = await apiClient.nodes.createNode(plan_id, nodeData);
1758
- return formatResponse(result);
1759
- }
1760
-
1761
- if (name === "update_node") {
1762
- const { plan_id, node_id, ...nodeData } = args;
1763
- const result = await apiClient.nodes.updateNode(plan_id, node_id, nodeData);
1764
- return formatResponse(result);
1765
- }
1766
-
1767
- if (name === "delete_node") {
1768
- const { plan_id, node_id } = args;
1769
- await apiClient.nodes.deleteNode(plan_id, node_id);
1770
- return formatResponse({
1771
- success: true,
1772
- message: `Node ${node_id} and its children deleted successfully`
1773
- });
1774
- }
1775
-
1776
- if (name === "move_node") {
1777
- const { plan_id, node_id, parent_id, order_index } = args;
1778
-
1779
- try {
1780
- // Build request body with only provided fields - don't send nulls
1781
- const body = {};
1782
- if (parent_id) body.parent_id = parent_id;
1783
- if (order_index !== undefined) body.order_index = order_index;
1784
-
1785
- // Call the move endpoint - using POST as per API definition
1786
- const response = await apiClient.axiosInstance.post(
1787
- `/plans/${plan_id}/nodes/${node_id}/move`,
1788
- body
1789
- );
1790
-
1791
- return formatResponse(response.data);
1792
- } catch (error) {
1793
- // If endpoint still doesn't work, try updating the node directly
1794
- if (error.response && error.response.status === 404) {
1795
- console.error('Move endpoint not found, trying direct update');
1796
- // Fallback to updating the node's parent_id via regular update
1797
- const updateData = {};
1798
- if (parent_id) updateData.parent_id = parent_id;
1799
- if (order_index !== undefined) updateData.order_index = order_index;
1800
- const updateResponse = await apiClient.nodes.updateNode(plan_id, node_id, updateData);
1801
- return formatResponse(updateResponse);
1802
- }
1803
- throw error;
1804
- }
1805
- }
1806
-
1807
- if (name === "get_node_ancestry") {
1808
- const { plan_id, node_id } = args;
1809
-
1810
- // Get node ancestry
1811
- const response = await apiClient.axiosInstance.get(
1812
- `/plans/${plan_id}/nodes/${node_id}/ancestry`
1813
- );
1814
-
1815
- return formatResponse(response.data);
1816
- }
1817
-
1818
- // ===== DEPENDENCIES =====
1819
- if (name === "create_dependency") {
1820
- const { plan_id, source_node_id, target_node_id, dependency_type, weight, metadata } = args;
1821
- const response = await apiClient.axiosInstance.post(
1822
- `/plans/${plan_id}/dependencies`,
1823
- { source_node_id, target_node_id, dependency_type, weight, metadata }
1824
- );
1825
- return formatResponse(response.data);
1826
- }
1827
-
1828
- if (name === "delete_dependency") {
1829
- const { plan_id, dependency_id } = args;
1830
- const response = await apiClient.axiosInstance.delete(
1831
- `/plans/${plan_id}/dependencies/${dependency_id}`
1832
- );
1833
- return formatResponse(response.data);
1834
- }
1835
-
1836
- if (name === "list_dependencies") {
1837
- const { plan_id } = args;
1838
- const response = await apiClient.axiosInstance.get(
1839
- `/plans/${plan_id}/dependencies`
1840
- );
1841
- return formatResponse(response.data);
1842
- }
1843
-
1844
- if (name === "get_node_dependencies") {
1845
- const { plan_id, node_id, direction = 'both' } = args;
1846
- const response = await apiClient.axiosInstance.get(
1847
- `/plans/${plan_id}/nodes/${node_id}/dependencies`,
1848
- { params: { direction } }
1849
- );
1850
- return formatResponse(response.data);
1851
- }
1852
-
1853
- // ===== RPI WORKFLOW =====
1854
- if (name === "create_rpi_chain") {
1855
- const { plan_id, title, description, parent_id } = args;
1856
- const response = await apiClient.axiosInstance.post(
1857
- `/plans/${plan_id}/nodes/rpi-chain`,
1858
- { title, description, parent_id }
1859
- );
1860
- return formatResponse(response.data);
1861
- }
1862
-
1863
- // ===== ANALYSIS =====
1864
- if (name === "analyze_impact") {
1865
- const { plan_id, node_id, scenario = 'block' } = args;
1866
- const response = await apiClient.axiosInstance.get(
1867
- `/plans/${plan_id}/nodes/${node_id}/impact`,
1868
- { params: { scenario } }
1869
- );
1870
- return formatResponse(response.data);
1871
- }
1872
-
1873
- if (name === "get_critical_path") {
1874
- const { plan_id } = args;
1875
- const response = await apiClient.axiosInstance.get(
1876
- `/plans/${plan_id}/critical-path`
1877
- );
1878
- return formatResponse(response.data);
1879
- }
1880
-
1881
- // ===== PROGRESSIVE CONTEXT =====
1882
- if (name === "get_task_context") {
1883
- const { node_id, depth = 2, token_budget = 0, log_limit = 10, include_research = true } = args;
1884
- const params = new URLSearchParams({
1885
- node_id,
1886
- depth: String(depth),
1887
- token_budget: String(token_budget),
1888
- log_limit: String(log_limit),
1889
- include_research: String(include_research),
1890
- });
1891
- const response = await apiClient.axiosInstance.get(`/context/progressive?${params.toString()}`);
1892
- return formatResponse(response.data);
1893
- }
1894
-
1895
- if (name === "suggest_next_tasks") {
1896
- const { plan_id, limit = 5 } = args;
1897
- const params = new URLSearchParams({ plan_id, limit: String(limit) });
1898
- const response = await apiClient.axiosInstance.get(`/context/suggest?${params.toString()}`);
1899
- return formatResponse(response.data);
1900
- }
1901
-
1902
- // ===== LOGGING =====
1903
- if (name === "add_log") {
1904
- const { plan_id, node_id, content, log_type = "comment", tags } = args;
1905
-
1906
- const logData = {
1907
- content,
1908
- log_type,
1909
- tags
1910
- };
1911
-
1912
- const result = await apiClient.logs.addLogEntry(plan_id, node_id, logData);
1913
- return formatResponse(result);
1914
- }
1915
-
1916
- if (name === "get_logs") {
1917
- const { plan_id, node_id, log_type, limit = 50 } = args;
1918
-
1919
- let logs = await apiClient.logs.getLogs(plan_id, node_id);
1920
-
1921
- // Apply filters
1922
- if (log_type) {
1923
- logs = logs.filter(log => log.log_type === log_type);
1924
- }
1925
-
1926
- // Apply limit
1927
- logs = logs.slice(0, limit);
1928
-
1929
- return formatResponse(logs);
1930
- }
1931
-
1932
- // ===== BATCH OPERATIONS =====
1933
- if (name === "batch_update_nodes") {
1934
- const { plan_id, updates } = args;
1935
-
1936
- const results = [];
1937
- const errors = [];
1938
-
1939
- for (const update of updates) {
1940
- const { node_id, ...updateData } = update;
1941
- try {
1942
- const result = await apiClient.nodes.updateNode(plan_id, node_id, updateData);
1943
- results.push({ node_id, success: true, data: result });
1944
- } catch (error) {
1945
- errors.push({ node_id, success: false, error: error.message });
1946
- }
1947
- }
1948
-
1949
- return formatResponse({
1950
- total: updates.length,
1951
- successful: results.length,
1952
- failed: errors.length,
1953
- results,
1954
- errors
1955
- });
1956
- }
1957
-
1958
- // ===== PLAN STRUCTURE & SUMMARY =====
1959
- if (name === "get_plan_structure") {
1960
- const { plan_id, include_details = false } = args;
1961
-
1962
- const plan = await apiClient.plans.getPlan(plan_id);
1963
- // Pass include_details to the API - defaults to minimal fields
1964
- const nodes = await apiClient.nodes.getNodes(plan_id, {
1965
- include_details: include_details
1966
- });
1967
-
1968
- // The API already returns a tree structure, not a flat list
1969
- // If it's already hierarchical, use it directly
1970
- let structure;
1971
- if (Array.isArray(nodes) && nodes.length > 0 && nodes[0].children !== undefined) {
1972
- // Already hierarchical - use directly
1973
- structure = nodes;
1974
- } else {
1975
- // Flat list - build hierarchy
1976
- structure = buildNodeHierarchy(nodes, include_details);
1977
- }
1978
-
1979
- return formatResponse({
1980
- plan: {
1981
- id: plan.id,
1982
- title: plan.title,
1983
- status: plan.status,
1984
- description: plan.description
1985
- },
1986
- structure
1987
- });
1988
- }
1989
-
1990
- if (name === "get_plan_summary") {
1991
- const { plan_id } = args;
1992
-
1993
- const plan = await apiClient.plans.getPlan(plan_id);
1994
- const nodes = await apiClient.nodes.getNodes(plan_id);
1995
-
1996
- // Calculate statistics
1997
- const stats = calculatePlanStatistics(nodes);
1998
-
1999
- return formatResponse({
2000
- plan: {
2001
- id: plan.id,
2002
- title: plan.title,
2003
- status: plan.status,
2004
- description: plan.description,
2005
- created_at: plan.created_at,
2006
- updated_at: plan.updated_at
2007
- },
2008
- statistics: stats,
2009
- progress_percentage: stats.total > 0
2010
- ? ((stats.status_counts.completed / stats.total) * 100).toFixed(1)
2011
- : 0
2012
- });
2013
- }
2014
-
2015
- // ===== AGENT CONTEXT TOOLS =====
2016
- if (name === "get_plan_context") {
2017
- const { plan_id, include_knowledge = true } = args;
2018
-
2019
- const result = await apiClient.context.getPlanContext(plan_id, {
2020
- include_knowledge
2021
- });
2022
-
2023
- return formatResponse(result);
2024
- }
2025
-
2026
- // ===== ORGANIZATION TOOLS =====
2027
- if (name === "list_organizations") {
2028
- const result = await apiClient.organizations.list();
2029
- return formatResponse(result);
2030
- }
2031
-
2032
- if (name === "get_organization") {
2033
- const { organization_id } = args;
2034
- const result = await apiClient.organizations.get(organization_id);
2035
- return formatResponse(result);
2036
- }
2037
-
2038
- if (name === "create_organization") {
2039
- const { name, description, slug } = args;
2040
- const result = await apiClient.organizations.create({ name, description, slug });
2041
- return formatResponse(result);
2042
- }
2043
-
2044
- if (name === "update_organization") {
2045
- const { organization_id, ...updateData } = args;
2046
- const result = await apiClient.organizations.update(organization_id, updateData);
2047
- return formatResponse(result);
2048
- }
2049
-
2050
- // ===== GOAL TOOLS =====
2051
- if (name === "list_goals") {
2052
- const { organization_id, status, include_inactive } = args;
2053
- const effectiveStatus = status || (!include_inactive ? 'active' : undefined);
2054
- const result = await apiClient.goals.list({ organization_id, status: effectiveStatus });
2055
- return formatResponse(result);
2056
- }
2057
-
2058
- if (name === "get_goal") {
2059
- const { goal_id } = args;
2060
- const result = await apiClient.goals.get(goal_id);
2061
- return formatResponse(result);
2062
- }
2063
-
2064
- if (name === "create_goal") {
2065
- const { organization_id, title, description, type, success_criteria, priority, parent_goal_id } = args;
2066
- const result = await apiClient.goals.create({
2067
- organization_id,
2068
- title,
2069
- description,
2070
- type: type || 'outcome',
2071
- successCriteria: success_criteria || null,
2072
- priority: priority || 0,
2073
- parentGoalId: parent_goal_id || null,
2074
- });
2075
- return formatResponse(result);
2076
- }
2077
-
2078
- if (name === "update_goal") {
2079
- const { goal_id, parent_goal_id, success_criteria, ...rest } = args;
2080
- const updateData = { ...rest };
2081
- if (parent_goal_id !== undefined) updateData.parentGoalId = parent_goal_id;
2082
- if (success_criteria !== undefined) updateData.successCriteria = success_criteria;
2083
- const result = await apiClient.goals.update(goal_id, updateData);
2084
- return formatResponse(result);
2085
- }
2086
-
2087
- if (name === "link_plan_to_goal") {
2088
- const { goal_id, plan_id } = args;
2089
- const result = await apiClient.goals.linkPlan(goal_id, plan_id);
2090
- return formatResponse({
2091
- success: true,
2092
- message: `Plan ${plan_id} linked to goal ${goal_id}`,
2093
- ...result
2094
- });
2095
- }
2096
-
2097
- if (name === "unlink_plan_from_goal") {
2098
- const { goal_id, plan_id } = args;
2099
- const result = await apiClient.goals.unlinkPlan(goal_id, plan_id);
2100
- return formatResponse({
2101
- success: true,
2102
- message: `Plan ${plan_id} unlinked from goal ${goal_id}`,
2103
- ...result
2104
- });
2105
- }
2106
-
2107
- // ===== CROSS-PLAN & EXTERNAL DEPENDENCY HANDLERS =====
2108
- if (name === "create_cross_plan_dependency") {
2109
- const { source_node_id, target_node_id, dependency_type, weight } = args;
2110
- const result = await apiClient.dependencies.createCrossPlan({
2111
- source_node_id, target_node_id, dependency_type, weight
2112
- });
2113
- return formatResponse(result);
2114
- }
2115
-
2116
- if (name === "list_cross_plan_dependencies") {
2117
- const { plan_ids } = args;
2118
- const result = await apiClient.dependencies.listCrossPlan(plan_ids);
2119
- return formatResponse(result);
2120
- }
2121
-
2122
- if (name === "create_external_dependency") {
2123
- const { plan_id, title, description, url, blocks_node_id } = args;
2124
- const result = await apiClient.dependencies.createExternal({
2125
- plan_id, title, description, url, blocks_node_id
2126
- });
2127
- return formatResponse(result);
2128
- }
2129
-
2130
- // ===== GOAL-DEPENDENCY HANDLERS =====
2131
- if (name === "goal_path") {
2132
- const { goal_id, max_depth } = args;
2133
- const result = await apiClient.goals.getPath(goal_id, max_depth);
2134
- return formatResponse(result);
2135
- }
2136
-
2137
- if (name === "goal_progress") {
2138
- const { goal_id } = args;
2139
- const result = await apiClient.goals.getProgress(goal_id);
2140
- return formatResponse(result);
2141
- }
2142
-
2143
- if (name === "add_achiever") {
2144
- const { goal_id, node_id, weight } = args;
2145
- const result = await apiClient.goals.addAchiever(goal_id, node_id, weight);
2146
- return formatResponse({
2147
- ...result,
2148
- message: `Task ${node_id} now achieves goal ${goal_id}`,
2149
- });
2150
- }
2151
-
2152
- if (name === "remove_achiever") {
2153
- const { goal_id, dependency_id } = args;
2154
- const result = await apiClient.goals.removeAchiever(goal_id, dependency_id);
2155
- return formatResponse(result);
2156
- }
2157
-
2158
- if (name === "goal_knowledge_gaps") {
2159
- const { goal_id } = args;
2160
- const result = await apiClient.goals.getKnowledgeGaps(goal_id);
2161
- return formatResponse(result);
2162
- }
2163
-
2164
- // ===== GRAPHITI KNOWLEDGE GRAPH HANDLERS =====
2165
- if (name === "add_learning") {
2166
- const { content, title, entry_type, plan_id, node_id } = args;
2167
-
2168
- // Add to Graphiti temporal knowledge graph
2169
- const result = await apiClient.graphiti.addEpisode({
2170
- content,
2171
- name: title,
2172
- plan_id,
2173
- node_id,
2174
- metadata: { entry_type: entry_type || 'learning' },
2175
- });
2176
- return formatResponse({
2177
- ...result,
2178
- message: 'Knowledge recorded in temporal graph',
2179
- tip: 'This is now searchable via recall_knowledge across all plans'
2180
- });
2181
- }
2182
-
2183
- if (name === "recall_knowledge") {
2184
- const { query, max_results = 10 } = args;
2185
-
2186
- // Try Graphiti first (temporal, cross-plan)
2187
- try {
2188
- const graphResult = await apiClient.graphiti.graphSearch({ query, max_results });
2189
- if (graphResult?.results) {
2190
- return formatResponse({
2191
- ...graphResult,
2192
- source: 'graphiti_temporal_graph'
2193
- });
2194
- }
2195
- } catch (err) {
2196
- return formatResponse({
2197
- results: [],
2198
- source: 'graphiti_temporal_graph',
2199
- error: 'Knowledge graph not available: ' + err.message,
2200
- });
2201
- }
2202
- }
2203
-
2204
- if (name === "find_entities") {
2205
- const { query, max_results = 10 } = args;
2206
-
2207
- try {
2208
- const result = await apiClient.graphiti.searchEntities({ query, max_results });
2209
- return formatResponse(result);
2210
- } catch (err) {
2211
- return formatResponse({
2212
- error: 'Entity search requires the temporal knowledge graph (Graphiti)',
2213
- detail: err.message
2214
- });
2215
- }
2216
- }
2217
-
2218
- if (name === "check_contradictions") {
2219
- const { query, max_results = 10 } = args;
2220
-
2221
- try {
2222
- const result = await apiClient.graphiti.detectContradictions({ query, max_results });
2223
- if (result.contradictions_found) {
2224
- return formatResponse({
2225
- ...result,
2226
- warning: 'Some knowledge has been superseded. Review the "superseded" facts before proceeding.',
2227
- });
2228
- }
2229
- return formatResponse({
2230
- ...result,
2231
- message: 'No contradictions found — all facts are current.',
2232
- });
2233
- } catch (err) {
2234
- return formatResponse({
2235
- error: 'Contradiction detection requires the temporal knowledge graph (Graphiti)',
2236
- detail: err.message,
2237
- });
2238
- }
2239
- }
2240
-
2241
- if (name === "get_recent_episodes") {
2242
- const { max_episodes = 20 } = args || {};
2243
-
2244
- try {
2245
- const result = await apiClient.graphiti.getEpisodes({ max_episodes });
2246
- return formatResponse(result);
2247
- } catch (err) {
2248
- return formatResponse({
2249
- error: 'Episodic memory requires the temporal knowledge graph (Graphiti)',
2250
- detail: err.message
2251
- });
2252
- }
2253
- }
2254
-
2255
- // ===== HELPER TOOLS =====
2256
- if (name === "get_started") {
2257
- const { topic = "overview" } = args || {};
2258
-
2259
- const guides = {
2260
- overview: {
2261
- title: "AgentPlanner Overview",
2262
- description: "AgentPlanner is a collaborative planning system for AI agents and humans to work together on structured plans.",
2263
- key_concepts: [
2264
- "Organizations - Groups of users, goals, and resources",
2265
- "Goals - High-level objectives with success metrics that plans work toward",
2266
- "Plans - Hierarchical structures with phases, tasks, and milestones",
2267
- "Nodes - Individual items in a plan (phases contain tasks and milestones)",
2268
- "Knowledge - Persistent storage for decisions, context, constraints, and learnings"
2269
- ],
2270
- recommended_workflow: [
2271
- "1. PREFLIGHT: check_coherence_pending to see if any plans/goals need alignment review",
2272
- " → If stale items found, run_coherence_check on each before starting task work",
2273
- "2. Check list_goals to understand current objectives",
2274
- "3. Use list_plans to see existing plans",
2275
- "4. Before working on a plan, use get_plan_context to get the full picture",
2276
- "5. Update task statuses as you work (update_node with status)",
2277
- "6. Store important decisions and learnings using add_learning",
2278
- "7. Check recall_knowledge before making decisions to see past context"
2279
- ],
2280
- quick_tips: [
2281
- "Always capture WHY decisions were made, not just WHAT",
2282
- "Mark tasks 'blocked' with notes when stuck - this helps humans help you",
2283
- "Use logs to document progress so others can follow your work"
2284
- ]
2285
- },
2286
- planning: {
2287
- title: "Planning Best Practices",
2288
- description: "How to create and structure effective plans.",
2289
- structure: [
2290
- "Plans have a hierarchical structure: Plan → Phases → Tasks/Milestones",
2291
- "Phases are major stages or milestones of work",
2292
- "Tasks are actionable work items within phases",
2293
- "Milestones mark significant checkpoints"
2294
- ],
2295
- tips: [
2296
- "Break work into phases (major stages)",
2297
- "Each phase should contain 3-7 tasks (not too granular, not too big)",
2298
- "Add clear acceptance_criteria to tasks so completion is unambiguous",
2299
- "Use agent_instructions to guide how AI agents should approach tasks",
2300
- "Link plans to goals to track how work contributes to objectives"
2301
- ],
2302
- tools_to_use: ["create_plan", "create_node", "get_plan_structure", "link_plan_to_goal"]
2303
- },
2304
- execution: {
2305
- title: "Executing Plans",
2306
- description: "How to work through plans effectively.",
2307
- workflow: [
2308
- "1. Use get_plan_structure to see the full plan",
2309
- "2. Find tasks with status 'not_started' or 'in_progress'",
2310
- "3. Before starting a task, check recall_knowledge for relevant context",
2311
- "4. Update task status to 'in_progress' when you begin",
2312
- "5. Add logs to document what you're doing",
2313
- "6. Mark 'completed' when done, or 'blocked' if stuck"
2314
- ],
2315
- status_values: {
2316
- not_started: "Work hasn't begun",
2317
- in_progress: "Currently being worked on",
2318
- completed: "Finished and verified",
2319
- blocked: "Cannot proceed - add notes explaining why",
2320
- cancelled: "No longer needed"
2321
- },
2322
- tips: [
2323
- "Check get_plan_summary for current progress and blockers",
2324
- "When blocked, clearly document what's blocking you",
2325
- "Store learnings as you go - don't wait until the end"
2326
- ],
2327
- tools_to_use: ["get_plan_structure", "update_node", "add_log", "recall_knowledge"]
2328
- },
2329
- knowledge: {
2330
- title: "Knowledge Management",
2331
- description: "How to capture and use organizational knowledge effectively.",
2332
- entry_types: {
2333
- decision: "Choices made and their rationale - ALWAYS capture WHY",
2334
- context: "Background information needed to understand something",
2335
- constraint: "Rules, limitations, or requirements that must be respected",
2336
- learning: "Insights gained from experience - what worked, what didn't",
2337
- reference: "Links to external resources or documentation",
2338
- note: "General notes that don't fit other categories"
2339
- },
2340
- best_practices: [
2341
- "ALWAYS capture significant decisions with reasoning",
2342
- "Search knowledge BEFORE making decisions (check for constraints)",
2343
- "Add learnings when you discover something useful",
2344
- "Tag entries well for easier retrieval later",
2345
- "Include enough context that future-you can understand"
2346
- ],
2347
- when_to_create_entries: [
2348
- "When a decision is made (especially if non-obvious)",
2349
- "When you learn something that might be useful later",
2350
- "When you discover a constraint or rule",
2351
- "When you find a useful resource or reference"
2352
- ],
2353
- tools_to_use: ["add_learning", "recall_knowledge", "find_entities", "check_contradictions"]
2354
- },
2355
- collaboration: {
2356
- title: "Collaboration",
2357
- description: "Working with humans and other agents.",
2358
- tips: [
2359
- "Plans can be shared with collaborators (viewer, editor, admin roles)",
2360
- "Use logs to document progress so others can follow your work",
2361
- "Knowledge stores are shared within their scope (org/goal/plan)",
2362
- "When stuck, mark tasks as 'blocked' with clear notes - humans will see this"
2363
- ],
2364
- communication: [
2365
- "Logs are visible to all plan collaborators",
2366
- "Knowledge entries persist and are searchable by others",
2367
- "Clear status updates help humans understand where things stand"
2368
- ],
2369
- tools_to_use: ["list_organizations", "list_goals", "add_log"]
2370
- }
2371
- };
2372
-
2373
- return formatResponse(guides[topic] || guides.overview);
2374
- }
2375
-
2376
- // ===== GOALS HEALTH DASHBOARD =====
2377
- if (name === "check_goals_health") {
2378
- const { status_filter } = args || {};
2379
- const result = await apiClient.goals.getDashboard();
2380
-
2381
- let goals = result.goals || result;
2382
- if (status_filter && Array.isArray(goals)) {
2383
- goals = goals.filter(g => g.health_status === status_filter || g.status === status_filter);
2384
- }
2385
-
2386
- return formatResponse({
2387
- ...result,
2388
- goals,
2389
- tip: "Prioritize: stale goals first, then at_risk, then on_track."
2390
- });
2391
- }
2392
-
2393
- // ===== TASK CLAIMING =====
2394
- if (name === "claim_task") {
2395
- const { task_id, plan_id, ttl_minutes = 30 } = args;
2396
- const result = await apiClient.nodes.claimTask(plan_id, task_id, 'mcp-agent', ttl_minutes);
2397
- return formatResponse({
2398
- success: true,
2399
- message: `Task ${task_id} claimed for ${ttl_minutes} minutes`,
2400
- ...result,
2401
- tip: "Remember to release the task when done, or it will auto-expire."
2402
- });
2403
- }
2404
36
 
2405
- if (name === "release_task") {
2406
- const { task_id, plan_id } = args;
2407
- const result = await apiClient.nodes.releaseTask(plan_id, task_id, 'mcp-agent');
2408
- return formatResponse({
2409
- success: true,
2410
- message: `Task ${task_id} released`,
2411
- ...result
2412
- });
2413
- }
2414
-
2415
- // ===== COHERENCE =====
2416
- if (name === "check_coherence_pending") {
2417
- const result = await apiClient.coherence.getPending();
2418
- const totalStale = (result.stale_plans?.length || 0) + (result.stale_goals?.length || 0);
2419
- return formatResponse({
2420
- ...result,
2421
- tip: totalStale > 0
2422
- ? "Review stale items. For each stale plan, call run_coherence_check to evaluate quality and stamp as checked."
2423
- : "Everything is up to date. No coherence review needed."
2424
- });
2425
- }
2426
-
2427
- if (name === "assess_goal_quality") {
2428
- const { goal_id } = args;
2429
- const result = await apiClient.goals.getQuality(goal_id);
2430
- const lowDims = Object.entries(result.dimensions || {})
2431
- .filter(([, v]) => v.score < 0.5)
2432
- .map(([k]) => k);
2433
- return formatResponse({
2434
- ...result,
2435
- tip: result.suggestions?.length > 0
2436
- ? `Goal needs improvement in: ${lowDims.join(', ') || 'minor areas'}. Follow the suggestions to strengthen it.`
2437
- : 'Goal is well-defined. Ready for agent execution.'
2438
- });
2439
- }
2440
-
2441
- if (name === "run_coherence_check") {
2442
- const { plan_id, goal_id } = args;
2443
- const result = await apiClient.coherence.runCheck(plan_id, goal_id);
2444
- return formatResponse({
2445
- ...result,
2446
- tip: result.coherence_issues_count > 0
2447
- ? `${result.coherence_issues_count} coherence issues found. Review tasks with stale_beliefs or contradiction_detected status.`
2448
- : "Plan is coherent. Quality score and checked_at timestamp updated."
2449
- });
2450
- }
2451
-
2452
- // Tool not found
2453
- throw new Error(`Unknown tool: ${name}`);
2454
- } catch (error) {
2455
- if (process.env.NODE_ENV === 'development') {
2456
- console.error(`Error calling tool ${name}:`, error);
2457
- }
37
+ if (!bdiToolNames.has(name)) {
2458
38
  return {
2459
39
  isError: true,
2460
- content: [
2461
- {
2462
- type: "text",
2463
- text: `Error: ${error.message}`
2464
- }
2465
- ]
40
+ content: [{
41
+ type: 'text',
42
+ text: `Unknown tool: ${name}. v0.9.0 ships 15 BDI tools. Run get_started to see them, or check ../docs/MIGRATION_v0.9.md for the legacy → BDI mapping.`,
43
+ }],
2466
44
  };
2467
45
  }
2468
- });
2469
-
2470
- if (process.env.NODE_ENV === 'development') {
2471
- console.error('Tools setup complete');
2472
- }
2473
- }
2474
46
 
2475
- /**
2476
- * Build hierarchical node structure
2477
- */
2478
- function buildNodeHierarchy(nodes, includeDetails = false) {
2479
- if (!nodes || nodes.length === 0) {
2480
- return [];
2481
- }
2482
-
2483
- // Debug logging to understand the structure
2484
- if (process.env.NODE_ENV === 'development') {
2485
- console.error('Building hierarchy for nodes:', nodes.length);
2486
- if (nodes[0]) {
2487
- console.error('Sample node:', {
2488
- id: nodes[0].id,
2489
- parent_id: nodes[0].parent_id,
2490
- node_type: nodes[0].node_type
2491
- });
2492
- }
2493
- }
2494
-
2495
- const nodeMap = new Map();
2496
- const rootNodes = [];
2497
-
2498
- // First pass: create all nodes in the map
2499
- nodes.forEach(node => {
2500
- const nodeData = includeDetails ? { ...node } : {
2501
- id: node.id,
2502
- title: node.title,
2503
- node_type: node.node_type,
2504
- status: node.status,
2505
- parent_id: node.parent_id,
2506
- order_index: node.order_index
2507
- };
2508
-
2509
- // Initialize with empty children array
2510
- nodeMap.set(node.id, {
2511
- ...nodeData,
2512
- children: []
2513
- });
2514
- });
2515
-
2516
- // Second pass: build parent-child relationships
2517
- nodes.forEach(node => {
2518
- const currentNode = nodeMap.get(node.id);
2519
-
2520
- if (node.parent_id) {
2521
- const parent = nodeMap.get(node.parent_id);
2522
- if (parent) {
2523
- // Add as child to parent
2524
- parent.children.push(currentNode);
2525
- } else {
2526
- // Parent not found, treat as root
2527
- if (process.env.NODE_ENV === 'development') {
2528
- console.error(`Parent ${node.parent_id} not found for node ${node.id}`);
2529
- }
2530
- rootNodes.push(currentNode);
47
+ try {
48
+ return await bdiToolHandler(name, args, apiClient);
49
+ } catch (err) {
50
+ if (process.env.NODE_ENV === 'development') {
51
+ console.error(`Tool ${name} threw:`, err);
2531
52
  }
2532
- } else {
2533
- // No parent_id means it's a root node
2534
- rootNodes.push(currentNode);
53
+ return {
54
+ isError: true,
55
+ content: [{
56
+ type: 'text',
57
+ text: `Tool ${name} failed: ${err.message || String(err)}`,
58
+ }],
59
+ };
2535
60
  }
2536
61
  });
2537
-
2538
- // Special case: if we have a single root node of type 'root', return its children
2539
- if (rootNodes.length === 1 && rootNodes[0].node_type === 'root') {
2540
- // Return the root node itself with its children
2541
- const rootNode = rootNodes[0];
2542
-
2543
- // Sort children by order_index
2544
- const sortNodes = (nodeArray) => {
2545
- nodeArray.sort((a, b) => {
2546
- const orderA = a.order_index ?? 999;
2547
- const orderB = b.order_index ?? 999;
2548
- return orderA - orderB;
2549
- });
2550
-
2551
- nodeArray.forEach(node => {
2552
- if (node.children && node.children.length > 0) {
2553
- sortNodes(node.children);
2554
- }
2555
- });
2556
- };
2557
-
2558
- sortNodes(rootNode.children);
2559
- return [rootNode]; // Return root with its properly sorted children
2560
- }
2561
-
2562
- // Sort all root nodes and their children
2563
- const sortNodes = (nodeArray) => {
2564
- nodeArray.sort((a, b) => {
2565
- const orderA = a.order_index ?? 999;
2566
- const orderB = b.order_index ?? 999;
2567
- return orderA - orderB;
2568
- });
2569
-
2570
- nodeArray.forEach(node => {
2571
- if (node.children && node.children.length > 0) {
2572
- sortNodes(node.children);
2573
- }
2574
- });
2575
- };
2576
-
2577
- sortNodes(rootNodes);
2578
-
2579
- return rootNodes;
2580
- }
2581
-
2582
- /**
2583
- * Flatten a hierarchical node structure into a flat array
2584
- */
2585
- function flattenNodes(nodes) {
2586
- const flat = [];
2587
-
2588
- const processNode = (node) => {
2589
- flat.push(node);
2590
- if (node.children && node.children.length > 0) {
2591
- node.children.forEach(processNode);
2592
- }
2593
- };
2594
-
2595
- if (Array.isArray(nodes)) {
2596
- nodes.forEach(processNode);
2597
- }
2598
-
2599
- return flat;
2600
- }
2601
-
2602
- /**
2603
- * Calculate plan statistics
2604
- */
2605
- function calculatePlanStatistics(nodes) {
2606
- const stats = {
2607
- total: 0,
2608
- type_counts: {
2609
- root: 0,
2610
- phase: 0,
2611
- task: 0,
2612
- milestone: 0
2613
- },
2614
- status_counts: {
2615
- not_started: 0,
2616
- in_progress: 0,
2617
- completed: 0,
2618
- blocked: 0
2619
- },
2620
- in_progress_nodes: [],
2621
- blocked_nodes: []
2622
- };
2623
-
2624
- const processNode = (node) => {
2625
- stats.total++;
2626
-
2627
- if (node.node_type && stats.type_counts[node.node_type] !== undefined) {
2628
- stats.type_counts[node.node_type]++;
2629
- }
2630
-
2631
- if (node.status && stats.status_counts[node.status] !== undefined) {
2632
- stats.status_counts[node.status]++;
2633
-
2634
- if (node.status === 'in_progress') {
2635
- stats.in_progress_nodes.push({
2636
- id: node.id,
2637
- title: node.title,
2638
- type: node.node_type
2639
- });
2640
- } else if (node.status === 'blocked') {
2641
- stats.blocked_nodes.push({
2642
- id: node.id,
2643
- title: node.title,
2644
- type: node.node_type
2645
- });
2646
- }
2647
- }
2648
-
2649
- if (node.children && node.children.length > 0) {
2650
- node.children.forEach(processNode);
2651
- }
2652
- };
2653
-
2654
- nodes.forEach(processNode);
2655
-
2656
- return stats;
2657
62
  }
2658
63
 
2659
64
  module.exports = { setupTools };