@neat.is/mcp 0.4.10 → 0.4.12

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
@@ -249,9 +249,6 @@ function registerResources(server2, client2, options = {}) {
249
249
  };
250
250
  }
251
251
 
252
- // src/index.ts
253
- var import_types3 = require("@neat.is/types");
254
-
255
252
  // src/tools.ts
256
253
  var import_types = require("@neat.is/types");
257
254
 
@@ -716,6 +713,154 @@ async function postJson(client2, path, body) {
716
713
  }
717
714
  return c.post(path, body);
718
715
  }
716
+ async function neatListUninstrumented(client2, input) {
717
+ try {
718
+ const result = await client2.get(
719
+ projectPath(input.project, "/extend/list-uninstrumented")
720
+ );
721
+ const libs = result.libraries;
722
+ if (libs.length === 0) {
723
+ return formatEmptyResponse(
724
+ "All detected libraries are covered by the auto-instrumentations bundle or the HTTP fallback. No extension needed."
725
+ );
726
+ }
727
+ const blockLines = libs.map((l) => {
728
+ const pkgBit = l.instrumentation_package ? ` \u2192 ${l.instrumentation_package}@${l.package_version ?? "*"}` : " \u2192 no registry entry";
729
+ return ` \u2022 ${l.library} [${l.coverage}]${pkgBit}${l.notes ? ` \u2014 ${l.notes}` : ""}`;
730
+ });
731
+ return formatToolResponse({
732
+ summary: `${libs.length} librar${libs.length === 1 ? "y needs" : "ies need"} instrumentation beyond the auto-instrumentations bundle.`,
733
+ block: blockLines.join("\n")
734
+ });
735
+ } catch (err) {
736
+ return formatErrorResponse(`Error talking to neat-core: ${err.message}`);
737
+ }
738
+ }
739
+ async function neatLookupInstrumentation(client2, input) {
740
+ const qs = input.installedVersion ? `?library=${encodeURIComponent(input.library)}&version=${encodeURIComponent(input.installedVersion)}` : `?library=${encodeURIComponent(input.library)}`;
741
+ try {
742
+ const result = await client2.get(
743
+ projectPath(input.project, `/extend/lookup${qs}`)
744
+ );
745
+ const lines = [
746
+ ` coverage: ${result.coverage}`,
747
+ ...result.instrumentation_package ? [` instrumentation_package: ${result.instrumentation_package}@${result.package_version ?? "*"}`] : [],
748
+ ...result.registration ? [` registration: ${result.registration}`] : [],
749
+ ...result.notes ? [` notes: ${result.notes}`] : []
750
+ ];
751
+ return formatToolResponse({
752
+ summary: `Registry entry for ${input.library}: coverage is ${result.coverage}.`,
753
+ block: lines.join("\n")
754
+ });
755
+ } catch (err) {
756
+ if (err instanceof HttpError && err.status === 404) {
757
+ return formatEmptyResponse(`${input.library} is not in the instrumentation registry.`);
758
+ }
759
+ return formatErrorResponse(`Error talking to neat-core: ${err.message}`);
760
+ }
761
+ }
762
+ async function neatDescribeProjectInstrumentation(client2, input) {
763
+ try {
764
+ const state = await client2.get(
765
+ projectPath(input.project, "/extend/describe")
766
+ );
767
+ const lines = [
768
+ ` hook files: ${state.hookFiles.length > 0 ? state.hookFiles.join(", ") : "(none \u2014 run neat init first)"}`,
769
+ ` .env.neat: ${state.envNeat ? "present" : "absent"}`
770
+ ];
771
+ const depEntries = Object.entries(state.installedDeps);
772
+ if (depEntries.length > 0) {
773
+ lines.push(" installed OTel deps:");
774
+ for (const [pkg, ver] of depEntries) {
775
+ lines.push(` ${pkg}@${ver}`);
776
+ }
777
+ } else {
778
+ lines.push(" installed OTel deps: (none)");
779
+ }
780
+ const ready = state.hookFiles.length > 0;
781
+ return formatToolResponse({
782
+ summary: ready ? `Project has ${state.hookFiles.length} instrumentation hook file${state.hookFiles.length === 1 ? "" : "s"} and is ready for neat_apply_extension.` : "Project has no instrumentation hook files. Run neat init before extending.",
783
+ block: lines.join("\n")
784
+ });
785
+ } catch (err) {
786
+ return formatErrorResponse(`Error talking to neat-core: ${err.message}`);
787
+ }
788
+ }
789
+ async function neatApplyExtension(client2, input) {
790
+ try {
791
+ const result = await postJson(
792
+ client2,
793
+ projectPath(input.project, "/extend/apply"),
794
+ {
795
+ library: input.library,
796
+ instrumentation_package: input.instrumentation_package,
797
+ version: input.version,
798
+ registration_snippet: input.registration_snippet
799
+ }
800
+ );
801
+ if (result.alreadyApplied) {
802
+ return formatEmptyResponse(
803
+ `${input.library} instrumentation is already applied \u2014 no changes made.`
804
+ );
805
+ }
806
+ const lines = [
807
+ ` files touched: ${result.filesTouched.join(", ") || "(none)"}`,
808
+ ` deps added: ${result.depsAdded.join(", ") || "(none)"}`,
809
+ ` install: ${result.installOutput}`
810
+ ];
811
+ return formatToolResponse({
812
+ summary: `Applied ${input.instrumentation_package} for ${input.library}. ${result.filesTouched.length} file${result.filesTouched.length === 1 ? "" : "s"} touched, logged to ~/.neat/extend-log.ndjson.`,
813
+ block: lines.join("\n")
814
+ });
815
+ } catch (err) {
816
+ return formatErrorResponse(`Error talking to neat-core: ${err.message}`);
817
+ }
818
+ }
819
+ async function neatDryRunExtension(client2, input) {
820
+ try {
821
+ const result = await postJson(
822
+ client2,
823
+ projectPath(input.project, "/extend/dry-run"),
824
+ {
825
+ library: input.library,
826
+ instrumentation_package: input.instrumentation_package,
827
+ version: input.version,
828
+ registration_snippet: input.registration_snippet
829
+ }
830
+ );
831
+ const lines = [
832
+ ` files that would be touched: ${result.filesTouched.join(", ") || "(none)"}`,
833
+ ` deps to add: ${result.depsToAdd.join(", ") || "(none)"}`,
834
+ ` hook file patch: ${result.templatePatch}`
835
+ ];
836
+ return formatToolResponse({
837
+ summary: `Dry run for ${input.library}: ${result.filesTouched.length} file${result.filesTouched.length === 1 ? "" : "s"} would be touched. No changes made.`,
838
+ block: lines.join("\n")
839
+ });
840
+ } catch (err) {
841
+ return formatErrorResponse(`Error talking to neat-core: ${err.message}`);
842
+ }
843
+ }
844
+ async function neatRollbackExtension(client2, input) {
845
+ try {
846
+ const result = await postJson(
847
+ client2,
848
+ projectPath(input.project, "/extend/rollback"),
849
+ { library: input.library }
850
+ );
851
+ if (!result.undone) {
852
+ return formatEmptyResponse(
853
+ `No prior apply found for ${input.library} \u2014 nothing to roll back.`
854
+ );
855
+ }
856
+ return formatToolResponse({
857
+ summary: `Rolled back instrumentation for ${input.library}. ${result.message}. Run your package manager install to sync the lockfile.`,
858
+ block: ` result: ${result.message}`
859
+ });
860
+ } catch (err) {
861
+ return formatErrorResponse(`Error talking to neat-core: ${err.message}`);
862
+ }
863
+ }
719
864
 
