agent-planner-mcp 1.5.6 → 1.5.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-planner-mcp",
3
- "version": "1.5.6",
3
+ "version": "1.5.8",
4
4
  "description": "MCP server for AgentPlanner — AI agent orchestration with planning, dependencies, knowledge graphs, and human oversight",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -7,6 +7,21 @@
7
7
 
8
8
  const { asOf, formatResponse, errorResponse, safeArray, isV1Unavailable, planUrl } = require('./_shared');
9
9
 
10
+ // A Graphiti fact is superseded once it has an `expired_at`, or an `invalid_at`
11
+ // that is in the past — the temporal graph has replaced it with a newer truth.
12
+ // recall_knowledge used to return these inline with current facts, undistinguished,
13
+ // so an agent could act on stale knowledge. Tag each fact `status` and sort
14
+ // current-first so the valid facts read first.
15
+ function annotateFacts(facts, nowMs = Date.now()) {
16
+ const list = safeArray(facts).map((f) => {
17
+ const invalidMs = f.invalid_at ? new Date(f.invalid_at).getTime() : null;
18
+ const superseded = Boolean(f.expired_at) || (invalidMs != null && invalidMs <= nowMs);
19
+ return { ...f, status: superseded ? 'superseded' : 'current' };
20
+ });
21
+ list.sort((a, b) => (a.status === b.status ? 0 : a.status === 'current' ? -1 : 1));
22
+ return list;
23
+ }
24
+
10
25
  // ─────────────────────────────────────────────────────────────────────────
11
26
  // briefing — bundled mission control state. Replaces 4 round trips.
12
27
  // ─────────────────────────────────────────────────────────────────────────
@@ -343,6 +358,8 @@ const recallKnowledgeDefinition = {
343
358
  description:
344
359
  "Universal knowledge graph query. Returns facts, entities, recent episodes, " +
345
360
  "and contradictions in one shape. Use result_kind to control payload size. " +
361
+ "Each fact carries status: 'current' or 'superseded' (the graph has since " +
362
+ "replaced it) — facts are sorted current-first; prefer current facts. " +
346
363
  "Replaces recall_knowledge legacy + find_entities + get_recent_episodes + check_contradictions.",
347
364
  inputSchema: {
348
365
  type: 'object',
@@ -363,6 +380,9 @@ const recallKnowledgeDefinition = {
363
380
 
364
381
  async function recallKnowledgeHandler(args, apiClient) {
365
382
  const { query, scope = {}, since, entry_type = 'all', result_kind = 'all', max_results = 10, include_contradictions = false } = args;
383
+ if (!query || !String(query).trim()) {
384
+ return errorResponse('invalid_arg', 'recall_knowledge requires a non-empty query string');
385
+ }
366
386
 
367
387
  // v1 facade: one server-side call replaces the 4-endpoint fan-out below.
368
388
  if (apiClient.v1) {
@@ -370,6 +390,11 @@ async function recallKnowledgeHandler(args, apiClient) {
370
390
  const data = await apiClient.v1.knowledgeSearch({
371
391
  query, since, entry_type, result_kind, max_results, include_contradictions, ...scope,
372
392
  });
393
+ if (data && Array.isArray(data.facts)) {
394
+ data.facts = annotateFacts(data.facts);
395
+ const superseded = data.facts.filter((f) => f.status === 'superseded').length;
396
+ data.meta = { ...(data.meta || {}), superseded_fact_count: superseded };
397
+ }
373
398
  return formatResponse(data);
374
399
  } catch (err) {
375
400
  if (!isV1Unavailable(err)) {
@@ -407,7 +432,7 @@ async function recallKnowledgeHandler(args, apiClient) {
407
432
  return;
408
433
  }
409
434
  const v = s.value;
410
- if (key === 'facts') out.facts = safeArray(v.facts || v);
435
+ if (key === 'facts') out.facts = annotateFacts(v.facts || v);
411
436
  if (key === 'entities') out.entities = safeArray(v.entities || v);
412
437
  if (key === 'episodes') {
413
438
  let eps = safeArray(v.episodes?.episodes || v.episodes || v);
@@ -423,6 +448,7 @@ async function recallKnowledgeHandler(args, apiClient) {
423
448
  if (key === 'contradictions') out.contradictions = v;
424
449
  });
425
450
 
451
+ out.meta.superseded_fact_count = out.facts.filter((f) => f.status === 'superseded').length;
426
452
  return formatResponse(out);
427
453
  }
428
454
 
@@ -539,6 +565,9 @@ const searchDefinition = {
539
565
 
540
566
  async function searchHandler(args, apiClient) {
541
567
  const { query, scope = 'global', scope_id, filters = {} } = args;
568
+ if (!query || !String(query).trim()) {
569
+ return errorResponse('invalid_arg', 'search requires a non-empty query string');
570
+ }
542
571
  const limit = filters.limit || 20;
543
572
  try {
544
573
  let result;
@@ -601,8 +630,17 @@ const planAnalysisDefinition = {
601
630
  },
602
631
  };
603
632
 
633
+ const PLAN_ANALYSIS_TYPES = ['impact', 'critical_path', 'bottlenecks', 'coherence'];
634
+
604
635
  async function planAnalysisHandler(args, apiClient) {
605
636
  const { plan_id, type, node_id, scenario } = args;
637
+ // The hosted MCP transport does not enforce `required`, so a missing/unknown
638
+ // `type` would otherwise fall through every branch and return an empty
639
+ // `results: {}` with no explanation. Validate explicitly with a clear error.
640
+ if (!plan_id) return errorResponse('invalid_arg', 'plan_analysis requires plan_id');
641
+ if (!type || !PLAN_ANALYSIS_TYPES.includes(type)) {
642
+ return errorResponse('invalid_arg', `plan_analysis requires type (one of: ${PLAN_ANALYSIS_TYPES.join(', ')})`);
643
+ }
606
644
  try {
607
645
  let result;
608
646
  if (type === 'critical_path') {
@@ -616,7 +654,18 @@ async function planAnalysisHandler(args, apiClient) {
616
654
  } else if (type === 'coherence') {
617
655
  result = await apiClient.coherence.runCheck(plan_id);
618
656
  }
619
- return formatResponse({ as_of: asOf(), type, results: result || {} });
657
+ const payload = { as_of: asOf(), type, results: result || {} };
658
+ // Critical-path / bottleneck analysis is driven by `blocks` dependency
659
+ // edges. A flat (edgeless) plan returns empty here — without a hint the
660
+ // agent can't tell "nothing blocking" from "this plan has no edges at all".
661
+ const emptyCriticalPath = type === 'critical_path' && !(result?.nodes?.length);
662
+ const emptyBottlenecks = type === 'bottlenecks'
663
+ && !((Array.isArray(result) ? result : result?.bottlenecks)?.length);
664
+ if (emptyCriticalPath || emptyBottlenecks) {
665
+ payload.note = 'No dependency edges drive this result — the plan may be flat (no blocks edges). '
666
+ + 'Use task_context (depth 2+) to read its tasks, or add dependencies with link_intentions.';
667
+ }
668
+ return formatResponse(payload);
620
669
  } catch (err) {
621
670
  return errorResponse('upstream_unavailable', `plan_analysis failed: ${err.response?.data?.error || err.message}`);
622
671
  }