agent-planner-mcp 0.3.1 → 0.5.1

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
@@ -10,7 +10,11 @@
10
10
  */
11
11
 
12
12
  const { ListToolsRequestSchema, CallToolRequestSchema } = require('@modelcontextprotocol/sdk/types.js');
13
- const apiClient = require('./api-client');
13
+ 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}`; }
14
18
 
15
19
  /**
16
20
  * Format JSON data as text for Claude Desktop
@@ -43,8 +47,10 @@ function formatResponse(data) {
43
47
  /**
44
48
  * Setup tools for the MCP server
45
49
  * @param {Server} server - MCP server instance
50
+ * @param {Object} [apiClientOverride] - Per-session API client (HTTP mode). Falls back to default (stdio mode).
46
51
  */
47
- function setupTools(server) {
52
+ function setupTools(server, apiClientOverride) {
53
+ const apiClient = apiClientOverride || defaultApiClient;
48
54
  // Suppress console logs when not in debug mode
49
55
  if (process.env.NODE_ENV !== 'development') {
50
56
  // Silent mode for production
@@ -56,6 +62,191 @@ function setupTools(server) {
56
62
  server.setRequestHandler(ListToolsRequestSchema, async () => {
57
63
  return {
58
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
+ // CONTEXT LOADING - Get everything you need
183
+ // Use before starting work on a plan/goal
184
+ // ========================================
185
+ {
186
+ name: "get_context",
187
+ description: "Load EVERYTHING you need to work on a plan or goal: structure, status, progress, blocked tasks, recent activity, and relevant knowledge. Call this FIRST before starting work.",
188
+ inputSchema: {
189
+ type: "object",
190
+ properties: {
191
+ plan_id: { type: "string", description: "Plan to get context for" },
192
+ goal_id: { type: "string", description: "Goal to get context for" },
193
+ include_knowledge: { type: "boolean", default: true, description: "Include relevant knowledge entries" }
194
+ }
195
+ }
196
+ },
197
+ {
198
+ name: "get_my_tasks",
199
+ description: "Get tasks that need attention - blocked tasks, in-progress tasks, and next tasks to start. Perfect for status check-ins.",
200
+ inputSchema: {
201
+ type: "object",
202
+ properties: {
203
+ plan_id: { type: "string", description: "Specific plan to check (optional - checks all if not provided)" },
204
+ status: {
205
+ type: "array",
206
+ items: { type: "string" },
207
+ default: ["blocked", "in_progress"],
208
+ description: "Task statuses to include"
209
+ }
210
+ }
211
+ }
212
+ },
213
+
214
+ // ========================================
215
+ // MARKDOWN EXPORT/IMPORT - Filesystem pattern
216
+ // ========================================
217
+ {
218
+ name: "export_plan_markdown",
219
+ description: "Export a plan as markdown text. Useful for reviewing plans in text format or saving to files.",
220
+ inputSchema: {
221
+ type: "object",
222
+ properties: {
223
+ plan_id: { type: "string", description: "Plan to export" },
224
+ include_descriptions: { type: "boolean", default: true, description: "Include task descriptions" },
225
+ include_status: { type: "boolean", default: true, description: "Include status indicators" }
226
+ },
227
+ required: ["plan_id"]
228
+ }
229
+ },
230
+ {
231
+ name: "import_plan_markdown",
232
+ 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.",
233
+ inputSchema: {
234
+ type: "object",
235
+ properties: {
236
+ markdown: {
237
+ type: "string",
238
+ description: "Markdown text to parse. Use # for title, ## for phases, - for tasks"
239
+ },
240
+ title: {
241
+ type: "string",
242
+ description: "Plan title (optional - extracted from first # heading if not provided)"
243
+ },
244
+ goal_id: { type: "string", description: "Optionally link to a goal" }
245
+ },
246
+ required: ["markdown"]
247
+ }
248
+ },
249
+
59
250
  // ===== UNIFIED SEARCH TOOL =====
60
251
  {
61
252
  name: "search",
@@ -89,7 +280,7 @@ function setupTools(server) {
89
280
  type: {
90
281
  type: "string",
91
282
  description: "Filter by type",
92
- enum: ["plan", "node", "phase", "task", "milestone", "artifact", "log"]
283
+ enum: ["plan", "node", "phase", "task", "milestone", "log"]
93
284
  },
94
285
  limit: {
95
286
  type: "integer",
@@ -165,6 +356,31 @@ function setupTools(server) {
165
356
  required: ["plan_id"]
166
357
  }
167
358
  },
359
+ {
360
+ name: "share_plan",
361
+ description: "Share a plan by making it public or private. Public plans can be viewed by anyone with the link.",
362
+ inputSchema: {
363
+ type: "object",
364
+ properties: {
365
+ plan_id: { type: "string", description: "Plan ID to share" },
366
+ visibility: {
367
+ type: "string",
368
+ description: "Plan visibility setting",
369
+ enum: ["public", "private"],
370
+ default: "public"
371
+ },
372
+ github_repo_owner: {
373
+ type: "string",
374
+ description: "GitHub repository owner (optional, for linking public plans to a repo)"
375
+ },
376
+ github_repo_name: {
377
+ type: "string",
378
+ description: "GitHub repository name (optional, for linking public plans to a repo)"
379
+ }
380
+ },
381
+ required: ["plan_id"]
382
+ }
383
+ },
168
384
 
169
385
  // ===== NODE MANAGEMENT TOOLS =====
170
386
  {
@@ -185,14 +401,20 @@ function setupTools(server) {
185
401
  status: {
186
402
  type: "string",
187
403
  description: "Node status",
188
- enum: ["not_started", "in_progress", "completed", "blocked"],
404
+ enum: ["not_started", "in_progress", "completed", "blocked", "plan_ready"],
189
405
  default: "not_started"
190
406
  },
191
407
  context: { type: "string", description: "Additional context for the node" },
192
408
  agent_instructions: { type: "string", description: "Instructions for AI agents working on this node" },
193
409
  acceptance_criteria: { type: "string", description: "Criteria for node completion" },
194
410
  due_date: { type: "string", description: "Due date (ISO format)" },
195
- metadata: { type: "object", description: "Additional metadata" }
411
+ metadata: { type: "object", description: "Additional metadata" },
412
+ task_mode: {
413
+ type: "string",
414
+ description: "RPI workflow mode for the node",
415
+ enum: ["research", "plan", "implement", "free"],
416
+ default: "free"
417
+ }
196
418
  },
197
419
  required: ["plan_id", "node_type", "title"]
198
420
  }
@@ -210,13 +432,18 @@ function setupTools(server) {
210
432
  status: {
211
433
  type: "string",
212
434
  description: "New node status",
213
- enum: ["not_started", "in_progress", "completed", "blocked"]
435
+ enum: ["not_started", "in_progress", "completed", "blocked", "plan_ready"]
214
436
  },
215
437
  context: { type: "string", description: "New context" },
216
438
  agent_instructions: { type: "string", description: "New agent instructions" },
217
439
  acceptance_criteria: { type: "string", description: "New acceptance criteria" },
218
440
  due_date: { type: "string", description: "New due date (ISO format)" },
219
- metadata: { type: "object", description: "New metadata" }
441
+ metadata: { type: "object", description: "New metadata" },
442
+ task_mode: {
443
+ type: "string",
444
+ description: "RPI workflow mode for the node",
445
+ enum: ["research", "plan", "implement", "free"]
446
+ }
220
447
  },
221
448
  required: ["plan_id", "node_id"]
222
449
  }
@@ -249,7 +476,7 @@ function setupTools(server) {
249
476
  },
250
477
  {
251
478
  name: "get_node_context",
252
- description: "Get comprehensive context for a node including children, logs, and artifacts",
479
+ description: "Get comprehensive context for a node including children and logs",
253
480
  inputSchema: {
254
481
  type: "object",
255
482
  properties: {
@@ -272,6 +499,168 @@ function setupTools(server) {
272
499
  }
273
500
  },
274
501
 
502
+ // ===== DEPENDENCY TOOLS =====
503
+ {
504
+ name: "create_dependency",
505
+ description: "Create a dependency edge between two nodes in a plan. Source 'blocks' target by default.",
506
+ inputSchema: {
507
+ type: "object",
508
+ properties: {
509
+ plan_id: { type: "string", description: "Plan ID" },
510
+ source_node_id: { type: "string", description: "Source node ID (the blocker)" },
511
+ target_node_id: { type: "string", description: "Target node ID (the blocked)" },
512
+ dependency_type: {
513
+ type: "string",
514
+ description: "Type of dependency",
515
+ enum: ["blocks", "requires", "relates_to"],
516
+ default: "blocks"
517
+ },
518
+ weight: { type: "integer", description: "Edge weight (default 1)", default: 1 },
519
+ metadata: { type: "object", description: "Additional metadata" }
520
+ },
521
+ required: ["plan_id", "source_node_id", "target_node_id"]
522
+ }
523
+ },
524
+ {
525
+ name: "delete_dependency",
526
+ description: "Delete a dependency edge",
527
+ inputSchema: {
528
+ type: "object",
529
+ properties: {
530
+ plan_id: { type: "string", description: "Plan ID" },
531
+ dependency_id: { type: "string", description: "Dependency edge ID" }
532
+ },
533
+ required: ["plan_id", "dependency_id"]
534
+ }
535
+ },
536
+ {
537
+ name: "list_dependencies",
538
+ description: "List all dependency edges in a plan",
539
+ inputSchema: {
540
+ type: "object",
541
+ properties: {
542
+ plan_id: { type: "string", description: "Plan ID" }
543
+ },
544
+ required: ["plan_id"]
545
+ }
546
+ },
547
+ {
548
+ name: "get_node_dependencies",
549
+ description: "Get upstream and downstream dependencies for a node",
550
+ inputSchema: {
551
+ type: "object",
552
+ properties: {
553
+ plan_id: { type: "string", description: "Plan ID" },
554
+ node_id: { type: "string", description: "Node ID" },
555
+ direction: {
556
+ type: "string",
557
+ description: "Direction to query",
558
+ enum: ["upstream", "downstream", "both"],
559
+ default: "both"
560
+ }
561
+ },
562
+ required: ["plan_id", "node_id"]
563
+ }
564
+ },
565
+
566
+ // ===== RPI WORKFLOW =====
567
+ {
568
+ name: "create_rpi_chain",
569
+ description: "Create a Research→Plan→Implement task chain with automatic dependency edges. The three tasks are linked: Research blocks Plan, Plan blocks Implement.",
570
+ inputSchema: {
571
+ type: "object",
572
+ properties: {
573
+ plan_id: { type: "string", description: "Plan ID" },
574
+ title: { type: "string", description: "Base title for the chain (e.g. 'Auth refactor')" },
575
+ description: { type: "string", description: "Description for the research task" },
576
+ parent_id: { type: "string", description: "Parent node ID (optional, defaults to root)" }
577
+ },
578
+ required: ["plan_id", "title"]
579
+ }
580
+ },
581
+
582
+ // ===== ANALYSIS TOOLS =====
583
+ {
584
+ name: "analyze_impact",
585
+ description: "Analyze what happens if a node is delayed, blocked, or removed. Shows directly and transitively affected nodes.",
586
+ inputSchema: {
587
+ type: "object",
588
+ properties: {
589
+ plan_id: { type: "string", description: "Plan ID" },
590
+ node_id: { type: "string", description: "Node ID to analyze" },
591
+ scenario: {
592
+ type: "string",
593
+ description: "Impact scenario",
594
+ enum: ["delay", "block", "remove"],
595
+ default: "block"
596
+ }
597
+ },
598
+ required: ["plan_id", "node_id"]
599
+ }
600
+ },
601
+ {
602
+ name: "get_critical_path",
603
+ description: "Find the critical path (longest dependency chain) through incomplete tasks in a plan",
604
+ inputSchema: {
605
+ type: "object",
606
+ properties: {
607
+ plan_id: { type: "string", description: "Plan ID" }
608
+ },
609
+ required: ["plan_id"]
610
+ }
611
+ },
612
+
613
+ // ===== PROGRESSIVE CONTEXT TOOLS =====
614
+ {
615
+ name: "get_task_context",
616
+ 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.",
617
+ inputSchema: {
618
+ type: "object",
619
+ properties: {
620
+ node_id: { type: "string", description: "Task/node ID to get context for" },
621
+ depth: {
622
+ type: "integer",
623
+ description: "Context depth 1-4 (default 2). Start with 2, go deeper if needed.",
624
+ minimum: 1,
625
+ maximum: 4,
626
+ default: 2
627
+ },
628
+ token_budget: {
629
+ type: "integer",
630
+ description: "Max estimated tokens (0 = unlimited). Use to stay within context window limits.",
631
+ default: 0
632
+ },
633
+ log_limit: {
634
+ type: "integer",
635
+ description: "Max recent logs to include per node",
636
+ default: 10
637
+ },
638
+ include_research: {
639
+ type: "boolean",
640
+ description: "Include research outputs from RPI chain siblings (for implement tasks)",
641
+ default: true
642
+ }
643
+ },
644
+ required: ["node_id"]
645
+ }
646
+ },
647
+ {
648
+ name: "suggest_next_tasks",
649
+ 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.",
650
+ inputSchema: {
651
+ type: "object",
652
+ properties: {
653
+ plan_id: { type: "string", description: "Plan ID" },
654
+ limit: {
655
+ type: "integer",
656
+ description: "Maximum suggestions to return",
657
+ default: 5
658
+ }
659
+ },
660
+ required: ["plan_id"]
661
+ }
662
+ },
663
+
275
664
  // ===== LOGGING TOOLS (Replaces Comments) =====
276
665
  {
277
666
  name: "add_log",
@@ -320,30 +709,6 @@ function setupTools(server) {
320
709
  }
321
710
  },
322
711
 
323
- // ===== ARTIFACT MANAGEMENT =====
324
- {
325
- name: "manage_artifact",
326
- description: "Add, get, or search for artifacts",
327
- inputSchema: {
328
- type: "object",
329
- properties: {
330
- action: {
331
- type: "string",
332
- description: "Action to perform",
333
- enum: ["add", "get", "search", "list"]
334
- },
335
- plan_id: { type: "string", description: "Plan ID" },
336
- node_id: { type: "string", description: "Node ID" },
337
- artifact_id: { type: "string", description: "Artifact ID (for 'get' action)" },
338
- name: { type: "string", description: "Artifact name (for 'add' or 'search')" },
339
- content_type: { type: "string", description: "Content MIME type (for 'add')" },
340
- url: { type: "string", description: "URL where artifact can be accessed (for 'add')" },
341
- metadata: { type: "object", description: "Additional metadata (for 'add')" }
342
- },
343
- required: ["action", "plan_id", "node_id"]
344
- }
345
- },
346
-
347
712
  // ===== BATCH OPERATIONS =====
348
713
  {
349
714
  name: "batch_update_nodes",
@@ -361,7 +726,7 @@ function setupTools(server) {
361
726
  node_id: { type: "string", description: "Node ID" },
362
727
  status: {
363
728
  type: "string",
364
- enum: ["not_started", "in_progress", "completed", "blocked"]
729
+ enum: ["not_started", "in_progress", "completed", "blocked", "plan_ready"]
365
730
  },
366
731
  title: { type: "string" },
367
732
  description: { type: "string" }
@@ -373,41 +738,18 @@ function setupTools(server) {
373
738
  required: ["plan_id", "updates"]
374
739
  }
375
740
  },
376
- {
377
- name: "batch_get_artifacts",
378
- description: "Get multiple artifacts at once",
379
- inputSchema: {
380
- type: "object",
381
- properties: {
382
- plan_id: { type: "string", description: "Plan ID" },
383
- artifact_requests: {
384
- type: "array",
385
- description: "List of artifact requests",
386
- items: {
387
- type: "object",
388
- properties: {
389
- node_id: { type: "string", description: "Node ID" },
390
- artifact_id: { type: "string", description: "Artifact ID" }
391
- },
392
- required: ["node_id", "artifact_id"]
393
- }
394
- }
395
- },
396
- required: ["plan_id", "artifact_requests"]
397
- }
398
- },
399
741
 
400
742
  // ===== PLAN STRUCTURE & SUMMARY =====
401
743
  {
402
744
  name: "get_plan_structure",
403
- description: "Get the complete hierarchical structure of a plan",
745
+ description: "Get the hierarchical structure of a plan with minimal fields (id, parent_id, node_type, title, status, order_index). Use get_node_context for detailed information about specific nodes.",
404
746
  inputSchema: {
405
747
  type: "object",
406
748
  properties: {
407
749
  plan_id: { type: "string", description: "Plan ID" },
408
- include_details: {
409
- type: "boolean",
410
- description: "Include full node details",
750
+ include_details: {
751
+ type: "boolean",
752
+ description: "Include full node details (description, context, agent_instructions, etc.). Default is false for efficient context usage.",
411
753
  default: false
412
754
  }
413
755
  },
@@ -424,13 +766,405 @@ function setupTools(server) {
424
766
  },
425
767
  required: ["plan_id"]
426
768
  }
427
- }
428
- ]
429
- };
430
- });
431
-
432
- // Handler for calling tools
433
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
769
+ },
770
+
771
+ // ===== AGENT CONTEXT TOOLS (Leaf-up context loading) =====
772
+ {
773
+ name: "get_agent_context",
774
+ description: "Get focused context for a specific task/node. Uses leaf-up traversal - returns only the relevant path from the node to root, not the entire plan tree. Best for agents starting work on a specific task.",
775
+ inputSchema: {
776
+ type: "object",
777
+ properties: {
778
+ node_id: {
779
+ type: "string",
780
+ description: "Task or phase node ID to get context for"
781
+ },
782
+ include_knowledge: {
783
+ type: "boolean",
784
+ description: "Include knowledge entries from relevant scopes (plan, goals, org)",
785
+ default: true
786
+ },
787
+ include_siblings: {
788
+ type: "boolean",
789
+ description: "Include sibling tasks in the same phase",
790
+ default: false
791
+ }
792
+ },
793
+ required: ["node_id"]
794
+ }
795
+ },
796
+ {
797
+ name: "get_plan_context",
798
+ description: "Get plan-level context overview. Returns plan details, phase summaries (not full tree), linked goals, and organization. Use get_agent_context for task-focused work.",
799
+ inputSchema: {
800
+ type: "object",
801
+ properties: {
802
+ plan_id: {
803
+ type: "string",
804
+ description: "Plan ID"
805
+ },
806
+ include_knowledge: {
807
+ type: "boolean",
808
+ description: "Include knowledge entries",
809
+ default: true
810
+ }
811
+ },
812
+ required: ["plan_id"]
813
+ }
814
+ },
815
+
816
+ // ===== ORGANIZATION TOOLS =====
817
+ {
818
+ name: "list_organizations",
819
+ description: "List all organizations the user is a member of",
820
+ inputSchema: {
821
+ type: "object",
822
+ properties: {}
823
+ }
824
+ },
825
+ {
826
+ name: "get_organization",
827
+ description: "Get organization details including member count and plan count",
828
+ inputSchema: {
829
+ type: "object",
830
+ properties: {
831
+ organization_id: { type: "string", description: "Organization ID" }
832
+ },
833
+ required: ["organization_id"]
834
+ }
835
+ },
836
+ {
837
+ name: "create_organization",
838
+ description: "Create a new organization. You become the owner.",
839
+ inputSchema: {
840
+ type: "object",
841
+ properties: {
842
+ name: { type: "string", description: "Organization name" },
843
+ description: { type: "string", description: "Organization description" },
844
+ slug: { type: "string", description: "URL-friendly slug (auto-generated if not provided)" }
845
+ },
846
+ required: ["name"]
847
+ }
848
+ },
849
+ {
850
+ name: "update_organization",
851
+ description: "Update organization details (owner only)",
852
+ inputSchema: {
853
+ type: "object",
854
+ properties: {
855
+ organization_id: { type: "string", description: "Organization ID" },
856
+ name: { type: "string", description: "New name" },
857
+ description: { type: "string", description: "New description" }
858
+ },
859
+ required: ["organization_id"]
860
+ }
861
+ },
862
+
863
+ // ===== GOAL TOOLS =====
864
+ {
865
+ name: "list_goals",
866
+ description: "List goals, optionally filtered by organization or status",
867
+ inputSchema: {
868
+ type: "object",
869
+ properties: {
870
+ organization_id: { type: "string", description: "Filter by organization ID" },
871
+ status: {
872
+ type: "string",
873
+ description: "Filter by status",
874
+ enum: ["active", "achieved", "at_risk", "abandoned"]
875
+ }
876
+ }
877
+ }
878
+ },
879
+ {
880
+ name: "get_goal",
881
+ description: "Get goal details including linked plans",
882
+ inputSchema: {
883
+ type: "object",
884
+ properties: {
885
+ goal_id: { type: "string", description: "Goal ID" }
886
+ },
887
+ required: ["goal_id"]
888
+ }
889
+ },
890
+ {
891
+ name: "create_goal",
892
+ description: "Create a new goal within an organization",
893
+ inputSchema: {
894
+ type: "object",
895
+ properties: {
896
+ organization_id: { type: "string", description: "Organization ID" },
897
+ title: { type: "string", description: "Goal title" },
898
+ description: { type: "string", description: "Goal description" },
899
+ success_metrics: {
900
+ type: "array",
901
+ description: "Success metrics array [{metric, target, current, unit}]",
902
+ items: {
903
+ type: "object",
904
+ properties: {
905
+ metric: { type: "string" },
906
+ target: { type: "number" },
907
+ current: { type: "number" },
908
+ unit: { type: "string" }
909
+ }
910
+ }
911
+ },
912
+ time_horizon: { type: "string", description: "Target date (ISO format)" },
913
+ github_repo_url: { type: "string", description: "Related GitHub repo URL" }
914
+ },
915
+ required: ["organization_id", "title"]
916
+ }
917
+ },
918
+ {
919
+ name: "update_goal",
920
+ description: "Update goal details or status",
921
+ inputSchema: {
922
+ type: "object",
923
+ properties: {
924
+ goal_id: { type: "string", description: "Goal ID" },
925
+ title: { type: "string", description: "New title" },
926
+ description: { type: "string", description: "New description" },
927
+ status: {
928
+ type: "string",
929
+ description: "New status",
930
+ enum: ["active", "achieved", "at_risk", "abandoned"]
931
+ },
932
+ success_metrics: { type: "array", description: "Updated metrics" },
933
+ time_horizon: { type: "string", description: "New target date" }
934
+ },
935
+ required: ["goal_id"]
936
+ }
937
+ },
938
+ {
939
+ name: "link_plan_to_goal",
940
+ description: "Link a plan to a goal (shows the plan contributes to this goal)",
941
+ inputSchema: {
942
+ type: "object",
943
+ properties: {
944
+ goal_id: { type: "string", description: "Goal ID" },
945
+ plan_id: { type: "string", description: "Plan ID to link" }
946
+ },
947
+ required: ["goal_id", "plan_id"]
948
+ }
949
+ },
950
+ {
951
+ name: "unlink_plan_from_goal",
952
+ description: "Remove a plan-goal link",
953
+ inputSchema: {
954
+ type: "object",
955
+ properties: {
956
+ goal_id: { type: "string", description: "Goal ID" },
957
+ plan_id: { type: "string", description: "Plan ID to unlink" }
958
+ },
959
+ required: ["goal_id", "plan_id"]
960
+ }
961
+ },
962
+
963
+ // ===== CROSS-PLAN & EXTERNAL DEPENDENCY TOOLS =====
964
+ {
965
+ name: "create_cross_plan_dependency",
966
+ 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.",
967
+ inputSchema: {
968
+ type: "object",
969
+ properties: {
970
+ source_node_id: { type: "string", description: "Source node ID (the blocker/prerequisite)" },
971
+ target_node_id: { type: "string", description: "Target node ID (the blocked/dependent task)" },
972
+ dependency_type: { type: "string", enum: ["blocks", "requires", "relates_to"], default: "blocks", description: "Edge type (default: blocks)" },
973
+ weight: { type: "number", description: "Edge weight (default 1)" }
974
+ },
975
+ required: ["source_node_id", "target_node_id"]
976
+ }
977
+ },
978
+ {
979
+ name: "list_cross_plan_dependencies",
980
+ description: "List all dependency edges that cross plan boundaries between specified plans.",
981
+ inputSchema: {
982
+ type: "object",
983
+ properties: {
984
+ plan_ids: {
985
+ type: "array",
986
+ items: { type: "string" },
987
+ description: "Plan IDs to check for cross-plan edges (at least 2)"
988
+ }
989
+ },
990
+ required: ["plan_ids"]
991
+ }
992
+ },
993
+ {
994
+ name: "create_external_dependency",
995
+ description: "Create an external dependency node representing a blocker outside the system (vendor API, legal approval, etc.). Optionally blocks a target task.",
996
+ inputSchema: {
997
+ type: "object",
998
+ properties: {
999
+ plan_id: { type: "string", description: "Plan to add the external dependency to" },
1000
+ title: { type: "string", description: "External dependency title (e.g., 'Waiting for vendor API access')" },
1001
+ description: { type: "string", description: "Details about the external dependency" },
1002
+ url: { type: "string", description: "URL reference (ticket, docs, etc.)" },
1003
+ blocks_node_id: { type: "string", description: "Node ID that this external dep blocks" }
1004
+ },
1005
+ required: ["plan_id", "title"]
1006
+ }
1007
+ },
1008
+
1009
+ // ===== GOAL-DEPENDENCY TOOLS =====
1010
+ {
1011
+ name: "goal_path",
1012
+ 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.",
1013
+ inputSchema: {
1014
+ type: "object",
1015
+ properties: {
1016
+ goal_id: { type: "string", description: "Goal ID" },
1017
+ max_depth: { type: "number", description: "Max traversal depth (default 20)" }
1018
+ },
1019
+ required: ["goal_id"]
1020
+ }
1021
+ },
1022
+ {
1023
+ name: "goal_progress",
1024
+ description: "Get goal progress calculated from its dependency graph. Returns overall completion percentage and direct achiever progress.",
1025
+ inputSchema: {
1026
+ type: "object",
1027
+ properties: {
1028
+ goal_id: { type: "string", description: "Goal ID" }
1029
+ },
1030
+ required: ["goal_id"]
1031
+ }
1032
+ },
1033
+ {
1034
+ name: "add_achiever",
1035
+ description: "Link a task to a goal via an 'achieves' dependency edge. This declares that completing this task contributes to achieving the goal.",
1036
+ inputSchema: {
1037
+ type: "object",
1038
+ properties: {
1039
+ goal_id: { type: "string", description: "Goal ID" },
1040
+ node_id: { type: "string", description: "Task/node ID that achieves this goal" },
1041
+ weight: { type: "number", description: "Edge weight for critical path (default 1)" }
1042
+ },
1043
+ required: ["goal_id", "node_id"]
1044
+ }
1045
+ },
1046
+ {
1047
+ name: "remove_achiever",
1048
+ description: "Remove an achieves edge between a task and a goal",
1049
+ inputSchema: {
1050
+ type: "object",
1051
+ properties: {
1052
+ goal_id: { type: "string", description: "Goal ID" },
1053
+ dependency_id: { type: "string", description: "Dependency edge ID to remove" }
1054
+ },
1055
+ required: ["goal_id", "dependency_id"]
1056
+ }
1057
+ },
1058
+ {
1059
+ name: "goal_knowledge_gaps",
1060
+ 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.",
1061
+ inputSchema: {
1062
+ type: "object",
1063
+ properties: {
1064
+ goal_id: { type: "string", description: "Goal ID" }
1065
+ },
1066
+ required: ["goal_id"]
1067
+ }
1068
+ },
1069
+
1070
+ // ===== GRAPHITI KNOWLEDGE GRAPH TOOLS =====
1071
+ {
1072
+ name: "add_learning",
1073
+ 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.",
1074
+ inputSchema: {
1075
+ type: "object",
1076
+ properties: {
1077
+ content: { type: "string", description: "The knowledge content — be detailed. Include context, reasoning, and conclusions." },
1078
+ title: { type: "string", description: "Short title/name for the episode" },
1079
+ entry_type: { type: "string", enum: ["decision", "learning", "context", "constraint"], description: "Type of knowledge" },
1080
+ plan_id: { type: "string", description: "Plan ID this knowledge relates to (optional)" },
1081
+ node_id: { type: "string", description: "Node/task ID this knowledge relates to (optional)" }
1082
+ },
1083
+ required: ["content"]
1084
+ }
1085
+ },
1086
+ {
1087
+ name: "recall_knowledge",
1088
+ 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.",
1089
+ inputSchema: {
1090
+ type: "object",
1091
+ properties: {
1092
+ query: { type: "string", description: "What to search for — be specific" },
1093
+ max_results: { type: "number", description: "Maximum results (default 10)", default: 10 }
1094
+ },
1095
+ required: ["query"]
1096
+ }
1097
+ },
1098
+ {
1099
+ name: "find_entities",
1100
+ description: "Search for entities (technologies, people, patterns, constraints) in the knowledge graph. Returns entity nodes with their relationships.",
1101
+ inputSchema: {
1102
+ type: "object",
1103
+ properties: {
1104
+ query: { type: "string", description: "Entity search query" },
1105
+ max_results: { type: "number", description: "Maximum results (default 10)", default: 10 }
1106
+ },
1107
+ required: ["query"]
1108
+ }
1109
+ },
1110
+
1111
+ {
1112
+ name: "check_contradictions",
1113
+ 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.",
1114
+ inputSchema: {
1115
+ type: "object",
1116
+ properties: {
1117
+ query: { type: "string", description: "Topic to check for contradictions" },
1118
+ max_results: { type: "number", description: "Maximum results (default 10)", default: 10 }
1119
+ },
1120
+ required: ["query"]
1121
+ }
1122
+ },
1123
+ {
1124
+ name: "get_recent_episodes",
1125
+ 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.",
1126
+ inputSchema: {
1127
+ type: "object",
1128
+ properties: {
1129
+ max_episodes: { type: "number", description: "Maximum episodes to return (default 20)", default: 20 }
1130
+ }
1131
+ }
1132
+ },
1133
+
1134
+ // ===== HELPER / GUIDANCE TOOLS =====
1135
+ {
1136
+ name: "get_started",
1137
+ 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.",
1138
+ inputSchema: {
1139
+ type: "object",
1140
+ properties: {
1141
+ topic: {
1142
+ type: "string",
1143
+ enum: ["overview", "planning", "execution", "knowledge", "collaboration"],
1144
+ description: "Specific topic to learn about: 'overview' (system intro), 'planning' (creating plans), 'execution' (working through tasks), 'knowledge' (storing decisions/learnings), 'collaboration' (working with others)",
1145
+ default: "overview"
1146
+ }
1147
+ }
1148
+ }
1149
+ },
1150
+ {
1151
+ name: "understand_context",
1152
+ description: "Get comprehensive context about a plan or goal - its purpose, current state, recent activity, blocked tasks, and relevant knowledge. Use this BEFORE starting work to understand the full situation.",
1153
+ inputSchema: {
1154
+ type: "object",
1155
+ properties: {
1156
+ plan_id: { type: "string", description: "Plan ID to understand" },
1157
+ goal_id: { type: "string", description: "Goal ID to understand" },
1158
+ include_knowledge: { type: "boolean", description: "Include relevant knowledge entries", default: true }
1159
+ }
1160
+ }
1161
+ }
1162
+ ]
1163
+ };
1164
+ });
1165
+
1166
+ // Handler for calling tools
1167
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
434
1168
  const { name, arguments: args } = request.params;
435
1169
 
436
1170
  // Only log in development mode
@@ -439,6 +1173,550 @@ function setupTools(server) {
439
1173
  }
440
1174
 
441
1175
  try {
1176
+ // ========================================
1177
+ // QUICK ACTIONS IMPLEMENTATIONS
1178
+ // ========================================
1179
+
1180
+ if (name === "quick_plan") {
1181
+ const { title, description, tasks, goal_id, organization_id } = args;
1182
+
1183
+ if (!tasks || tasks.length === 0) {
1184
+ return formatResponse({
1185
+ error: "At least one task is required",
1186
+ suggestion: "Provide an array of task titles in the 'tasks' parameter"
1187
+ });
1188
+ }
1189
+
1190
+ // Create the plan
1191
+ const planData = { title };
1192
+ if (description) planData.description = description;
1193
+ if (organization_id) planData.organization_id = organization_id;
1194
+
1195
+ const plan = await apiClient.plans.createPlan(planData);
1196
+
1197
+ // Get the root node
1198
+ const nodes = await apiClient.nodes.getNodes(plan.id);
1199
+ const rootNode = nodes.find(n => n.node_type === 'root') || nodes[0];
1200
+
1201
+ // Create a phase for the tasks
1202
+ const phase = await apiClient.nodes.createNode(plan.id, {
1203
+ parent_id: rootNode.id,
1204
+ node_type: 'phase',
1205
+ title: 'Tasks',
1206
+ status: 'not_started',
1207
+ order_index: 0
1208
+ });
1209
+
1210
+ // Create tasks
1211
+ const createdTasks = [];
1212
+ for (let i = 0; i < tasks.length; i++) {
1213
+ const task = await apiClient.nodes.createNode(plan.id, {
1214
+ parent_id: phase.id,
1215
+ node_type: 'task',
1216
+ title: tasks[i],
1217
+ status: 'not_started',
1218
+ order_index: i
1219
+ });
1220
+ createdTasks.push({ id: task.id, title: task.title });
1221
+ }
1222
+
1223
+ // Link to goal if provided
1224
+ if (goal_id) {
1225
+ try {
1226
+ await apiClient.goals.linkPlan(goal_id, plan.id);
1227
+ } catch (e) {
1228
+ // Goal linking failed, continue anyway
1229
+ }
1230
+ }
1231
+
1232
+ return formatResponse({
1233
+ success: true,
1234
+ message: `Plan "${title}" created with ${tasks.length} tasks`,
1235
+ plan_id: plan.id,
1236
+ plan_url: buildPlanUrl(plan.id),
1237
+ phase_id: phase.id,
1238
+ task_ids: createdTasks.map(t => t.id),
1239
+ tasks: createdTasks,
1240
+ next_steps: [
1241
+ "Use quick_status to update task progress",
1242
+ "Use quick_log to document your work",
1243
+ "Use get_context to load full plan details"
1244
+ ]
1245
+ });
1246
+ }
1247
+
1248
+ if (name === "quick_task") {
1249
+ const { plan_id, title, description, phase_id, agent_instructions } = args;
1250
+
1251
+ // Get plan structure to find the phase
1252
+ const nodes = await apiClient.nodes.getNodes(plan_id);
1253
+ const flatNodes = flattenNodes(nodes);
1254
+
1255
+ // Find target phase
1256
+ let targetPhaseId = phase_id;
1257
+ if (!targetPhaseId) {
1258
+ // Find first phase
1259
+ const phases = flatNodes.filter(n => n.node_type === 'phase');
1260
+ if (phases.length > 0) {
1261
+ targetPhaseId = phases[0].id;
1262
+ } else {
1263
+ // No phase exists, create one
1264
+ const rootNode = flatNodes.find(n => n.node_type === 'root') || nodes[0];
1265
+ const newPhase = await apiClient.nodes.createNode(plan_id, {
1266
+ parent_id: rootNode.id,
1267
+ node_type: 'phase',
1268
+ title: 'Tasks',
1269
+ status: 'not_started',
1270
+ order_index: 0
1271
+ });
1272
+ targetPhaseId = newPhase.id;
1273
+ }
1274
+ }
1275
+
1276
+ // Get task count in phase for order_index
1277
+ const phaseTasks = flatNodes.filter(n => n.parent_id === targetPhaseId && n.node_type === 'task');
1278
+
1279
+ // Create the task
1280
+ const taskData = {
1281
+ parent_id: targetPhaseId,
1282
+ node_type: 'task',
1283
+ title,
1284
+ status: 'not_started',
1285
+ order_index: phaseTasks.length
1286
+ };
1287
+ if (description) taskData.description = description;
1288
+ if (agent_instructions) taskData.agent_instructions = agent_instructions;
1289
+
1290
+ const task = await apiClient.nodes.createNode(plan_id, taskData);
1291
+
1292
+ return formatResponse({
1293
+ success: true,
1294
+ message: `Task "${title}" added to plan`,
1295
+ task_id: task.id,
1296
+ plan_id: plan_id,
1297
+ phase_id: targetPhaseId,
1298
+ task_url: buildTaskUrl(plan_id, task.id),
1299
+ next_steps: [
1300
+ "Use quick_status to mark as in_progress when you start",
1301
+ "Use quick_log to document progress"
1302
+ ]
1303
+ });
1304
+ }
1305
+
1306
+ if (name === "quick_status") {
1307
+ const { task_id, plan_id, status, note } = args;
1308
+
1309
+ // Update the task status
1310
+ const updateData = { status };
1311
+ const updated = await apiClient.nodes.updateNode(plan_id, task_id, updateData);
1312
+
1313
+ // Add a log entry if note provided or if blocking
1314
+ if (note || status === 'blocked') {
1315
+ const logMessage = note || (status === 'blocked' ? 'Task blocked - needs attention' : `Status changed to ${status}`);
1316
+ try {
1317
+ await apiClient.logs.addLogEntry(plan_id, task_id, {
1318
+ type: status === 'blocked' ? 'blocker' : 'progress',
1319
+ content: logMessage
1320
+ });
1321
+ } catch (e) {
1322
+ // Log failed, continue
1323
+ }
1324
+ }
1325
+
1326
+ // Get next tasks for suggestion
1327
+ let nextTasks = [];
1328
+ try {
1329
+ const nodes = await apiClient.nodes.getNodes(plan_id);
1330
+ const flatNodes = flattenNodes(nodes);
1331
+ nextTasks = flatNodes
1332
+ .filter(n => n.node_type === 'task' && n.status === 'not_started')
1333
+ .slice(0, 3)
1334
+ .map(n => ({ id: n.id, title: n.title }));
1335
+ } catch (e) {
1336
+ // Failed to get next tasks, continue
1337
+ }
1338
+
1339
+ const response = {
1340
+ success: true,
1341
+ message: `Task status updated to "${status}"`,
1342
+ task_id,
1343
+ plan_id,
1344
+ new_status: status
1345
+ };
1346
+
1347
+ if (status === 'completed' && nextTasks.length > 0) {
1348
+ response.next_tasks = nextTasks;
1349
+ response.suggestion = "Here are the next tasks to work on";
1350
+ } else if (status === 'blocked') {
1351
+ response.suggestion = "Task marked as blocked. A human will be notified to help unblock.";
1352
+ }
1353
+
1354
+ return formatResponse(response);
1355
+ }
1356
+
1357
+ if (name === "quick_log") {
1358
+ const { task_id, plan_id, message, log_type = 'progress' } = args;
1359
+
1360
+ const logEntry = await apiClient.logs.addLogEntry(plan_id, task_id, {
1361
+ type: log_type,
1362
+ content: message
1363
+ });
1364
+
1365
+ return formatResponse({
1366
+ success: true,
1367
+ message: "Progress logged",
1368
+ log_id: logEntry.id,
1369
+ task_id,
1370
+ plan_id,
1371
+ logged: message,
1372
+ tip: "Good practice! Logging helps humans follow your work."
1373
+ });
1374
+ }
1375
+
1376
+ // ========================================
1377
+ // CONTEXT LOADING IMPLEMENTATIONS
1378
+ // ========================================
1379
+
1380
+ if (name === "get_context") {
1381
+ // Redirect to understand_context implementation (same functionality)
1382
+ const { plan_id, goal_id, include_knowledge = true } = args;
1383
+
1384
+ if (!plan_id && !goal_id) {
1385
+ return formatResponse({
1386
+ error: "Provide either plan_id or goal_id to get context",
1387
+ suggestion: "Use list_plans or list_goals to find IDs"
1388
+ });
1389
+ }
1390
+
1391
+ const context = {
1392
+ retrieved_at: new Date().toISOString()
1393
+ };
1394
+
1395
+ // Get goal context
1396
+ if (goal_id) {
1397
+ try {
1398
+ context.goal = await apiClient.goals.get(goal_id);
1399
+ if (include_knowledge) {
1400
+ try {
1401
+ const graphResult = await apiClient.graphiti.graphSearch({ query: context.goal?.title || '', max_results: 10 });
1402
+ context.goal_knowledge = graphResult?.results?.facts || [];
1403
+ } catch (e) {}
1404
+ }
1405
+ } catch (e) {
1406
+ context.goal_error = e.message;
1407
+ }
1408
+ }
1409
+
1410
+ // Get plan context
1411
+ if (plan_id) {
1412
+ try {
1413
+ context.plan = await apiClient.plans.getPlan(plan_id);
1414
+ context.plan_url = buildPlanUrl(plan_id);
1415
+
1416
+ const nodes = await apiClient.nodes.getNodes(plan_id);
1417
+ context.statistics = calculatePlanStatistics(nodes);
1418
+ context.progress_percentage = context.statistics.total > 0
1419
+ ? ((context.statistics.status_counts.completed / context.statistics.total) * 100).toFixed(1) + '%'
1420
+ : '0%';
1421
+
1422
+ const flatNodes = flattenNodes(nodes);
1423
+ context.needs_attention = {
1424
+ blocked: flatNodes.filter(n => n.status === 'blocked').map(n => ({
1425
+ id: n.id, title: n.title, type: n.node_type
1426
+ })),
1427
+ in_progress: flatNodes.filter(n => n.status === 'in_progress').map(n => ({
1428
+ id: n.id, title: n.title, type: n.node_type
1429
+ })),
1430
+ ready_to_start: flatNodes
1431
+ .filter(n => n.node_type === 'task' && n.status === 'not_started')
1432
+ .slice(0, 5)
1433
+ .map(n => ({ id: n.id, title: n.title }))
1434
+ };
1435
+
1436
+ try {
1437
+ const activity = await apiClient.activity.getPlanActivity(plan_id);
1438
+ context.recent_activity = (activity || []).slice(0, 5);
1439
+ } catch (e) {}
1440
+
1441
+ if (include_knowledge) {
1442
+ try {
1443
+ const graphResult = await apiClient.graphiti.graphSearch({ query: context.plan?.title || '', max_results: 10 });
1444
+ context.plan_knowledge = graphResult?.results?.facts || [];
1445
+ } catch (e) {}
1446
+ }
1447
+ } catch (e) {
1448
+ context.plan_error = e.message;
1449
+ }
1450
+ }
1451
+
1452
+ context.recommendation = context.needs_attention?.blocked?.length > 0
1453
+ ? "⚠️ There are blocked tasks that need attention first"
1454
+ : context.needs_attention?.in_progress?.length > 0
1455
+ ? "Continue working on the in-progress tasks"
1456
+ : "Pick a task from ready_to_start to begin";
1457
+
1458
+ return formatResponse(context);
1459
+ }
1460
+
1461
+ if (name === "get_my_tasks") {
1462
+ const { plan_id, status = ["blocked", "in_progress"] } = args;
1463
+
1464
+ const tasks = {
1465
+ retrieved_at: new Date().toISOString(),
1466
+ needs_attention: [],
1467
+ ready_to_start: []
1468
+ };
1469
+
1470
+ // Get plans to check
1471
+ let plansToCheck = [];
1472
+ if (plan_id) {
1473
+ plansToCheck = [{ id: plan_id }];
1474
+ } else {
1475
+ plansToCheck = await apiClient.plans.getPlans();
1476
+ }
1477
+
1478
+ for (const plan of plansToCheck.slice(0, 10)) { // Limit to 10 plans
1479
+ try {
1480
+ const nodes = await apiClient.nodes.getNodes(plan.id);
1481
+ const flatNodes = flattenNodes(nodes);
1482
+
1483
+ const matchingTasks = flatNodes
1484
+ .filter(n => n.node_type === 'task' && status.includes(n.status))
1485
+ .map(n => ({
1486
+ id: n.id,
1487
+ title: n.title,
1488
+ status: n.status,
1489
+ plan_id: plan.id,
1490
+ plan_title: plan.title
1491
+ }));
1492
+
1493
+ tasks.needs_attention.push(...matchingTasks);
1494
+
1495
+ // Also get a few ready-to-start tasks
1496
+ const readyTasks = flatNodes
1497
+ .filter(n => n.node_type === 'task' && n.status === 'not_started')
1498
+ .slice(0, 3)
1499
+ .map(n => ({
1500
+ id: n.id,
1501
+ title: n.title,
1502
+ plan_id: plan.id,
1503
+ plan_title: plan.title
1504
+ }));
1505
+
1506
+ tasks.ready_to_start.push(...readyTasks);
1507
+ } catch (e) {
1508
+ // Skip this plan
1509
+ }
1510
+ }
1511
+
1512
+ tasks.summary = {
1513
+ blocked: tasks.needs_attention.filter(t => t.status === 'blocked').length,
1514
+ in_progress: tasks.needs_attention.filter(t => t.status === 'in_progress').length,
1515
+ ready: tasks.ready_to_start.length
1516
+ };
1517
+
1518
+ return formatResponse(tasks);
1519
+ }
1520
+
1521
+ // add_learning handled in GRAPHITI KNOWLEDGE GRAPH HANDLERS section below
1522
+
1523
+ // ========================================
1524
+ // MARKDOWN EXPORT
1525
+ // ========================================
1526
+
1527
+ if (name === "export_plan_markdown") {
1528
+ const { plan_id, include_descriptions = true, include_status = true } = args;
1529
+
1530
+ const plan = await apiClient.plans.getPlan(plan_id);
1531
+ const nodes = await apiClient.nodes.getNodes(plan_id);
1532
+
1533
+ let markdown = `# ${plan.title}\n\n`;
1534
+ if (plan.description) {
1535
+ markdown += `${plan.description}\n\n`;
1536
+ }
1537
+
1538
+ const statusEmoji = {
1539
+ not_started: '⬜',
1540
+ in_progress: '🔄',
1541
+ completed: '✅',
1542
+ blocked: '🚫',
1543
+ cancelled: '❌'
1544
+ };
1545
+
1546
+ // Process nodes recursively
1547
+ const processNode = (node, depth = 0) => {
1548
+ const indent = ' '.repeat(depth);
1549
+ const status = include_status ? (statusEmoji[node.status] || '⬜') + ' ' : '';
1550
+
1551
+ if (node.node_type === 'phase') {
1552
+ markdown += `\n${indent}## ${node.title}\n`;
1553
+ if (include_descriptions && node.description) {
1554
+ markdown += `${indent}${node.description}\n`;
1555
+ }
1556
+ } else if (node.node_type === 'task') {
1557
+ markdown += `${indent}- ${status}${node.title}\n`;
1558
+ if (include_descriptions && node.description) {
1559
+ markdown += `${indent} _${node.description}_\n`;
1560
+ }
1561
+ } else if (node.node_type === 'milestone') {
1562
+ markdown += `${indent}- 🎯 ${status}**${node.title}**\n`;
1563
+ }
1564
+
1565
+ if (node.children) {
1566
+ node.children.forEach(child => processNode(child, depth + 1));
1567
+ }
1568
+ };
1569
+
1570
+ // Start from root's children
1571
+ if (nodes.length > 0 && nodes[0].children) {
1572
+ nodes[0].children.forEach(child => processNode(child, 0));
1573
+ }
1574
+
1575
+ return formatResponse({
1576
+ plan_id,
1577
+ title: plan.title,
1578
+ markdown,
1579
+ tip: "You can save this to a file or share it as text"
1580
+ });
1581
+ }
1582
+
1583
+ if (name === "import_plan_markdown") {
1584
+ const { markdown, title: providedTitle, goal_id } = args;
1585
+
1586
+ // Parse markdown
1587
+ const lines = markdown.split('\n').map(l => l.trim()).filter(l => l);
1588
+
1589
+ let planTitle = providedTitle;
1590
+ let planDescription = '';
1591
+ const phases = [];
1592
+ let currentPhase = null;
1593
+
1594
+ for (const line of lines) {
1595
+ // H1 = Plan title
1596
+ if (line.startsWith('# ')) {
1597
+ if (!planTitle) {
1598
+ planTitle = line.slice(2).trim();
1599
+ }
1600
+ }
1601
+ // H2 = Phase
1602
+ else if (line.startsWith('## ')) {
1603
+ const phaseTitle = line.slice(3).trim();
1604
+ currentPhase = { title: phaseTitle, tasks: [] };
1605
+ phases.push(currentPhase);
1606
+ }
1607
+ // List item = Task (with optional status emoji)
1608
+ else if (line.startsWith('- ') || line.startsWith('* ')) {
1609
+ let taskTitle = line.slice(2).trim();
1610
+ let taskStatus = 'not_started';
1611
+
1612
+ // Parse status from emoji
1613
+ if (taskTitle.startsWith('✅')) {
1614
+ taskStatus = 'completed';
1615
+ taskTitle = taskTitle.slice(1).trim();
1616
+ } else if (taskTitle.startsWith('🔄')) {
1617
+ taskStatus = 'in_progress';
1618
+ taskTitle = taskTitle.slice(1).trim();
1619
+ } else if (taskTitle.startsWith('🚫')) {
1620
+ taskStatus = 'blocked';
1621
+ taskTitle = taskTitle.slice(1).trim();
1622
+ } else if (taskTitle.startsWith('⬜')) {
1623
+ taskTitle = taskTitle.slice(1).trim();
1624
+ }
1625
+
1626
+ // Skip milestone markers for now
1627
+ if (taskTitle.startsWith('🎯')) {
1628
+ taskTitle = taskTitle.slice(1).trim().replace(/\*\*/g, '');
1629
+ }
1630
+
1631
+ if (currentPhase) {
1632
+ currentPhase.tasks.push({ title: taskTitle, status: taskStatus });
1633
+ } else {
1634
+ // No phase yet, create a default one
1635
+ currentPhase = { title: 'Tasks', tasks: [{ title: taskTitle, status: taskStatus }] };
1636
+ phases.push(currentPhase);
1637
+ }
1638
+ }
1639
+ // Regular text after title = description
1640
+ else if (planTitle && !currentPhase && !line.startsWith('#')) {
1641
+ planDescription += (planDescription ? ' ' : '') + line;
1642
+ }
1643
+ }
1644
+
1645
+ if (!planTitle) {
1646
+ return formatResponse({
1647
+ error: "Could not extract plan title",
1648
+ suggestion: "Add a '# Title' heading at the start, or provide the 'title' parameter"
1649
+ });
1650
+ }
1651
+
1652
+ if (phases.length === 0) {
1653
+ return formatResponse({
1654
+ error: "No phases or tasks found",
1655
+ suggestion: "Use '## Phase Name' for phases and '- Task name' for tasks"
1656
+ });
1657
+ }
1658
+
1659
+ // Create the plan
1660
+ const planData = { title: planTitle };
1661
+ if (planDescription) planData.description = planDescription;
1662
+
1663
+ const plan = await apiClient.plans.createPlan(planData);
1664
+
1665
+ // Get root node
1666
+ const nodes = await apiClient.nodes.getNodes(plan.id);
1667
+ const rootNode = nodes.find(n => n.node_type === 'root') || nodes[0];
1668
+
1669
+ // Create phases and tasks
1670
+ const createdPhases = [];
1671
+ const createdTasks = [];
1672
+
1673
+ for (let i = 0; i < phases.length; i++) {
1674
+ const phaseData = phases[i];
1675
+
1676
+ const phase = await apiClient.nodes.createNode(plan.id, {
1677
+ parent_id: rootNode.id,
1678
+ node_type: 'phase',
1679
+ title: phaseData.title,
1680
+ status: 'not_started',
1681
+ order_index: i
1682
+ });
1683
+ createdPhases.push({ id: phase.id, title: phase.title });
1684
+
1685
+ for (let j = 0; j < phaseData.tasks.length; j++) {
1686
+ const taskData = phaseData.tasks[j];
1687
+
1688
+ const task = await apiClient.nodes.createNode(plan.id, {
1689
+ parent_id: phase.id,
1690
+ node_type: 'task',
1691
+ title: taskData.title,
1692
+ status: taskData.status,
1693
+ order_index: j
1694
+ });
1695
+ createdTasks.push({ id: task.id, title: task.title, status: task.status });
1696
+ }
1697
+ }
1698
+
1699
+ // Link to goal if provided
1700
+ if (goal_id) {
1701
+ try {
1702
+ await apiClient.goals.linkPlan(goal_id, plan.id);
1703
+ } catch (e) {}
1704
+ }
1705
+
1706
+ return formatResponse({
1707
+ success: true,
1708
+ message: `Plan "${planTitle}" created from markdown with ${phases.length} phases and ${createdTasks.length} tasks`,
1709
+ plan_id: plan.id,
1710
+ plan_url: buildPlanUrl(plan.id),
1711
+ phases: createdPhases,
1712
+ tasks: createdTasks,
1713
+ next_steps: [
1714
+ "Use get_context to review the imported plan",
1715
+ "Use quick_status to update task progress"
1716
+ ]
1717
+ });
1718
+ }
1719
+
442
1720
  // ===== UNIFIED SEARCH TOOL =====
