@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 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 downstream dependencies. Nothing else would break if it failed.`
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} affected node${result.totalAffected === 1 ? "" : "s"} reachable downstream.`,
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 edges = await client2.get(
436
- projectPath(input.project, `/graph/edges/${encodeURIComponent(input.nodeId)}`)
439
+ const result = await client2.get(
440
+ projectPath(
441
+ input.project,
442
+ `/graph/observed-dependencies/${encodeURIComponent(input.nodeId)}`
443
+ )
437
444
  );
438
- const observed = edges.outbound.filter((e) => e.provenance === import_types.Provenance.OBSERVED);
439
- if (observed.length === 0) {
440
- const hasExtracted = edges.outbound.some((e) => e.provenance === import_types.Provenance.EXTRACTED);
441
- const note = hasExtracted ? " Static (EXTRACTED) dependencies exist but no runtime traffic has been seen \u2014 is OTel running?" : "";
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 = observed.map((e) => ` \u2022 ${e.target} \u2014 ${e.type}${edgeMeta(e)}`);
455
+ const blockLines = result.dependencies.map((e) => observedDepLine(input.nodeId, e));
445
456
  return formatToolResponse({
446
- summary: `${input.nodeId} has ${observed.length} runtime dependenc${observed.length === 1 ? "y" : "ies"} confirmed by OTel.`,
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 downstream of the given node \u2014 what would break if this node failed or was redeployed.",
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 or dry-run the project's policy.json. Without hypotheticalAction, 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).",
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, {