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