@neat.is/mcp 0.9.7-dev.20260827 → 0.9.8-dev.20260828

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
@@ -445,7 +445,29 @@ function projectPath(project, suffix) {
445
445
  if (!project) return suffix;
446
446
  return `/projects/${encodeURIComponent(project)}${suffix}`;
447
447
  }
448
- async function withMissingNodeFallback(fn, notFoundMessage) {
448
+ async function readinessReason(client2, project) {
449
+ let health;
450
+ try {
451
+ const named = project ?? "default";
452
+ health = await client2.get(projectPath(named, "/health"));
453
+ } catch {
454
+ return void 0;
455
+ }
456
+ const coverage = health.coverage;
457
+ if (!coverage || typeof coverage.skippedFiles !== "number") {
458
+ return "(index still building \u2014 the first extraction pass has not finished yet, so this may be incomplete; retry once it settles.)";
459
+ }
460
+ if (coverage.skippedFiles > 0) {
461
+ const n = coverage.skippedFiles;
462
+ return `(index partial \u2014 ${n} file${n === 1 ? "" : "s"} failed to parse in the last pass, so this may be incomplete.)`;
463
+ }
464
+ return "(graph is current \u2014 this is a real empty result, not a still-building index.)";
465
+ }
466
+ async function emptyWithReadiness(client2, project, summary) {
467
+ const reason = await readinessReason(client2, project);
468
+ return reason ? formatToolResponse({ summary: `${summary} ${reason}` }) : formatEmptyResponse(summary);
469
+ }
470
+ async function withMissingNodeFallback(client2, project, fn, notFoundMessage) {
449
471
  try {
450
472
  return await fn();
451
473
  } catch (err) {
@@ -453,7 +475,7 @@ async function withMissingNodeFallback(fn, notFoundMessage) {
453
475
  return formatErrorResponse(err.message);
454
476
  }
455
477
  if (err instanceof HttpError && err.status === 404) {
456
- return formatEmptyResponse(notFoundMessage);
478
+ return emptyWithReadiness(client2, project, notFoundMessage);
457
479
  }
458
480
  return formatErrorResponse(`Error talking to neat-core: ${err.message}`);
459
481
  }
@@ -464,7 +486,7 @@ async function getRootCause(client2, input) {
464
486
  input.project,
465
487
  `/graph/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
466
488
  );
467
- return withMissingNodeFallback(async () => {
489
+ return withMissingNodeFallback(client2, input.project, async () => {
468
490
  const result = await client2.get(path);
469
491
  const arrowPath = result.traversalPath.join(" \u2190 ");
470
492
  const provenances = result.edgeProvenances.length ? result.edgeProvenances.join(", ") : "(direct, no edges traversed)";
@@ -498,10 +520,12 @@ async function getBlastRadius(client2, input) {
498
520
  input.project,
499
521
  `/graph/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
500
522
  );
501
- return withMissingNodeFallback(async () => {
523
+ return withMissingNodeFallback(client2, input.project, async () => {
502
524
  const result = await client2.get(path);
503
525
  if (result.totalAffected === 0) {
504
- return formatEmptyResponse(
526
+ return emptyWithReadiness(
527
+ client2,
528
+ input.project,
505
529
  `${result.origin} has no dependents. Nothing else would break if it failed.`
506
530
  );
507
531
  }
@@ -532,10 +556,12 @@ async function getDependencies(client2, input) {
532
556
  input.project,
533
557
  `/graph/dependencies/${encodeURIComponent(input.nodeId)}?depth=${depth}`
534
558
  );
535
- return withMissingNodeFallback(async () => {
559
+ return withMissingNodeFallback(client2, input.project, async () => {
536
560
  const result = await client2.get(path);
537
561
  if (result.total === 0) {
538
- return formatEmptyResponse(
562
+ return emptyWithReadiness(
563
+ client2,
564
+ input.project,
539
565
  depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
540
566
  );
541
567
  }
@@ -568,7 +594,7 @@ function observedDepLine(nodeId, e) {
568
594
  return ` \u2022 ${e.target} \u2014 ${e.type}${via}${edgeMeta(e)}`;
569
595
  }
570
596
  async function getObservedDependencies(client2, input) {
571
- return withMissingNodeFallback(async () => {
597
+ return withMissingNodeFallback(client2, input.project, async () => {
572
598
  const result = await client2.get(
573
599
  projectPath(
574
600
  input.project,
@@ -623,7 +649,7 @@ async function expandNode(client2, input) {
623
649
  input.project,
624
650
  `/graph/expand/${encodeURIComponent(input.nodeId)}?direction=${input.direction}`
625
651
  );
626
- return withMissingNodeFallback(async () => {
652
+ return withMissingNodeFallback(client2, input.project, async () => {
627
653
  const result = await client2.get(path);
628
654
  const dirWord = input.direction === "up" ? "callers/dependents (up)" : "callees/dependencies (down)";
629
655
  const summary = `${result.node.id} is ${result.node.classification}. ${result.neighbours.length} ${dirWord}.`;
@@ -665,7 +691,7 @@ async function relate(client2, input) {
665
691
  async function ask(client2, input) {
666
692
  const question = input.question.trim();
667
693
  if (!question) return formatEmptyResponse("ask: the question was empty.");
668
- return withMissingNodeFallback(async () => {
694
+ return withMissingNodeFallback(client2, input.project, async () => {
669
695
  const result = await client2.get(
670
696
  projectPath(input.project, `/graph/ask?q=${encodeURIComponent(question)}`)
671
697
  );
@@ -693,7 +719,7 @@ async function ask(client2, input) {
693
719
  }, `ask: nothing in "${question}" resolved to a node in the graph.`);
694
720
  }
695
721
  async function getIncidentHistory(client2, input) {
696
- return withMissingNodeFallback(async () => {
722
+ return withMissingNodeFallback(client2, input.project, async () => {
697
723
  const body = await client2.get(
698
724
  projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`)
699
725
  );
@@ -968,13 +994,23 @@ function formatDivergenceLine(d) {
968
994
  const member = d.symbol ? ` ${d.symbol}` : "";
969
995
  return ` \u2022 [${d.type}] ${d.source}${member}${at} (${d.mismatchKind}) \u2014 confidence ${d.confidence.toFixed(2)}`;
970
996
  }
997
+ case "observed-failing": {
998
+ if (d.edgeType) {
999
+ const rate = d.errorRate !== void 0 ? ` ${Math.round(d.errorRate * 100)}% of observed calls fail` : "";
1000
+ return ` \u2022 [${d.type}] ${d.source} \u2192 ${d.target} (${d.edgeType}) \u2014${rate} (${d.failureKind}) \u2014 confidence ${d.confidence.toFixed(2)}`;
1001
+ }
1002
+ const at = d.location ? ` at ${d.location}` : "";
1003
+ return ` \u2022 [${d.type}] ${d.source}${at} (${d.failureKind}) \u2014 confidence ${d.confidence.toFixed(2)}`;
1004
+ }
971
1005
  }
972
1006
  }
973
1007
  async function getDivergences(client2, input) {
974
1008
  try {
975
1009
  const result = await client2.get(buildDivergencesPath(input));
976
1010
  if (result.totalAffected === 0) {
977
- return formatEmptyResponse(
1011
+ return emptyWithReadiness(
1012
+ client2,
1013
+ input.project,
978
1014
  "No divergences found between the declared (EXTRACTED) and observed (OBSERVED) views of the graph."
979
1015
  );
980
1016
  }
@@ -1297,7 +1333,7 @@ registerTool(
1297
1333
  "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.",
1298
1334
  {
1299
1335
  type: import_zod.z.array(import_types2.DivergenceTypeSchema).optional().describe(
1300
- "Filter by divergence type. One or more of: missing-observed, missing-extracted, version-mismatch, host-mismatch, compat-violation, observed-symbol-mismatch. Omit for all."
1336
+ "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."
1301
1337
  ),
1302
1338
  minConfidence: import_zod.z.number().min(0).max(1).optional().describe("Drop divergences below this confidence threshold (0.0 - 1.0)."),
1303
1339
  node: import_zod.z.string().optional().describe("Scope to divergences involving this node id (as source or target)."),