@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.cjs +49 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +49 -13
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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)."),
|