443
1721
  if (name === "search") {
444
1722
  const { scope, scope_id, query, filters = {} } = args;
@@ -544,6 +1822,33 @@ function setupTools(server) {
544
1822
  });
545
1823
  }
546
1824
 
1825
+ if (name === "share_plan") {
1826
+ const { plan_id, visibility = "public", github_repo_owner, github_repo_name } = args;
1827
+
1828
+ const visibilityData = { visibility };
1829
+ if (github_repo_owner) visibilityData.github_repo_owner = github_repo_owner;
1830
+ if (github_repo_name) visibilityData.github_repo_name = github_repo_name;
1831
+
1832
+ const result = await apiClient.plans.updateVisibility(plan_id, visibilityData);
1833
+
1834
+ const shareUrl = visibility === "public"
1835
+ ? buildPlanUrl(plan_id)
1836
+ : null;
1837
+
1838
+ return formatResponse({
1839
+ success: true,
1840
+ plan_id: plan_id,
1841
+ visibility: result.visibility,
1842
+ is_public: result.is_public,
1843
+ share_url: shareUrl,
1844
+ github_repo_owner: result.github_repo_owner,
1845
+ github_repo_name: result.github_repo_name,
1846
+ message: visibility === "public"
1847
+ ? `Plan is now public. Share URL: ${shareUrl}`
1848
+ : `Plan is now private.`
1849
+ });
1850
+ }
1851
+
547
1852
  // ===== NODE MANAGEMENT =====
