@neat.is/mcp 0.4.20-dev.20260629 → 0.4.22
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 +56 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +56 -12
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -374,7 +374,7 @@ async function getBlastRadius(client2, input) {
|
|
|
374
374
|
const result = await client2.get(path);
|
|
375
375
|
if (result.totalAffected === 0) {
|
|
376
376
|
return formatEmptyResponse(
|
|
377
|
-
`${result.origin} has no
|
|
377
|
+
`${result.origin} has no dependents. Nothing else would break if it failed.`
|
|
378
378
|
);
|
|
379
379
|
}
|
|
380
380
|
const sorted = [...result.affectedNodes].sort(
|
|
@@ -387,7 +387,7 @@ async function getBlastRadius(client2, input) {
|
|
|
387
387
|
);
|
|
388
388
|
const provenances = [...new Set(sorted.map((n) => n.edgeProvenance))];
|
|
389
389
|
return formatToolResponse({
|
|
390
|
-
summary: `Blast radius for ${result.origin}: ${result.totalAffected}
|
|
390
|
+
summary: `Blast radius for ${result.origin}: ${result.totalAffected} dependent node${result.totalAffected === 1 ? "" : "s"} would break if it changed.`,
|
|
391
391
|
block: blockLines.join("\n"),
|
|
392
392
|
confidence: Number.isFinite(minConfidence) ? minConfidence : void 0,
|
|
393
393
|
provenance: provenances.length ? provenances : void 0
|
|
@@ -435,20 +435,31 @@ async function getDependencies(client2, input) {
|
|
|
435
435
|
});
|
|
436
436
|
}, `Node ${input.nodeId} not found in the graph.`);
|
|
437
437
|
}
|
|
438
|
+
function observedDepLine(nodeId, e) {
|
|
439
|
+
const via = e.source !== nodeId ? ` (via ${e.source})` : "";
|
|
440
|
+
return ` \u2022 ${e.target} \u2014 ${e.type}${via}${edgeMeta(e)}`;
|
|
441
|
+
}
|
|
438
442
|
async function getObservedDependencies(client2, input) {
|
|
439
443
|
return withMissingNodeFallback(async () => {
|
|
440
|
-
const
|
|
441
|
-
projectPath(
|
|
444
|
+
const result = await client2.get(
|
|
445
|
+
projectPath(
|
|
446
|
+
input.project,
|
|
447
|
+
`/graph/observed-dependencies/${encodeURIComponent(input.nodeId)}`
|
|
448
|
+
)
|
|
442
449
|
);
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
450
|
+
if (result.dependencies.length === 0) {
|
|
451
|
+
if (result.observed) {
|
|
452
|
+
return formatToolResponse({
|
|
453
|
+
summary: `${input.nodeId} makes no outbound runtime calls, but OTel has observed it receiving traffic on ${result.inboundObservedCount} inbound call path${result.inboundObservedCount === 1 ? "" : "s"} \u2014 it's a pure receiver.`,
|
|
454
|
+
provenance: Provenance.OBSERVED
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
const note = result.hasExtractedOutbound ? " Static (EXTRACTED) dependencies exist but no runtime traffic has been seen \u2014 is OTel running?" : "";
|
|
447
458
|
return formatEmptyResponse(`No OBSERVED dependencies for ${input.nodeId}.${note}`);
|
|
448
459
|
}
|
|
449
|
-
const blockLines =
|
|
460
|
+
const blockLines = result.dependencies.map((e) => observedDepLine(input.nodeId, e));
|
|
450
461
|
return formatToolResponse({
|
|
451
|
-
summary: `${input.nodeId} has ${
|
|
462
|
+
summary: `${input.nodeId} has ${result.dependencies.length} runtime dependenc${result.dependencies.length === 1 ? "y" : "ies"} confirmed by OTel.`,
|
|
452
463
|
block: blockLines.join("\n"),
|
|
453
464
|
provenance: Provenance.OBSERVED
|
|
454
465
|
});
|
|
@@ -631,6 +642,15 @@ async function getRecentStaleEdges(client2, input) {
|
|
|
631
642
|
}
|
|
632
643
|
async function checkPolicies(client2, input) {
|
|
633
644
|
try {
|
|
645
|
+
if (input.applicableTo) {
|
|
646
|
+
const body = await client2.get(
|
|
647
|
+
projectPath(
|
|
648
|
+
input.project,
|
|
649
|
+
`/policies/applicable?node=${encodeURIComponent(input.applicableTo)}`
|
|
650
|
+
)
|
|
651
|
+
);
|
|
652
|
+
return formatApplicablePolicies(body);
|
|
653
|
+
}
|
|
634
654
|
let violations;
|
|
635
655
|
let allowed = true;
|
|
636
656
|
let hypothetical;
|
|
@@ -696,6 +716,27 @@ async function checkPolicies(client2, input) {
|
|
|
696
716
|
return formatErrorResponse(`Error talking to neat-core: ${err.message}`);
|
|
697
717
|
}
|
|
698
718
|
}
|
|
719
|
+
function formatApplicablePolicies(body) {
|
|
720
|
+
const { node, applicable } = body;
|
|
721
|
+
if (applicable.length === 0) {
|
|
722
|
+
return formatEmptyResponse(
|
|
723
|
+
`No policies apply to ${node}. Nothing to keep inside the lines here \u2014 and note this is advisory: NEAT surfaces policies for awareness, it never blocks your edit.`
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
const summary = `APPLICABLE POLICIES \u2014 ${applicable.length} ${applicable.length === 1 ? "policy applies" : "policies apply"} where you're working (${node}). Keep these in mind as you edit. They inform; they do not block. Nothing here gates or stops your change.`;
|
|
727
|
+
const lines = applicable.map((p) => {
|
|
728
|
+
const tail = p.match === "region" ? " [nearby]" : "";
|
|
729
|
+
return ` \u2022 [${p.severity}/${p.onViolation}] ${p.policyName}${tail}: ${p.reason}`;
|
|
730
|
+
});
|
|
731
|
+
return formatToolResponse({
|
|
732
|
+
summary,
|
|
733
|
+
block: lines.join("\n"),
|
|
734
|
+
// The policies themselves are declared in policy.json — EXTRACTED, fully
|
|
735
|
+
// known; the confidence is in the rule's existence, not a guess.
|
|
736
|
+
confidence: 1,
|
|
737
|
+
provenance: "EXTRACTED (policy.json)"
|
|
738
|
+
});
|
|
739
|
+
}
|
|
699
740
|
function buildDivergencesPath(input) {
|
|
700
741
|
const params = new URLSearchParams();
|
|
701
742
|
if (input.type && input.type.length > 0) params.set("type", input.type.join(","));
|
|
@@ -934,7 +975,7 @@ registerTool(
|
|
|
934
975
|
);
|
|
935
976
|
registerTool(
|
|
936
977
|
"get_blast_radius",
|
|
937
|
-
"List every node
|
|
978
|
+
"List every node that depends on the given node \u2014 what would break if this node failed or was redeployed.",
|
|
938
979
|
{
|
|
939
980
|
nodeId: z.string().describe("Graph node id to compute blast radius from"),
|
|
940
981
|
depth: z.number().int().nonnegative().max(20).optional().describe("Max BFS depth (default 10)"),
|
|
@@ -1016,7 +1057,7 @@ registerTool(
|
|
|
1016
1057
|
);
|
|
1017
1058
|
registerTool(
|
|
1018
1059
|
"check_policies",
|
|
1019
|
-
"Inspect
|
|
1060
|
+
"Inspect, dry-run, or get the soft guardrail for the project's policy.json. With applicableTo, returns the policies that apply where you are working \u2014 surfaced as context so you stay inside the lines (informs, never blocks). Without hypotheticalAction or applicableTo, returns currently-recorded violations. With hypotheticalAction, returns violations that would result if the action were applied. Architectural assertions in five shapes (structural / compatibility / provenance / ownership / blast-radius).",
|
|
1020
1061
|
{
|
|
1021
1062
|
scope: CheckPoliciesScopeSchema.optional().describe(
|
|
1022
1063
|
'Narrow to a subset. Default "all".'
|
|
@@ -1024,6 +1065,9 @@ registerTool(
|
|
|
1024
1065
|
hypotheticalAction: HypotheticalActionSchema.optional().describe(
|
|
1025
1066
|
"Dry-run mode: simulate the action and return resulting violations. Omit for current state."
|
|
1026
1067
|
),
|
|
1068
|
+
applicableTo: z.string().optional().describe(
|
|
1069
|
+
"Soft guardrail (ADR-108): pass the node id you are about to edit and check_policies returns the policies that govern it, as a context block \u2014 so you stay inside the lines. Advisory only; never blocks."
|
|
1070
|
+
),
|
|
1027
1071
|
project: projectField
|
|
1028
1072
|
},
|
|
1029
1073
|
async (input) => checkPolicies(client, {
|