@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.js CHANGED
@@ -424,6 +424,14 @@ async function getRootCause(client2, input) {
424
424
  if (result.fixRecommendation) {
425
425
  blockLines.push(`Recommended fix: ${result.fixRecommendation}`);
426
426
  }
427
+ if (result.candidates && result.candidates.length > 0) {
428
+ blockLines.push("", "Candidates (ranked, most likely cause first):");
429
+ for (const c of result.candidates) {
430
+ blockLines.push(
431
+ ` \u2022 ${c.node} \u2014 ${c.classification} (confidence ${c.confidence.toFixed(2)}): ${c.reason}`
432
+ );
433
+ }
434
+ }
427
435
  return formatToolResponse({
428
436
  summary,
429
437
  block: blockLines.join("\n"),
@@ -558,6 +566,50 @@ function formatDuration(ms) {
558
566
  if (h < 48) return `${h}h`;
559
567
  return `${Math.round(h / 24)}d`;
560
568
  }
569
+ async function expandNode(client2, input) {
570
+ const path = projectPath(
571
+ input.project,
572
+ `/graph/expand/${encodeURIComponent(input.nodeId)}?direction=${input.direction}`
573
+ );
574
+ return withMissingNodeFallback(async () => {
575
+ const result = await client2.get(path);
576
+ const dirWord = input.direction === "up" ? "callers/dependents (up)" : "callees/dependencies (down)";
577
+ const summary = `${result.node.id} is ${result.node.classification}. ${result.neighbours.length} ${dirWord}.`;
578
+ const blockLines = result.neighbours.map(
579
+ (n) => ` \u2022 ${n.node} \u2014 ${n.classification} via ${n.edgeType} (${n.provenance})`
580
+ );
581
+ return formatToolResponse({
582
+ summary,
583
+ block: blockLines.length ? blockLines.join("\n") : "(no runtime neighbours in this direction)"
584
+ });
585
+ }, `Node ${input.nodeId} not found in the graph.`);
586
+ }
587
+ async function relate(client2, input) {
588
+ const qs = input.maxDepth !== void 0 ? `&maxDepth=${input.maxDepth}` : "";
589
+ const path = projectPath(
590
+ input.project,
591
+ `/graph/relate?a=${encodeURIComponent(input.a)}&b=${encodeURIComponent(input.b)}${qs}`
592
+ );
593
+ try {
594
+ const result = await client2.get(path);
595
+ if (!result.related) {
596
+ return formatEmptyResponse(
597
+ `${input.a} and ${input.b} are not related \u2014 ${result.note ?? "no path found"}.`
598
+ );
599
+ }
600
+ const arrow = result.direction === "a->b" ? `${input.a} \u2192 ${input.b}` : `${input.b} \u2192 ${input.a}`;
601
+ const carries = result.paths[0]?.carriesSignal ? "and the path carries the failure end to end" : "but the path carries no failure signal";
602
+ const gap = result.grainGap ? " (grain gap \u2014 only a coarser link is in evidence)" : "";
603
+ const summary = `${arrow}: a path exists ${carries}${gap}.`;
604
+ const blockLines = result.paths.map(
605
+ (p) => ` ${p.nodes.join(" \u2192 ")} [${p.edgeTypes.join(", ")}] carriesSignal=${p.carriesSignal}`
606
+ );
607
+ return formatToolResponse({ summary, block: blockLines.join("\n") });
608
+ } catch (err) {
609
+ if (err instanceof ProjectNotFoundError) return formatErrorResponse(err.message);
610
+ return formatErrorResponse(`Error talking to neat-core: ${err.message}`);
611
+ }
612
+ }
561
613
  async function getIncidentHistory(client2, input) {
562
614
  return withMissingNodeFallback(async () => {
563
615
  const body = await client2.get(
@@ -1081,6 +1133,27 @@ registerTool(
1081
1133
  },
1082
1134
  async (input) => getObservedDependencies(client, { ...input, project: projectFor(input) })
1083
1135
  );
1136
+ registerTool(
1137
+ "expand",
1138
+ '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.',
1139
+ {
1140
+ nodeId: z.string().describe("Graph node id to step from"),
1141
+ direction: z.enum(["up", "down"]).describe("up = callers/dependents (toward the cause), down = callees/dependencies"),
1142
+ project: projectField
1143
+ },
1144
+ async (input) => expandNode(client, { ...input, project: projectFor(input) })
1145
+ );
1146
+ registerTool(
1147
+ "relate",
1148
+ '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".',
1149
+ {
1150
+ a: z.string().describe("First node id (the hypothesised cause)"),
1151
+ b: z.string().describe("Second node id (the hypothesised symptom)"),
1152
+ maxDepth: z.number().int().min(1).max(10).optional().describe("Max path length to search (default 5)"),
1153
+ project: projectField
1154
+ },
1155
+ async (input) => relate(client, { ...input, project: projectFor(input) })
1156
+ );
1084
1157
  registerTool(
1085
1158
  "get_incident_history",
1086
1159
  "Return recent OTel error events recorded against a node, most recent first.",