548
1853
  if (name === "create_node") {
549
1854
  const { plan_id, ...nodeData } = args;
@@ -568,27 +1873,29 @@ function setupTools(server) {
568
1873
 
569
1874
  if (name === "move_node") {
570
1875
  const { plan_id, node_id, parent_id, order_index } = args;
571
-
1876
+
572
1877
  try {
1878
+ // Build request body with only provided fields - don't send nulls
1879
+ const body = {};
1880
+ if (parent_id) body.parent_id = parent_id;
1881
+ if (order_index !== undefined) body.order_index = order_index;
1882
+
573
1883
  // Call the move endpoint - using POST as per API definition
574
1884
  const response = await apiClient.axiosInstance.post(
575
1885
  `/plans/${plan_id}/nodes/${node_id}/move`,
576
- {
577
- parent_id: parent_id || null,
578
- order_index: order_index !== undefined ? order_index : null
579
- }
1886
+ body
580
1887
  );
581
-
1888
+
582
1889
  return formatResponse(response.data);
583
1890
  } catch (error) {
584
1891
  // If endpoint still doesn't work, try updating the node directly
585
1892
  if (error.response && error.response.status === 404) {
586
1893
  console.error('Move endpoint not found, trying direct update');
587
1894
  // Fallback to updating the node's parent_id via regular update
588
- const updateResponse = await apiClient.nodes.updateNode(plan_id, node_id, {
589
- parent_id: parent_id || null,
590
- order_index: order_index !== undefined ? order_index : null
591
- });
1895
+ const updateData = {};
1896
+ if (parent_id) updateData.parent_id = parent_id;
1897
+ if (order_index !== undefined) updateData.order_index = order_index;
1898
+ const updateResponse = await apiClient.nodes.updateNode(plan_id, node_id, updateData);
592
1899
  return formatResponse(updateResponse);
593
1900
  }
594
1901
  throw error;
@@ -617,6 +1924,90 @@ function setupTools(server) {
617
1924
  return formatResponse(response.data);
618
1925
  }
619
1926
 
1927
+ // ===== DEPENDENCIES =====
1928
+ if (name === "create_dependency") {
1929
+ const { plan_id, source_node_id, target_node_id, dependency_type, weight, metadata } = args;
1930
+ const response = await apiClient.axiosInstance.post(
1931
+ `/plans/${plan_id}/dependencies`,
1932
+ { source_node_id, target_node_id, dependency_type, weight, metadata }
1933
+ );
1934
+ return formatResponse(response.data);
1935
+ }
1936
+
1937
+ if (name === "delete_dependency") {
1938
+ const { plan_id, dependency_id } = args;
1939
+ const response = await apiClient.axiosInstance.delete(
1940
+ `/plans/${plan_id}/dependencies/${dependency_id}`
1941
+ );
1942
+ return formatResponse(response.data);
1943
+ }
1944
+
1945
+ if (name === "list_dependencies") {
1946
+ const { plan_id } = args;
1947
+ const response = await apiClient.axiosInstance.get(
1948
+ `/plans/${plan_id}/dependencies`
1949
+ );
1950
+ return formatResponse(response.data);
1951
+ }
1952
+
1953
+ if (name === "get_node_dependencies") {
1954
+ const { plan_id, node_id, direction = 'both' } = args;
1955
+ const response = await apiClient.axiosInstance.get(
1956
+ `/plans/${plan_id}/nodes/${node_id}/dependencies`,
1957
+ { params: { direction } }
1958
+ );
1959
+ return formatResponse(response.data);
1960
+ }
1961
+
1962
+ // ===== RPI WORKFLOW =====
1963
+ if (name === "create_rpi_chain") {
1964
+ const { plan_id, title, description, parent_id } = args;
1965
+ const response = await apiClient.axiosInstance.post(
1966
+ `/plans/${plan_id}/nodes/rpi-chain`,
1967
+ { title, description, parent_id }
1968
+ );
1969
+ return formatResponse(response.data);
1970
+ }
1971
+
1972
+ // ===== ANALYSIS =====
1973
+ if (name === "analyze_impact") {
1974
+ const { plan_id, node_id, scenario = 'block' } = args;
1975
+ const response = await apiClient.axiosInstance.get(
1976
+ `/plans/${plan_id}/nodes/${node_id}/impact`,
1977
+ { params: { scenario } }
1978
+ );
1979
+ return formatResponse(response.data);
1980
+ }
1981
+
1982
+ if (name === "get_critical_path") {
1983
+ const { plan_id } = args;
1984
+ const response = await apiClient.axiosInstance.get(
1985
+ `/plans/${plan_id}/critical-path`
1986
+ );
1987
+ return formatResponse(response.data);
1988
+ }
1989
+
1990
+ // ===== PROGRESSIVE CONTEXT =====
1991
+ if (name === "get_task_context") {
1992
+ const { node_id, depth = 2, token_budget = 0, log_limit = 10, include_research = true } = args;
1993
+ const params = new URLSearchParams({
1994
+ node_id,
1995
+ depth: String(depth),
1996
+ token_budget: String(token_budget),
1997
+ log_limit: String(log_limit),
1998
+ include_research: String(include_research),
1999
+ });
2000
+ const response = await apiClient.axiosInstance.get(`/context/progressive?${params.toString()}`);
2001
+ return formatResponse(response.data);
2002
+ }
2003
+
2004
+ if (name === "suggest_next_tasks") {
2005
+ const { plan_id, limit = 5 } = args;
2006
+ const params = new URLSearchParams({ plan_id, limit: String(limit) });
2007
+ const response = await apiClient.axiosInstance.get(`/context/suggest?${params.toString()}`);
2008
+ return formatResponse(response.data);
2009
+ }
2010
+
620
2011
  // ===== LOGGING =====
621
2012
  if (name === "add_log") {
622
2013
  const { plan_id, node_id, content, log_type = "comment", tags } = args;
@@ -647,48 +2038,6 @@ function setupTools(server) {
647
2038
  return formatResponse(logs);
648
2039
  }
649
2040
 
650
- // ===== ARTIFACT MANAGEMENT =====
651
- if (name === "manage_artifact") {
652
- const { action, plan_id, node_id, ...params } = args;
653
-
654
- switch (action) {
655
- case "add":
656
- const { name, content_type, url, metadata } = params;
657
- const newArtifact = await apiClient.artifacts.addArtifact(plan_id, node_id, {
658
- name,
659
- content_type,
660
- url,
661
- metadata
662
- });
663
- return formatResponse(newArtifact);
664
-
665
- case "get":
666
- const { artifact_id } = params;
667
- const artifact = await apiClient.artifacts.getArtifact(plan_id, node_id, artifact_id);
668
- const content = await apiClient.artifacts.getArtifactContent(plan_id, node_id, artifact_id);
669
- return formatResponse({
670
- ...artifact,
671
- content
672
- });
673
-
674
- case "search":
675
- const { name: searchName } = params;
676
- const artifacts = await apiClient.artifacts.getArtifacts(plan_id, node_id);
677
- const searchLower = searchName.toLowerCase();
678
- const matches = artifacts.filter(a =>
679
- a.name.toLowerCase().includes(searchLower)
680
- );
681
- return formatResponse(matches);
682
-
683
- case "list":
684
- const allArtifacts = await apiClient.artifacts.getArtifacts(plan_id, node_id);
685
- return formatResponse(allArtifacts);
686
-
687
- default:
688
- throw new Error(`Unknown artifact action: ${action}`);
689
- }
690
- }
691
-
692
2041
  // ===== BATCH OPERATIONS =====
693
2042
  if (name === "batch_update_nodes") {
694
2043
  const { plan_id, updates } = args;
@@ -715,49 +2064,16 @@ function setupTools(server) {
715
2064
  });
716
2065
  }
717
2066
 
718
- if (name === "batch_get_artifacts") {
719
- const { plan_id, artifact_requests } = args;
720
-
721
- const results = [];
722
- const errors = [];
723
-
724
- for (const request of artifact_requests) {
725
- const { node_id, artifact_id } = request;
726
- try {
727
- const artifact = await apiClient.artifacts.getArtifact(plan_id, node_id, artifact_id);
728
- const content = await apiClient.artifacts.getArtifactContent(plan_id, node_id, artifact_id);
729
- results.push({
730
- node_id,
731
- artifact_id,
732
- success: true,
733
- data: { ...artifact, content }
734
- });
735
- } catch (error) {
736
- errors.push({
737
- node_id,
738
- artifact_id,
739
- success: false,
740
- error: error.message
741
- });
742
- }
743
- }
744
-
745
- return formatResponse({
746
- total: artifact_requests.length,
747
- successful: results.length,
748
- failed: errors.length,
749
- results,
750
- errors
751
- });
752
- }
753
-
754
2067
  // ===== PLAN STRUCTURE & SUMMARY =====
755
2068
  if (name === "get_plan_structure") {
756
2069
  const { plan_id, include_details = false } = args;
757
-
2070
+
758
2071
  const plan = await apiClient.plans.getPlan(plan_id);
759
- const nodes = await apiClient.nodes.getNodes(plan_id);
760
-
2072
+ // Pass include_details to the API - defaults to minimal fields
2073
+ const nodes = await apiClient.nodes.getNodes(plan_id, {
2074
+ include_details: include_details
2075
+ });
2076
+
761
2077
  // The API already returns a tree structure, not a flat list
762
2078
  // If it's already hierarchical, use it directly
763
2079
  let structure;
@@ -768,7 +2084,7 @@ function setupTools(server) {
768
2084
  // Flat list - build hierarchy
769
2085
  structure = buildNodeHierarchy(nodes, include_details);
770
2086
  }
771
-
2087
+
772
2088
  return formatResponse({
773
2089
  plan: {
774
2090
  id: plan.id,
@@ -805,6 +2121,494 @@ function setupTools(server) {
805
2121
  });
806
2122
  }
807
2123
 
2124
+ // ===== AGENT CONTEXT TOOLS =====
2125
+ if (name === "get_agent_context") {
2126
+ const { node_id, include_knowledge = true, include_siblings = false } = args;
2127
+
2128
+ const result = await apiClient.context.getNodeContext(node_id, {
2129
+ include_knowledge,
2130
+ include_siblings
2131
+ });
2132
+
2133
+ return formatResponse(result);
2134
+ }
2135
+
2136
+ if (name === "get_plan_context") {
2137
+ const { plan_id, include_knowledge = true } = args;
2138
+
2139
+ const result = await apiClient.context.getPlanContext(plan_id, {
2140
+ include_knowledge
2141
+ });
2142
+
2143
+ return formatResponse(result);
2144
+ }
2145
+
2146
+ // ===== ORGANIZATION TOOLS =====
2147
+ if (name === "list_organizations") {
2148
+ const result = await apiClient.organizations.list();
2149
+ return formatResponse(result);
2150
+ }
2151
+
2152
+ if (name === "get_organization") {
2153
+ const { organization_id } = args;
2154
+ const result = await apiClient.organizations.get(organization_id);
2155
+ return formatResponse(result);
2156
+ }
2157
+
2158
+ if (name === "create_organization") {
2159
+ const { name, description, slug } = args;
2160
+ const result = await apiClient.organizations.create({ name, description, slug });
2161
+ return formatResponse(result);
2162
+ }
2163
+
2164
+ if (name === "update_organization") {
2165
+ const { organization_id, ...updateData } = args;
2166
+ const result = await apiClient.organizations.update(organization_id, updateData);
2167
+ return formatResponse(result);
2168
+ }
2169
+
2170
+ // ===== GOAL TOOLS =====
2171
+ if (name === "list_goals") {
2172
+ const { organization_id, status } = args;
2173
+ const result = await apiClient.goals.list({ organization_id, status });
2174
+ return formatResponse(result);
2175
+ }
2176
+
2177
+ if (name === "get_goal") {
2178
+ const { goal_id } = args;
2179
+ const result = await apiClient.goals.get(goal_id);
2180
+ return formatResponse(result);
2181
+ }
2182
+
2183
+ if (name === "create_goal") {
2184
+ const { organization_id, title, description, success_metrics, time_horizon, github_repo_url } = args;
2185
+ const result = await apiClient.goals.create({
2186
+ organization_id,
2187
+ title,
2188
+ description,
2189
+ success_metrics,
2190
+ time_horizon,
2191
+ github_repo_url
2192
+ });
2193
+ return formatResponse(result);
2194
+ }
2195
+
2196
+ if (name === "update_goal") {
2197
+ const { goal_id, ...updateData } = args;
2198
+ const result = await apiClient.goals.update(goal_id, updateData);
2199
+ return formatResponse(result);
2200
+ }
2201
+
2202
+ if (name === "link_plan_to_goal") {
2203
+ const { goal_id, plan_id } = args;
2204
+ const result = await apiClient.goals.linkPlan(goal_id, plan_id);
2205
+ return formatResponse({
2206
+ success: true,
2207
+ message: `Plan ${plan_id} linked to goal ${goal_id}`,
2208
+ ...result
2209
+ });
2210
+ }
2211
+
2212
+ if (name === "unlink_plan_from_goal") {
2213
+ const { goal_id, plan_id } = args;
2214
+ const result = await apiClient.goals.unlinkPlan(goal_id, plan_id);
2215
+ return formatResponse({
2216
+ success: true,
2217
+ message: `Plan ${plan_id} unlinked from goal ${goal_id}`,
2218
+ ...result
2219
+ });
2220
+ }
2221
+
2222
+ // ===== CROSS-PLAN & EXTERNAL DEPENDENCY HANDLERS =====
2223
+ if (name === "create_cross_plan_dependency") {
2224
+ const { source_node_id, target_node_id, dependency_type, weight } = args;
2225
+ const result = await apiClient.dependencies.createCrossPlan({
2226
+ source_node_id, target_node_id, dependency_type, weight
2227
+ });
2228
+ return formatResponse(result);
2229
+ }
2230
+
2231
+ if (name === "list_cross_plan_dependencies") {
2232
+ const { plan_ids } = args;
2233
+ const result = await apiClient.dependencies.listCrossPlan(plan_ids);
2234
+ return formatResponse(result);
2235
+ }
2236
+
2237
+ if (name === "create_external_dependency") {
2238
+ const { plan_id, title, description, url, blocks_node_id } = args;
2239
+ const result = await apiClient.dependencies.createExternal({
2240
+ plan_id, title, description, url, blocks_node_id
2241
+ });
2242
+ return formatResponse(result);
2243
+ }
2244
+
2245
+ // ===== GOAL-DEPENDENCY HANDLERS =====
2246
+ if (name === "goal_path") {
2247
+ const { goal_id, max_depth } = args;
2248
+ const result = await apiClient.goals.getPath(goal_id, max_depth);
2249
+ return formatResponse(result);
2250
+ }
2251
+
2252
+ if (name === "goal_progress") {
2253
+ const { goal_id } = args;
2254
+ const result = await apiClient.goals.getProgress(goal_id);
2255
+ return formatResponse(result);
2256
+ }
2257
+
2258
+ if (name === "add_achiever") {
2259
+ const { goal_id, node_id, weight } = args;
2260
+ const result = await apiClient.goals.addAchiever(goal_id, node_id, weight);
2261
+ return formatResponse({
2262
+ ...result,
2263
+ message: `Task ${node_id} now achieves goal ${goal_id}`,
2264
+ });
2265
+ }
2266
+
2267
+ if (name === "remove_achiever") {
2268
+ const { goal_id, dependency_id } = args;
2269
+ const result = await apiClient.goals.removeAchiever(goal_id, dependency_id);
2270
+ return formatResponse(result);
2271
+ }
2272
+
2273
+ if (name === "goal_knowledge_gaps") {
2274
+ const { goal_id } = args;
2275
+ const result = await apiClient.goals.getKnowledgeGaps(goal_id);
2276
+ return formatResponse(result);
2277
+ }
2278
+
2279
+ // ===== GRAPHITI KNOWLEDGE GRAPH HANDLERS =====
2280
+ if (name === "add_learning") {
2281
+ const { content, title, entry_type, plan_id, node_id } = args;
2282
+
2283
+ // Add to Graphiti temporal knowledge graph
2284
+ const result = await apiClient.graphiti.addEpisode({
2285
+ content,
2286
+ name: title,
2287
+ plan_id,
2288
+ node_id,
2289
+ metadata: { entry_type: entry_type || 'learning' },
2290
+ });
2291
+ return formatResponse({
2292
+ ...result,
2293
+ message: 'Knowledge recorded in temporal graph',
2294
+ tip: 'This is now searchable via recall_knowledge across all plans'
2295
+ });
2296
+ }
2297
+
2298
+ if (name === "recall_knowledge") {
2299
+ const { query, max_results = 10 } = args;
2300
+
2301
+ // Try Graphiti first (temporal, cross-plan)
2302
+ try {
2303
+ const graphResult = await apiClient.graphiti.graphSearch({ query, max_results });
2304
+ if (graphResult?.results) {
2305
+ return formatResponse({
2306
+ ...graphResult,
2307
+ source: 'graphiti_temporal_graph'
2308
+ });
2309
+ }
2310
+ } catch (err) {
2311
+ return formatResponse({
2312
+ results: [],
2313
+ source: 'graphiti_temporal_graph',
2314
+ error: 'Knowledge graph not available: ' + err.message,
2315
+ });
2316
+ }
2317
+ }
2318
+
2319
+ if (name === "find_entities") {
2320
+ const { query, max_results = 10 } = args;
2321
+
2322
+ try {
2323
+ const result = await apiClient.graphiti.searchEntities({ query, max_results });
2324
+ return formatResponse(result);
2325
+ } catch (err) {
2326
+ return formatResponse({
2327
+ error: 'Entity search requires the temporal knowledge graph (Graphiti)',
2328
+ detail: err.message
2329
+ });
2330
+ }
2331
+ }
2332
+
2333
+ if (name === "check_contradictions") {
2334
+ const { query, max_results = 10 } = args;
2335
+
2336
+ try {
2337
+ const result = await apiClient.graphiti.detectContradictions({ query, max_results });
2338
+ if (result.contradictions_found) {
2339
+ return formatResponse({
2340
+ ...result,
2341
+ warning: 'Some knowledge has been superseded. Review the "superseded" facts before proceeding.',
2342
+ });
2343
+ }
2344
+ return formatResponse({
2345
+ ...result,
2346
+ message: 'No contradictions found — all facts are current.',
2347
+ });
2348
+ } catch (err) {
2349
+ return formatResponse({
2350
+ error: 'Contradiction detection requires the temporal knowledge graph (Graphiti)',
2351
+ detail: err.message,
2352
+ });
2353
+ }
2354
+ }
2355
+
2356
+ if (name === "get_recent_episodes") {
2357
+ const { max_episodes = 20 } = args || {};
2358
+
2359
+ try {
2360
+ const result = await apiClient.graphiti.getEpisodes({ max_episodes });
2361
+ return formatResponse(result);
2362
+ } catch (err) {
2363
+ return formatResponse({
2364
+ error: 'Episodic memory requires the temporal knowledge graph (Graphiti)',
2365
+ detail: err.message
2366
+ });
2367
+ }
2368
+ }
2369
+
2370
+ // ===== HELPER TOOLS =====
2371
+ if (name === "get_started") {
2372
+ const { topic = "overview" } = args || {};
2373
+
2374
+ const guides = {
2375
+ overview: {
2376
+ title: "AgentPlanner Overview",
2377
+ description: "AgentPlanner is a collaborative planning system for AI agents and humans to work together on structured plans.",
2378
+ key_concepts: [
2379
+ "Organizations - Groups of users, goals, and resources",
2380
+ "Goals - High-level objectives with success metrics that plans work toward",
2381
+ "Plans - Hierarchical structures with phases, tasks, and milestones",
2382
+ "Nodes - Individual items in a plan (phases contain tasks and milestones)",
2383
+ "Knowledge - Persistent storage for decisions, context, constraints, and learnings"
2384
+ ],
2385
+ recommended_workflow: [
2386
+ "1. Check list_goals to understand current objectives",
2387
+ "2. Use list_plans to see existing plans",
2388
+ "3. Before working on a plan, use understand_context to get the full picture",
2389
+ "4. Update task statuses as you work (update_node with status)",
2390
+ "5. Store important decisions and learnings using add_learning",
2391
+ "6. Check recall_knowledge before making decisions to see past context"
2392
+ ],
2393
+ quick_tips: [
2394
+ "Always capture WHY decisions were made, not just WHAT",
2395
+ "Mark tasks 'blocked' with notes when stuck - this helps humans help you",
2396
+ "Use logs to document progress so others can follow your work"
2397
+ ]
2398
+ },
2399
+ planning: {
2400
+ title: "Planning Best Practices",
2401
+ description: "How to create and structure effective plans.",
2402
+ structure: [
2403
+ "Plans have a hierarchical structure: Plan → Phases → Tasks/Milestones",
2404
+ "Phases are major stages or milestones of work",
2405
+ "Tasks are actionable work items within phases",
2406
+ "Milestones mark significant checkpoints"
2407
+ ],
2408
+ tips: [
2409
+ "Break work into phases (major stages)",
2410
+ "Each phase should contain 3-7 tasks (not too granular, not too big)",
2411
+ "Add clear acceptance_criteria to tasks so completion is unambiguous",
2412
+ "Use agent_instructions to guide how AI agents should approach tasks",
2413
+ "Link plans to goals to track how work contributes to objectives"
2414
+ ],
2415
+ tools_to_use: ["create_plan", "create_node", "get_plan_structure", "link_plan_to_goal"]
2416
+ },
2417
+ execution: {
2418
+ title: "Executing Plans",
2419
+ description: "How to work through plans effectively.",
2420
+ workflow: [
2421
+ "1. Use get_plan_structure to see the full plan",
2422
+ "2. Find tasks with status 'not_started' or 'in_progress'",
2423
+ "3. Before starting a task, check recall_knowledge for relevant context",
2424
+ "4. Update task status to 'in_progress' when you begin",
2425
+ "5. Add logs to document what you're doing",
2426
+ "6. Mark 'completed' when done, or 'blocked' if stuck"
2427
+ ],
2428
+ status_values: {
2429
+ not_started: "Work hasn't begun",
2430
+ in_progress: "Currently being worked on",
2431
+ completed: "Finished and verified",
2432
+ blocked: "Cannot proceed - add notes explaining why",
2433
+ cancelled: "No longer needed"
2434
+ },
2435
+ tips: [
2436
+ "Check get_plan_summary for current progress and blockers",
2437
+ "When blocked, clearly document what's blocking you",
2438
+ "Store learnings as you go - don't wait until the end"
2439
+ ],
2440
+ tools_to_use: ["get_plan_structure", "update_node", "add_log", "recall_knowledge"]
2441
+ },
2442
+ knowledge: {
2443
+ title: "Knowledge Management",
2444
+ description: "How to capture and use organizational knowledge effectively.",
2445
+ entry_types: {
2446
+ decision: "Choices made and their rationale - ALWAYS capture WHY",
2447
+ context: "Background information needed to understand something",
2448
+ constraint: "Rules, limitations, or requirements that must be respected",
2449
+ learning: "Insights gained from experience - what worked, what didn't",
2450
+ reference: "Links to external resources or documentation",
2451
+ note: "General notes that don't fit other categories"
2452
+ },
2453
+ best_practices: [
2454
+ "ALWAYS capture significant decisions with reasoning",
2455
+ "Search knowledge BEFORE making decisions (check for constraints)",
2456
+ "Add learnings when you discover something useful",
2457
+ "Tag entries well for easier retrieval later",
2458
+ "Include enough context that future-you can understand"
2459
+ ],
2460
+ when_to_create_entries: [
2461
+ "When a decision is made (especially if non-obvious)",
2462
+ "When you learn something that might be useful later",
2463
+ "When you discover a constraint or rule",
2464
+ "When you find a useful resource or reference"
2465
+ ],
2466
+ tools_to_use: ["add_learning", "recall_knowledge", "find_entities", "check_contradictions"]
2467
+ },
2468
+ collaboration: {
2469
+ title: "Collaboration",
2470
+ description: "Working with humans and other agents.",
2471
+ tips: [
2472
+ "Plans can be shared with collaborators (viewer, editor, admin roles)",
2473
+ "Use logs to document progress so others can follow your work",
2474
+ "Knowledge stores are shared within their scope (org/goal/plan)",
2475
+ "When stuck, mark tasks as 'blocked' with clear notes - humans will see this"
2476
+ ],
2477
+ communication: [
2478
+ "Logs are visible to all plan collaborators",
2479
+ "Knowledge entries persist and are searchable by others",
2480
+ "Clear status updates help humans understand where things stand"
2481
+ ],
2482
+ tools_to_use: ["list_organizations", "list_goals", "add_log"]
2483
+ }
2484
+ };
2485
+
2486
+ return formatResponse(guides[topic] || guides.overview);
2487
+ }
2488
+
2489
+ if (name === "understand_context") {
2490
+ const { plan_id, goal_id, include_knowledge = true } = args;
2491
+
2492
+ if (!plan_id && !goal_id) {
2493
+ return formatResponse({
2494
+ error: "Provide either plan_id or goal_id to get context"
2495
+ });
2496
+ }
2497
+
2498
+ const context = {
2499
+ retrieved_at: new Date().toISOString()
2500
+ };
2501
+
2502
+ // Get goal context
2503
+ if (goal_id) {
2504
+ try {
2505
+ context.goal = await apiClient.goals.get(goal_id);
2506
+
2507
+ // Get related knowledge if available
2508
+ if (include_knowledge) {
2509
+ try {
2510
+ const graphResult = await apiClient.graphiti.graphSearch({ query: context.goal?.title || '', max_results: 10 });
2511
+ context.goal_knowledge = graphResult?.results?.facts || [];
2512
+ } catch (e) {
2513
+ // Knowledge fetch failed, continue without it
2514
+ }
2515
+ }
2516
+ } catch (e) {
2517
+ context.goal_error = e.message;
2518
+ }
2519
+ }
2520
+
2521
+ // Get plan context
2522
+ if (plan_id) {
2523
+ try {
2524
+ context.plan = await apiClient.plans.getPlan(plan_id);
2525
+
2526
+ const nodes = await apiClient.nodes.getNodes(plan_id);
2527
+ context.statistics = calculatePlanStatistics(nodes);
2528
+ context.progress_percentage = context.statistics.total > 0
2529
+ ? ((context.statistics.status_counts.completed / context.statistics.total) * 100).toFixed(1) + '%'
2530
+ : '0%';
2531
+
2532
+ // Get blocked and in-progress tasks for attention
2533
+ const flatNodes = flattenNodes(nodes);
2534
+ context.needs_attention = {
2535
+ blocked: flatNodes.filter(n => n.status === 'blocked').map(n => ({
2536
+ id: n.id,
2537
+ title: n.title,
2538
+ type: n.node_type
2539
+ })),
2540
+ in_progress: flatNodes.filter(n => n.status === 'in_progress').map(n => ({
2541
+ id: n.id,
2542
+ title: n.title,
2543
+ type: n.node_type
2544
+ }))
2545
+ };
2546
+
2547
+ // Get recent activity
2548
+ try {
2549
+ const activity = await apiClient.activity.getPlanActivity(plan_id);
2550
+ context.recent_activity = (activity || []).slice(0, 5);
2551
+ } catch (e) {
2552
+ // Activity fetch failed, continue without it
2553
+ }
2554
+
2555
+ // Get related knowledge if available
2556
+ if (include_knowledge) {
2557
+ try {
2558
+ const graphResult = await apiClient.graphiti.graphSearch({ query: context.plan?.title || '', max_results: 10 });
2559
+ context.plan_knowledge = graphResult?.results?.facts || [];
2560
+ } catch (e) {
2561
+ // Knowledge fetch failed, continue without it
2562
+ }
2563
+ }
2564
+ } catch (e) {
2565
+ context.plan_error = e.message;
2566
+ }
2567
+ }
2568
+
2569
+ context.recommendation = "Review the statistics, needs_attention, and knowledge entries before starting work.";
2570
+ return formatResponse(context);
2571
+ }
2572
+
2573
+ // ===== GOALS HEALTH DASHBOARD =====
2574
+ if (name === "check_goals_health") {
2575
+ const { status_filter } = args || {};
2576
+ const result = await apiClient.goals.getDashboard();
2577
+
2578
+ let goals = result.goals || result;
2579
+ if (status_filter && Array.isArray(goals)) {
2580
+ goals = goals.filter(g => g.health_status === status_filter || g.status === status_filter);
2581
+ }
2582
+
2583
+ return formatResponse({
2584
+ ...result,
2585
+ goals,
2586
+ tip: "Prioritize: stale goals first, then at_risk, then on_track."
2587
+ });
2588
+ }
2589
+
2590
+ // ===== TASK CLAIMING =====
2591
+ if (name === "claim_task") {
2592
+ const { task_id, plan_id, ttl_minutes = 30 } = args;
2593
+ const result = await apiClient.nodes.claimTask(plan_id, task_id, 'mcp-agent', ttl_minutes);
2594
+ return formatResponse({
2595
+ success: true,
2596
+ message: `Task ${task_id} claimed for ${ttl_minutes} minutes`,
2597
+ ...result,
2598
+ tip: "Remember to release the task when done, or it will auto-expire."
2599
+ });
2600
+ }
2601
+
2602
+ if (name === "release_task") {
2603
+ const { task_id, plan_id } = args;
2604
+ const result = await apiClient.nodes.releaseTask(plan_id, task_id, 'mcp-agent');
2605
+ return formatResponse({
2606
+ success: true,
2607
+ message: `Task ${task_id} released`,
2608
+ ...result
2609
+ });
2610
+ }
2611
+
808
2612
  // Tool not found
809
2613
  throw new Error(`Unknown tool: ${name}`);
810
2614
  } catch (error) {
@@ -935,6 +2739,26 @@ function buildNodeHierarchy(nodes, includeDetails = false) {
935
2739
  return rootNodes;
936
2740
  }
937
2741
 
2742
+ /**
2743
+ * Flatten a hierarchical node structure into a flat array
2744
+ */
2745
+ function flattenNodes(nodes) {
2746
+ const flat = [];
2747
+
2748
+ const processNode = (node) => {
2749
+ flat.push(node);
2750
+ if (node.children && node.children.length > 0) {
2751
+ node.children.forEach(processNode);
2752
+ }
2753
+ };
2754
+
2755
+ if (Array.isArray(nodes)) {
2756
+ nodes.forEach(processNode);
2757
+ }
2758
+
2759
+ return flat;
2760
+ }
2761
+
938
2762
  /**
939
2763
  * Calculate plan statistics
940
2764
  */