@neat.is/mcp 0.9.6 → 0.9.7

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
@@ -450,7 +450,29 @@ function projectPath(project, suffix) {
450
450
  if (!project) return suffix;
451
451
  return `/projects/${encodeURIComponent(project)}${suffix}`;
452
452
  }
453
- async function withMissingNodeFallback(fn, notFoundMessage) {
453
+ async function readinessReason(client2, project) {
454
+ let health;
455
+ try {
456
+ const named = project ?? "default";
457
+ health = await client2.get(projectPath(named, "/health"));
458
+ } catch {
459
+ return void 0;
460
+ }
461
+ const coverage = health.coverage;
462
+ if (!coverage || typeof coverage.skippedFiles !== "number") {
463
+ return "(index still building \u2014 the first extraction pass has not finished yet, so this may be incomplete; retry once it settles.)";
464
+ }
465
+ if (coverage.skippedFiles > 0) {
466
+ const n = coverage.skippedFiles;
467
+ return `(index partial \u2014 ${n} file${n === 1 ? "" : "s"} failed to parse in the last pass, so this may be incomplete.)`;
468
+ }
469
+ return "(graph is current \u2014 this is a real empty result, not a still-building index.)";
470
+ }
471
+ async function emptyWithReadiness(client2, project, summary) {
472
+ const reason = await readinessReason(client2, project);
473
+ return reason ? formatToolResponse({ summary: `${summary} ${reason}` }) : formatEmptyResponse(summary);
474
+ }
475
+ async function withMissingNodeFallback(client2, project, fn, notFoundMessage) {
454
476
  try {
455
477
  return await fn();
456
478
  } catch (err) {
@@ -458,7 +480,7 @@ async function withMissingNodeFallback(fn, notFoundMessage) {
458
480
  return formatErrorResponse(err.message);
459
481
  }
460
482
  if (err instanceof HttpError && err.status === 404) {
461
- return formatEmptyResponse(notFoundMessage);
483
+ return emptyWithReadiness(client2, project, notFoundMessage);
462
484
  }
463
485
  return formatErrorResponse(`Error talking to neat-core: ${err.message}`);
464
486
  }
@@ -469,7 +491,7 @@ async function getRootCause(client2, input) {
469
491
  input.project,
470
492
  `/graph/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
471
493
  );
472
- return withMissingNodeFallback(async () => {
494
+ return withMissingNodeFallback(client2, input.project, async () => {
473
495
  const result = await client2.get(path);
474
496
  const arrowPath = result.traversalPath.join(" \u2190 ");
475
497
  const provenances = result.edgeProvenances.length ? result.edgeProvenances.join(", ") : "(direct, no edges traversed)";
@@ -503,10 +525,12 @@ async function getBlastRadius(client2, input) {
503
525
  input.project,
504
526
  `/graph/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
505
527
  );
506
- return withMissingNodeFallback(async () => {
528
+ return withMissingNodeFallback(client2, input.project, async () => {
507
529
  const result = await client2.get(path);
508
530
  if (result.totalAffected === 0) {
509
- return formatEmptyResponse(
531
+ return emptyWithReadiness(
532
+ client2,
533
+ input.project,
510
534
  `${result.origin} has no dependents. Nothing else would break if it failed.`
511
535
  );
512
536
  }
@@ -537,10 +561,12 @@ async function getDependencies(client2, input) {
537
561
  input.project,
538
562
  `/graph/dependencies/${encodeURIComponent(input.nodeId)}?depth=${depth}`
539
563
  );
540
- return withMissingNodeFallback(async () => {
564
+ return withMissingNodeFallback(client2, input.project, async () => {
541
565
  const result = await client2.get(path);
542
566
  if (result.total === 0) {
543
- return formatEmptyResponse(
567
+ return emptyWithReadiness(
568
+ client2,
569
+ input.project,
544
570
  depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
545
571
  );
546
572
  }
@@ -573,7 +599,7 @@ function observedDepLine(nodeId, e) {
573
599
  return ` \u2022 ${e.target} \u2014 ${e.type}${via}${edgeMeta(e)}`;
574
600
  }
575
601
  async function getObservedDependencies(client2, input) {
576
- return withMissingNodeFallback(async () => {
602
+ return withMissingNodeFallback(client2, input.project, async () => {
577
603
  const result = await client2.get(
578
604
  projectPath(
579
605
  input.project,
@@ -628,7 +654,7 @@ async function expandNode(client2, input) {
628
654
  input.project,
629
655
  `/graph/expand/${encodeURIComponent(input.nodeId)}?direction=${input.direction}`
630
656
  );
631
- return withMissingNodeFallback(async () => {
657
+ return withMissingNodeFallback(client2, input.project, async () => {
632
658
  const result = await client2.get(path);
633
659
  const dirWord = input.direction === "up" ? "callers/dependents (up)" : "callees/dependencies (down)";
634
660
  const summary = `${result.node.id} is ${result.node.classification}. ${result.neighbours.length} ${dirWord}.`;
@@ -670,7 +696,7 @@ async function relate(client2, input) {
670
696
  async function ask(client2, input) {
671
697
  const question = input.question.trim();
672
698
  if (!question) return formatEmptyResponse("ask: the question was empty.");
673
- return withMissingNodeFallback(async () => {
699
+ return withMissingNodeFallback(client2, input.project, async () => {
674
700
  const result = await client2.get(
675
701
  projectPath(input.project, `/graph/ask?q=${encodeURIComponent(question)}`)
676
702
  );
@@ -698,7 +724,7 @@ async function ask(client2, input) {
698
724
  }, `ask: nothing in "${question}" resolved to a node in the graph.`);
699
725
  }
700
726
  async function getIncidentHistory(client2, input) {
701
- return withMissingNodeFallback(async () => {
727
+ return withMissingNodeFallback(client2, input.project, async () => {
702
728
  const body = await client2.get(
703
729
  projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`)
704
730
  );
@@ -973,13 +999,23 @@ function formatDivergenceLine(d) {
973
999
  const member = d.symbol ? ` ${d.symbol}` : "";
974
1000
  return ` \u2022 [${d.type}] ${d.source}${member}${at} (${d.mismatchKind}) \u2014 confidence ${d.confidence.toFixed(2)}`;
975
1001
  }
1002
+ case "observed-failing": {
1003
+ if (d.edgeType) {
1004
+ const rate = d.errorRate !== void 0 ? ` ${Math.round(d.errorRate * 100)}% of observed calls fail` : "";
1005
+ return ` \u2022 [${d.type}] ${d.source} \u2192 ${d.target} (${d.edgeType}) \u2014${rate} (${d.failureKind}) \u2014 confidence ${d.confidence.toFixed(2)}`;
1006
+ }
1007
+ const at = d.location ? ` at ${d.location}` : "";
1008
+ return ` \u2022 [${d.type}] ${d.source}${at} (${d.failureKind}) \u2014 confidence ${d.confidence.toFixed(2)}`;
1009
+ }
976
1010
  }
977
1011
  }
978
1012
  async function getDivergences(client2, input) {
979
1013
  try {
980
1014
  const result = await client2.get(buildDivergencesPath(input));
981
1015
  if (result.totalAffected === 0) {
982
- return formatEmptyResponse(
1016
+ return emptyWithReadiness(
1017
+ client2,
1018
+ input.project,
983
1019
  "No divergences found between the declared (EXTRACTED) and observed (OBSERVED) views of the graph."
984
1020
  );
985
1021
  }
@@ -1302,7 +1338,7 @@ registerTool(
1302
1338
  "Returns places where what the code declares (EXTRACTED) doesn't match what production observed (OBSERVED). The single most NEAT-shaped query \u2014 the one that justifies the whole graph. Use when the user asks 'is anything weird?' or 'what does production do that the code doesn't?' or 'find me a bug' on an unfamiliar codebase. Returns divergences ranked by confidence \xD7 severity. Prefer this over `get_root_cause` when no specific node is failing.",
1303
1339
  {
1304
1340
  type: z.array(DivergenceTypeSchema).optional().describe(
1305
- "Filter by divergence type. One or more of: missing-observed, missing-extracted, version-mismatch, host-mismatch, compat-violation, observed-symbol-mismatch. Omit for all."
1341
+ "Filter by divergence type. One or more of: missing-observed, missing-extracted, version-mismatch, host-mismatch, compat-violation, observed-symbol-mismatch, observed-failing. Omit for all."
1306
1342
  ),
1307
1343
  minConfidence: z.number().min(0).max(1).optional().describe("Drop divergences below this confidence threshold (0.0 - 1.0)."),
1308
1344
  node: z.string().optional().describe("Scope to divergences involving this node id (as source or target)."),