@neat.is/mcp 0.7.10 → 0.8.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/dist/index.cjs CHANGED
@@ -419,6 +419,14 @@ async function getRootCause(client2, input) {
419
419
  if (result.fixRecommendation) {
420
420
  blockLines.push(`Recommended fix: ${result.fixRecommendation}`);
421
421
  }
422
+ if (result.candidates && result.candidates.length > 0) {
423
+ blockLines.push("", "Candidates (ranked, most likely cause first):");
424
+ for (const c of result.candidates) {
425
+ blockLines.push(
426
+ ` \u2022 ${c.node} \u2014 ${c.classification} (confidence ${c.confidence.toFixed(2)}): ${c.reason}`
427
+ );
428
+ }
429
+ }
422
430
  return formatToolResponse({
423
431
  summary,
424
432
  block: blockLines.join("\n"),
@@ -553,6 +561,50 @@ function formatDuration(ms) {
553
561
  if (h < 48) return `${h}h`;
554
562
  return `${Math.round(h / 24)}d`;
555
563
  }
564
+ async function expandNode(client2, input) {
565
+ const path = projectPath(
566
+ input.project,
567
+ `/graph/expand/${encodeURIComponent(input.nodeId)}?direction=${input.direction}`
568
+ );
569
+ return withMissingNodeFallback(async () => {
570
+ const result = await client2.get(path);
571
+ const dirWord = input.direction === "up" ? "callers/dependents (up)" : "callees/dependencies (down)";
572
+ const summary = `${result.node.id} is ${result.node.classification}. ${result.neighbours.length} ${dirWord}.`;
573
+ const blockLines = result.neighbours.map(
574
+ (n) => ` \u2022 ${n.node} \u2014 ${n.classification} via ${n.edgeType} (${n.provenance})`
575
+ );
576
+ return formatToolResponse({
577
+ summary,
578
+ block: blockLines.length ? blockLines.join("\n") : "(no runtime neighbours in this direction)"
579
+ });
580
+ }, `Node ${input.nodeId} not found in the graph.`);
581
+ }
582
+ async function relate(client2, input) {
583
+ const qs = input.maxDepth !== void 0 ? `&maxDepth=${input.maxDepth}` : "";
584
+ const path = projectPath(
585
+ input.project,
586
+ `/graph/relate?a=${encodeURIComponent(input.a)}&b=${encodeURIComponent(input.b)}${qs}`
587
+ );
588
+ try {
589
+ const result = await client2.get(path);
590
+ if (!result.related) {
591
+ return formatEmptyResponse(
592
+ `${input.a} and ${input.b} are not related \u2014 ${result.note ?? "no path found"}.`
593
+ );
594
+ }
595
+ const arrow = result.direction === "a->b" ? `${input.a} \u2192 ${input.b}` : `${input.b} \u2192 ${input.a}`;
596
+ const carries = result.paths[0]?.carriesSignal ? "and the path carries the failure end to end" : "but the path carries no failure signal";
597
+ const gap = result.grainGap ? " (grain gap \u2014 only a coarser link is in evidence)" : "";
598
+ const summary = `${arrow}: a path exists ${carries}${gap}.`;
599
+ const blockLines = result.paths.map(
600
+ (p) => ` ${p.nodes.join(" \u2192 ")} [${p.edgeTypes.join(", ")}] carriesSignal=${p.carriesSignal}`
601
+ );
602
+ return formatToolResponse({ summary, block: blockLines.join("\n") });
603
+ } catch (err) {
604
+ if (err instanceof ProjectNotFoundError) return formatErrorResponse(err.message);
605
+ return formatErrorResponse(`Error talking to neat-core: ${err.message}`);
606
+ }
607
+ }
556
608
  async function getIncidentHistory(client2, input) {
557
609
  return withMissingNodeFallback(async () => {
558
610
  const body = await client2.get(
@@ -1076,6 +1128,27 @@ registerTool(
1076
1128
  },
1077
1129
  async (input) => getObservedDependencies(client, { ...input, project: projectFor(input) })
1078
1130
  );
1131
+ registerTool(
1132
+ "expand",
1133
+ 'Take one navigation step from a node and classify the neighbourhood (ADR-189). direction "up" walks to callers/dependents (who calls this), "down" walks to callees/dependencies (what this calls). Each neighbour comes back classified primary-failure / symptom-only / unrelated. Use this to navigate a failure one hop at a time instead of trusting a single verdict \u2014 a symptom-only node is a downstream victim, so walk "up" from it toward the real cause.',
1134
+ {
1135
+ nodeId: import_zod.z.string().describe("Graph node id to step from"),
1136
+ direction: import_zod.z.enum(["up", "down"]).describe("up = callers/dependents (toward the cause), down = callees/dependencies"),
1137
+ project: projectField
1138
+ },
1139
+ async (input) => expandNode(client, { ...input, project: projectFor(input) })
1140
+ );
1141
+ registerTool(
1142
+ "relate",
1143
+ 'Confirm whether two nodes are connected, which way, and whether the connecting path carries the failure (ADR-189). Returns the direction (a\u2192b or b\u2192a), the path with per-hop provenance, and carriesSignal \u2014 whether errors/latency run end to end, which turns "a path exists" into "a is actually causing b". No path within the depth bound returns "no path within N hops", never a false "unrelated".',
1144
+ {
1145
+ a: import_zod.z.string().describe("First node id (the hypothesised cause)"),
1146
+ b: import_zod.z.string().describe("Second node id (the hypothesised symptom)"),
1147
+ maxDepth: import_zod.z.number().int().min(1).max(10).optional().describe("Max path length to search (default 5)"),
1148
+ project: projectField
1149
+ },
1150
+ async (input) => relate(client, { ...input, project: projectFor(input) })
1151
+ );
1079
1152
  registerTool(
1080
1153
  "get_incident_history",
1081
1154
  "Return recent OTel error events recorded against a node, most recent first.",