@neat.is/mcp 0.4.20-dev.20260630 → 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.cjs
CHANGED
|
@@ -369,7 +369,7 @@ async function getBlastRadius(client2, input) {
|
|
|
369
369
|
const result = await client2.get(path);
|
|
370
370
|
if (result.totalAffected === 0) {
|
|
371
371
|
return formatEmptyResponse(
|
|
372
|
-
`${result.origin} has no
|
|
372
|
+
`${result.origin} has no dependents. Nothing else would break if it failed.`
|
|
373
373
|
);
|
|
374
374
|
}
|
|
375
375
|
const sorted = [...result.affectedNodes].sort(
|
|
@@ -382,7 +382,7 @@ async function getBlastRadius(client2, input) {
|
|
|
382
382
|
);
|
|
383
383
|
const provenances = [...new Set(sorted.map((n) => n.edgeProvenance))];
|
|
384
384
|
return formatToolResponse({
|
|
385
|
-
summary: `Blast radius for ${result.origin}: ${result.totalAffected}
|
|
385
|
+
summary: `Blast radius for ${result.origin}: ${result.totalAffected} dependent node${result.totalAffected === 1 ? "" : "s"} would break if it changed.`,
|
|
386
386
|
block: blockLines.join("\n"),
|
|
387
387
|
confidence: Number.isFinite(minConfidence) ? minConfidence : void 0,
|
|
388
388
|
provenance: provenances.length ? provenances : void 0
|
|
@@ -430,20 +430,31 @@ async function getDependencies(client2, input) {
|
|
|
430
430
|
});
|
|
431
431
|
}, `Node ${input.nodeId} not found in the graph.`);
|
|
432
432
|
}
|
|
433
|
+
function observedDepLine(nodeId, e) {
|
|
434
|
+
const via = e.source !== nodeId ? ` (via ${e.source})` : "";
|
|
435
|
+
return ` \u2022 ${e.target} \u2014 ${e.type}${via}${edgeMeta(e)}`;
|
|
436
|
+
}
|
|
433
437
|
async function getObservedDependencies(client2, input) {
|
|
434
438
|
return withMissingNodeFallback(async () => {
|
|
435
|
-
const
|
|
436
|
-
projectPath(
|
|
439
|
+
const result = await client2.get(
|
|
440
|
+
projectPath(
|
|
441
|
+
input.project,
|
|
442
|
+
`/graph/observed-dependencies/${encodeURIComponent(input.nodeId)}`
|
|
443
|
+
)
|
|
437
444
|
);
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
445
|
+
if (result.dependencies.length === 0) {
|
|
446
|
+
if (result.observed) {
|
|
447
|
+
return formatToolResponse({
|
|
448
|
+
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.`,
|
|
449
|
+
provenance: import_types.Provenance.OBSERVED
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
const note = result.hasExtractedOutbound ? " Static (EXTRACTED) dependencies exist but no runtime traffic has been seen \u2014 is OTel running?" : "";
|
|
442
453
|
return formatEmptyResponse(`No OBSERVED dependencies for ${input.nodeId}.${note}`);
|
|
443
454
|
}
|
|
444
|
-
const blockLines =
|
|
455
|
+
const blockLines = result.dependencies.map((e) => observedDepLine(input.nodeId, e));
|
|
445
456
|
return formatToolResponse({
|
|
446
|
-
summary: `${input.nodeId} has ${
|
|
457
|
+
summary: `${input.nodeId} has ${result.dependencies.length} runtime dependenc${result.dependencies.length === 1 ? "y" : "ies"} confirmed by OTel.`,
|
|
447
458
|
block: blockLines.join("\n"),
|
|
448
459
|
provenance: import_types.Provenance.OBSERVED
|
|
449
460
|
});
|
|
@@ -626,6 +637,15 @@ async function getRecentStaleEdges(client2, input) {
|
|
|
626
637
|
}
|
|
627
638
|
async function checkPolicies(client2, input) {
|
|
628
639
|
try {
|
|
640
|
+
if (input.applicableTo) {
|
|
641
|
+
const body = await client2.get(
|
|
642
|
+
projectPath(
|
|
643
|
+
input.project,
|
|
644
|
+
`/policies/applicable?node=${encodeURIComponent(input.applicableTo)}`
|
|
645
|
+
)
|
|
646
|
+
);
|
|
647
|
+
return formatApplicablePolicies(body);
|
|
648
|
+
}
|
|
629
649
|
let violations;
|
|
630
650
|
let allowed = true;
|
|
631
651
|
let hypothetical;
|
|
@@ -691,6 +711,27 @@ async function checkPolicies(client2, input) {
|
|
|
691
711
|
return formatErrorResponse(`Error talking to neat-core: ${err.message}`);
|
|
692
712
|
}
|
|
693
713
|
}
|
|
714
|
+
function formatApplicablePolicies(body) {
|
|
715
|
+
const { node, applicable } = body;
|
|
716
|
+
if (applicable.length === 0) {
|
|
717
|
+
return formatEmptyResponse(
|
|
718
|
+
`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.`
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
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.`;
|
|
722
|
+
const lines = applicable.map((p) => {
|
|
723
|
+
const tail = p.match === "region" ? " [nearby]" : "";
|
|
724
|
+
return ` \u2022 [${p.severity}/${p.onViolation}] ${p.policyName}${tail}: ${p.reason}`;
|
|
725
|
+
});
|
|
726
|
+
return formatToolResponse({
|
|
727
|
+
summary,
|
|
728
|
+
block: lines.join("\n"),
|
|
729
|
+
// The policies themselves are declared in policy.json — EXTRACTED, fully
|
|
730
|
+
// known; the confidence is in the rule's existence, not a guess.
|
|
731
|
+
confidence: 1,
|
|
732
|
+
provenance: "EXTRACTED (policy.json)"
|
|
733
|
+
});
|
|
734
|
+
}
|
|
694
735
|
function buildDivergencesPath(input) {
|
|
695
736
|
const params = new URLSearchParams();
|
|
696
737
|
if (input.type && input.type.length > 0) params.set("type", input.type.join(","));
|
|
@@ -929,7 +970,7 @@ registerTool(
|
|
|
929
970
|
);
|
|
930
971
|
registerTool(
|
|
931
972
|
"get_blast_radius",
|
|
932
|
-
"List every node
|
|
973
|
+
"List every node that depends on the given node \u2014 what would break if this node failed or was redeployed.",
|
|
933
974
|
{
|
|
934
975
|
nodeId: import_zod.z.string().describe("Graph node id to compute blast radius from"),
|
|
935
976
|
depth: import_zod.z.number().int().nonnegative().max(20).optional().describe("Max BFS depth (default 10)"),
|
|
@@ -1011,7 +1052,7 @@ registerTool(
|
|
|
1011
1052
|
);
|
|
1012
1053
|
registerTool(
|
|
1013
1054
|
"check_policies",
|
|
1014
|
-
"Inspect
|
|
1055
|
+
"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).",
|
|
1015
1056
|
{
|
|
1016
1057
|
scope: import_types2.CheckPoliciesScopeSchema.optional().describe(
|
|
1017
1058
|
'Narrow to a subset. Default "all".'
|
|
@@ -1019,6 +1060,9 @@ registerTool(
|
|
|
1019
1060
|
hypotheticalAction: import_types2.HypotheticalActionSchema.optional().describe(
|
|
1020
1061
|
"Dry-run mode: simulate the action and return resulting violations. Omit for current state."
|
|
1021
1062
|
),
|
|
1063
|
+
applicableTo: import_zod.z.string().optional().describe(
|
|
1064
|
+
"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."
|
|
1065
|
+
),
|
|
1022
1066
|
project: projectField
|
|
1023
1067
|
},
|
|
1024
1068
|
async (input) => checkPolicies(client, {
|