720
865
  // src/index.ts
721
866
  var baseUrl = process.env.NEAT_CORE_URL ?? "http://localhost:8080";
@@ -730,7 +875,8 @@ var server = new import_mcp2.McpServer({
730
875
  name: "neat",
731
876
  version: "0.1.0"
732
877
  });
733
- server.tool(
878
+ var registerTool = (name, description, paramsSchema, cb) => server.tool(name, description, paramsSchema, cb);
879
+ registerTool(
734
880
  "get_root_cause",
735
881
  "Trace a failing node up its dependency graph to find the underlying cause. Use this when something is breaking and you want to know which upstream component is the actual culprit.",
736
882
  {
@@ -740,7 +886,7 @@ server.tool(
740
886
  },
741
887
  async (input) => getRootCause(client, { ...input, project: projectFor(input) })
742
888
  );
743
- server.tool(
889
+ registerTool(
744
890
  "get_blast_radius",
745
891
  "List every node downstream of the given node \u2014 what would break if this node failed or was redeployed.",
746
892
  {
@@ -750,7 +896,7 @@ server.tool(
750
896
  },
751
897
  async (input) => getBlastRadius(client, { ...input, project: projectFor(input) })
752
898
  );
753
- server.tool(
899
+ registerTool(
754
900
  "get_dependencies",
755
901
  "List the transitive outgoing dependencies of a node, BFS to depth N (default 3, max 10). Each result carries distance, edge type, and provenance \u2014 both static (EXTRACTED) and runtime (OBSERVED). Pass depth=1 for direct-only.",
756
902
  {
@@ -760,7 +906,7 @@ server.tool(
760
906
  },
761
907
  async (input) => getDependencies(client, { ...input, project: projectFor(input) })
762
908
  );
763
- server.tool(
909
+ registerTool(
764
910
  "get_observed_dependencies",
765
911
  "List only the runtime (OBSERVED via OTel) outgoing dependencies of a node. Use this to compare what code SAYS the service depends on vs what production actually does.",
766
912
  {
@@ -769,7 +915,7 @@ server.tool(
769
915
  },
770
916
  async (input) => getObservedDependencies(client, { ...input, project: projectFor(input) })
771
917
  );
772
- server.tool(
918
+ registerTool(
773
919
  "get_incident_history",
774
920
  "Return recent OTel error events recorded against a node, most recent first.",
775
921
  {
@@ -779,7 +925,7 @@ server.tool(
779
925
  },
780
926
  async (input) => getIncidentHistory(client, { ...input, project: projectFor(input) })
781
927
  );
782
- server.tool(
928
+ registerTool(
783
929
  "semantic_search",
784
930
  "Search nodes by natural-language query. Uses embedding vectors when an embedder is available (Ollama nomic-embed-text \u2192 in-process MiniLM \u2192 substring fallback) \u2014 phrase the query the way you would describe what you want.",
785
931
  {
@@ -788,7 +934,7 @@ server.tool(
788
934
  },
789
935
  async (input) => semanticSearch(client, { ...input, project: projectFor(input) })
790
936
  );
791
- server.tool(
937
+ registerTool(
792
938
  "get_graph_diff",
793
939
  'Diff a saved graph snapshot against the current live graph. Useful for change reviews and post-incidents \u2014 answers "what changed in the architecture between then and now." Returns added/removed/changed nodes and edges with both snapshot timestamps.',
794
940
  {
@@ -799,7 +945,7 @@ server.tool(
799
945
  },
800
946
  async (input) => getGraphDiff(client, { ...input, project: projectFor(input) })
801
947
  );
802
- server.tool(
948
+ registerTool(
803
949
  "get_recent_stale_edges",
804
950
  "List the most recent OBSERVED \u2192 STALE edge transitions. Use this to spot integrations that have gone quiet \u2014 a CALLS edge that just went stale typically means an upstream stopped calling, not that the link is healthy.",
805
951
  {
@@ -809,11 +955,11 @@ server.tool(
809
955
  },
810
956
  async (input) => getRecentStaleEdges(client, { ...input, project: projectFor(input) })
811
957
  );
812
- server.tool(
958
+ registerTool(
813
959
  "get_divergences",
814
960
  "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.",
815
961
  {
816
- type: import_zod.z.array(import_types3.DivergenceTypeSchema).optional().describe(
962
+ type: import_zod.z.array(import_types2.DivergenceTypeSchema).optional().describe(
817
963
  "Filter by divergence type. One or more of: missing-observed, missing-extracted, version-mismatch, host-mismatch, compat-violation. Omit for all."
818
964
  ),
819
965
  minConfidence: import_zod.z.number().min(0).max(1).optional().describe("Drop divergences below this confidence threshold (0.0 - 1.0)."),
@@ -822,7 +968,7 @@ server.tool(
822
968
  },
823
969
  async (input) => getDivergences(client, { ...input, project: projectFor(input) })
824
970
  );
825
- server.tool(
971
+ registerTool(
826
972
  "check_policies",
827
973
  "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).",
828
974
  {
@@ -839,6 +985,61 @@ server.tool(
839
985
  project: projectFor(input)
840
986
  })
841
987
  );
988
+ registerTool(
989
+ "neat_list_uninstrumented",
990
+ "List libraries in the project that need instrumentation beyond the auto-instrumentations bundle. Returns first-party, third-party, and gap libraries that require an explicit instrumentation package.",
991
+ { project: projectField },
992
+ async (input) => neatListUninstrumented(client, { project: projectFor(input) })
993
+ );
994
+ registerTool(
995
+ "neat_lookup_instrumentation",
996
+ "Look up the registry entry for a specific library. Returns the canonical instrumentation package, version, and registration snippet if one exists.",
997
+ {
998
+ library: import_zod.z.string().describe('npm package name, e.g. "@prisma/client"'),
999
+ installedVersion: import_zod.z.string().optional().describe("Installed version for range matching"),
1000
+ project: projectField
1001
+ },
1002
+ async (input) => neatLookupInstrumentation(client, { ...input, project: projectFor(input) })
1003
+ );
1004
+ registerTool(
1005
+ "neat_describe_project_instrumentation",
1006
+ "Describe the current state of OTel instrumentation in the project: which hook files exist, whether .env.neat is present, which OTel deps are installed.",
1007
+ { project: projectField },
1008
+ async (input) => neatDescribeProjectInstrumentation(client, { project: projectFor(input) })
1009
+ );
1010
+ registerTool(
1011
+ "neat_apply_extension",
1012
+ "Install an instrumentation package and splice its registration into the existing OTel hook file. Idempotent \u2014 calling twice with the same args is a no-op. Only modifies instrumentation files, package.json, and the lockfile (via the project package manager).",
1013
+ {
1014
+ library: import_zod.z.string().describe('The library being instrumented, e.g. "@prisma/client"'),
1015
+ instrumentation_package: import_zod.z.string().describe('The instrumentation npm package, e.g. "@prisma/instrumentation"'),
1016
+ version: import_zod.z.string().describe('Semver range for the instrumentation package, e.g. "^6.0.0"'),
1017
+ registration_snippet: import_zod.z.string().describe('The JS/TS snippet to splice into the instrumentations array, e.g. "instrumentations.push(new PrismaInstrumentation())"'),
1018
+ project: projectField
1019
+ },
1020
+ async (input) => neatApplyExtension(client, { ...input, project: projectFor(input) })
1021
+ );
1022
+ registerTool(
1023
+ "neat_dry_run_extension",
1024
+ "Preview what neat_apply_extension would do without making any changes. Returns the exact file diff, deps to add, and install command.",
1025
+ {
1026
+ library: import_zod.z.string().describe('The library being instrumented, e.g. "@prisma/client"'),
1027
+ instrumentation_package: import_zod.z.string().describe('The instrumentation npm package, e.g. "@prisma/instrumentation"'),
1028
+ version: import_zod.z.string().describe('Semver range for the instrumentation package, e.g. "^6.0.0"'),
1029
+ registration_snippet: import_zod.z.string().describe("The JS/TS snippet to splice into the instrumentations array"),
1030
+ project: projectField
1031
+ },
1032
+ async (input) => neatDryRunExtension(client, { ...input, project: projectFor(input) })
1033
+ );
1034
+ registerTool(
1035
+ "neat_rollback_extension",
1036
+ "Undo the last neat_apply_extension for a given library. Removes the dep from package.json and the registration from the hook file. Does not re-run the package manager \u2014 run install manually to sync the lockfile.",
1037
+ {
1038
+ library: import_zod.z.string().describe("The library whose instrumentation should be rolled back"),
1039
+ project: projectField
1040
+ },
1041
+ async (input) => neatRollbackExtension(client, { ...input, project: projectFor(input) })
1042
+ );
842
1043
  var incidentsPollMs = process.env.NEAT_RESOURCE_POLL_MS ? Number(process.env.NEAT_RESOURCE_POLL_MS) : void 0;
843
1044
  var resourceRegistration = registerResources(server, client, {
844
1045
  ...incidentsPollMs !== void 0 ? { incidentsPollMs } : {},