@neat.is/mcp 0.4.11 → 0.4.13

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
@@ -4,7 +4,11 @@
4
4
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
6
  import { z } from "zod";
7
- import { CheckPoliciesScopeSchema, HypotheticalActionSchema } from "@neat.is/types";
7
+ import {
8
+ CheckPoliciesScopeSchema,
9
+ DivergenceTypeSchema,
10
+ HypotheticalActionSchema
11
+ } from "@neat.is/types";
8
12
 
9
13
  // src/client.ts
10
14
  function createHttpClient(baseUrl2, bearerToken) {
@@ -250,9 +254,6 @@ function registerResources(server2, client2, options = {}) {
250
254
  };
251
255
  }
252
256
 
253
- // src/index.ts
254
- import { DivergenceTypeSchema } from "@neat.is/types";
255
-
256
257
  // src/tools.ts
257
258
  import { Provenance } from "@neat.is/types";
258
259
 
@@ -717,6 +718,154 @@ async function postJson(client2, path, body) {
717
718
  }
718
719
  return c.post(path, body);
719
720
  }
721
+ async function neatListUninstrumented(client2, input) {
722
+ try {
723
+ const result = await client2.get(
724
+ projectPath(input.project, "/extend/list-uninstrumented")
725
+ );
726
+ const libs = result.libraries;
727
+ if (libs.length === 0) {
728
+ return formatEmptyResponse(
729
+ "All detected libraries are covered by the auto-instrumentations bundle or the HTTP fallback. No extension needed."
730
+ );
731
+ }
732
+ const blockLines = libs.map((l) => {
733
+ const pkgBit = l.instrumentation_package ? ` \u2192 ${l.instrumentation_package}@${l.package_version ?? "*"}` : " \u2192 no registry entry";
734
+ return ` \u2022 ${l.library} [${l.coverage}]${pkgBit}${l.notes ? ` \u2014 ${l.notes}` : ""}`;
735
+ });
736
+ return formatToolResponse({
737
+ summary: `${libs.length} librar${libs.length === 1 ? "y needs" : "ies need"} instrumentation beyond the auto-instrumentations bundle.`,
738
+ block: blockLines.join("\n")
739
+ });
740
+ } catch (err) {
741
+ return formatErrorResponse(`Error talking to neat-core: ${err.message}`);
742
+ }
743
+ }
744
+ async function neatLookupInstrumentation(client2, input) {
745
+ const qs = input.installedVersion ? `?library=${encodeURIComponent(input.library)}&version=${encodeURIComponent(input.installedVersion)}` : `?library=${encodeURIComponent(input.library)}`;
746
+ try {
747
+ const result = await client2.get(
748
+ projectPath(input.project, `/extend/lookup${qs}`)
749
+ );
750
+ const lines = [
751
+ ` coverage: ${result.coverage}`,
752
+ ...result.instrumentation_package ? [` instrumentation_package: ${result.instrumentation_package}@${result.package_version ?? "*"}`] : [],
753
+ ...result.registration ? [` registration: ${result.registration}`] : [],
754
+ ...result.notes ? [` notes: ${result.notes}`] : []
755
+ ];
756
+ return formatToolResponse({
757
+ summary: `Registry entry for ${input.library}: coverage is ${result.coverage}.`,
758
+ block: lines.join("\n")
759
+ });
760
+ } catch (err) {
761
+ if (err instanceof HttpError && err.status === 404) {
762
+ return formatEmptyResponse(`${input.library} is not in the instrumentation registry.`);
763
+ }
764
+ return formatErrorResponse(`Error talking to neat-core: ${err.message}`);
765
+ }
766
+ }
767
+ async function neatDescribeProjectInstrumentation(client2, input) {
768
+ try {
769
+ const state = await client2.get(
770
+ projectPath(input.project, "/extend/describe")
771
+ );
772
+ const lines = [
773
+ ` hook files: ${state.hookFiles.length > 0 ? state.hookFiles.join(", ") : "(none \u2014 run neat init first)"}`,
774
+ ` .env.neat: ${state.envNeat ? "present" : "absent"}`
775
+ ];
776
+ const depEntries = Object.entries(state.installedDeps);
777
+ if (depEntries.length > 0) {
778
+ lines.push(" installed OTel deps:");
779
+ for (const [pkg, ver] of depEntries) {
780
+ lines.push(` ${pkg}@${ver}`);
781
+ }
782
+ } else {
783
+ lines.push(" installed OTel deps: (none)");
784
+ }
785
+ const ready = state.hookFiles.length > 0;
786
+ return formatToolResponse({
787
+ 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.",
788
+ block: lines.join("\n")
789
+ });
790
+ } catch (err) {
791
+ return formatErrorResponse(`Error talking to neat-core: ${err.message}`);
792
+ }
793
+ }
794
+ async function neatApplyExtension(client2, input) {
795
+ try {
796
+ const result = await postJson(
797
+ client2,
798
+ projectPath(input.project, "/extend/apply"),
799
+ {
800
+ library: input.library,
801
+ instrumentation_package: input.instrumentation_package,
802
+ version: input.version,
803
+ registration_snippet: input.registration_snippet
804
+ }
805
+ );
806
+ if (result.alreadyApplied) {
807
+ return formatEmptyResponse(
808
+ `${input.library} instrumentation is already applied \u2014 no changes made.`
809
+ );
810
+ }
811
+ const lines = [
812
+ ` files touched: ${result.filesTouched.join(", ") || "(none)"}`,
813
+ ` deps added: ${result.depsAdded.join(", ") || "(none)"}`,
814
+ ` install: ${result.installOutput}`
815
+ ];
816
+ return formatToolResponse({
817
+ summary: `Applied ${input.instrumentation_package} for ${input.library}. ${result.filesTouched.length} file${result.filesTouched.length === 1 ? "" : "s"} touched, logged to ~/.neat/extend-log.ndjson.`,
818
+ block: lines.join("\n")
819
+ });
820
+ } catch (err) {
821
+ return formatErrorResponse(`Error talking to neat-core: ${err.message}`);
822
+ }
823
+ }
824
+ async function neatDryRunExtension(client2, input) {
825
+ try {
826
+ const result = await postJson(
827
+ client2,
828
+ projectPath(input.project, "/extend/dry-run"),
829
+ {
830
+ library: input.library,
831
+ instrumentation_package: input.instrumentation_package,
832
+ version: input.version,
833
+ registration_snippet: input.registration_snippet
834
+ }
835
+ );
836
+ const lines = [
837
+ ` files that would be touched: ${result.filesTouched.join(", ") || "(none)"}`,
838
+ ` deps to add: ${result.depsToAdd.join(", ") || "(none)"}`,
839
+ ` hook file patch: ${result.templatePatch}`
840
+ ];
841
+ return formatToolResponse({
842
+ summary: `Dry run for ${input.library}: ${result.filesTouched.length} file${result.filesTouched.length === 1 ? "" : "s"} would be touched. No changes made.`,
843
+ block: lines.join("\n")
844
+ });
845
+ } catch (err) {
846
+ return formatErrorResponse(`Error talking to neat-core: ${err.message}`);
847
+ }
848
+ }
849
+ async function neatRollbackExtension(client2, input) {
850
+ try {
851
+ const result = await postJson(
852
+ client2,
853
+ projectPath(input.project, "/extend/rollback"),
854
+ { library: input.library }
855
+ );
856
+ if (!result.undone) {
857
+ return formatEmptyResponse(
858
+ `No prior apply found for ${input.library} \u2014 nothing to roll back.`
859
+ );
860
+ }
861
+ return formatToolResponse({
862
+ summary: `Rolled back instrumentation for ${input.library}. ${result.message}. Run your package manager install to sync the lockfile.`,
863
+ block: ` result: ${result.message}`
864
+ });
865
+ } catch (err) {
866
+ return formatErrorResponse(`Error talking to neat-core: ${err.message}`);
867
+ }
868
+ }
720
869
 
721
870
  // src/index.ts
722
871
  var baseUrl = process.env.NEAT_CORE_URL ?? "http://localhost:8080";
@@ -731,7 +880,8 @@ var server = new McpServer({
731
880
  name: "neat",
732
881
  version: "0.1.0"
733
882
  });
734
- server.tool(
883
+ var registerTool = (name, description, paramsSchema, cb) => server.tool(name, description, paramsSchema, cb);
884
+ registerTool(
735
885
  "get_root_cause",
736
886
  "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.",
737
887
  {
@@ -741,7 +891,7 @@ server.tool(
741
891
  },
742
892
  async (input) => getRootCause(client, { ...input, project: projectFor(input) })
743
893
  );
744
- server.tool(
894
+ registerTool(
745
895
  "get_blast_radius",
746
896
  "List every node downstream of the given node \u2014 what would break if this node failed or was redeployed.",
747
897
  {
@@ -751,7 +901,7 @@ server.tool(
751
901
  },
752
902
  async (input) => getBlastRadius(client, { ...input, project: projectFor(input) })
753
903
  );
754
- server.tool(
904
+ registerTool(
755
905
  "get_dependencies",
756
906
  "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.",
757
907
  {
@@ -761,7 +911,7 @@ server.tool(
761
911
  },
762
912
  async (input) => getDependencies(client, { ...input, project: projectFor(input) })
763
913
  );
764
- server.tool(
914
+ registerTool(
765
915
  "get_observed_dependencies",
766
916
  "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.",
767
917
  {
@@ -770,7 +920,7 @@ server.tool(
770
920
  },
771
921
  async (input) => getObservedDependencies(client, { ...input, project: projectFor(input) })
772
922
  );
773
- server.tool(
923
+ registerTool(
774
924
  "get_incident_history",
775
925
  "Return recent OTel error events recorded against a node, most recent first.",
776
926
  {
@@ -780,7 +930,7 @@ server.tool(
780
930
  },
781
931
  async (input) => getIncidentHistory(client, { ...input, project: projectFor(input) })
782
932
  );
783
- server.tool(
933
+ registerTool(
784
934
  "semantic_search",
785
935
  "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.",
786
936
  {
@@ -789,7 +939,7 @@ server.tool(
789
939
  },
790
940
  async (input) => semanticSearch(client, { ...input, project: projectFor(input) })
791
941
  );
792
- server.tool(
942
+ registerTool(
793
943
  "get_graph_diff",
794
944
  '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.',
795
945
  {
@@ -800,7 +950,7 @@ server.tool(
800
950
  },
801
951
  async (input) => getGraphDiff(client, { ...input, project: projectFor(input) })
802
952
  );
803
- server.tool(
953
+ registerTool(
804
954
  "get_recent_stale_edges",
805
955
  "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.",
806
956
  {
@@ -810,7 +960,7 @@ server.tool(
810
960
  },
811
961
  async (input) => getRecentStaleEdges(client, { ...input, project: projectFor(input) })
812
962
  );
813
- server.tool(
963
+ registerTool(
814
964
  "get_divergences",
815
965
  "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.",
816
966
  {
@@ -823,7 +973,7 @@ server.tool(
823
973
  },
824
974
  async (input) => getDivergences(client, { ...input, project: projectFor(input) })
825
975
  );
826
- server.tool(
976
+ registerTool(
827
977
  "check_policies",
828
978
  "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).",
829
979
  {
@@ -840,6 +990,61 @@ server.tool(
840
990
  project: projectFor(input)
841
991
  })
842
992
  );
993
+ registerTool(
994
+ "neat_list_uninstrumented",
995
+ "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.",
996
+ { project: projectField },
997
+ async (input) => neatListUninstrumented(client, { project: projectFor(input) })
998
+ );
999
+ registerTool(
1000
+ "neat_lookup_instrumentation",
1001
+ "Look up the registry entry for a specific library. Returns the canonical instrumentation package, version, and registration snippet if one exists.",
1002
+ {
1003
+ library: z.string().describe('npm package name, e.g. "@prisma/client"'),
1004
+ installedVersion: z.string().optional().describe("Installed version for range matching"),
1005
+ project: projectField
1006
+ },
1007
+ async (input) => neatLookupInstrumentation(client, { ...input, project: projectFor(input) })
1008
+ );
1009
+ registerTool(
1010
+ "neat_describe_project_instrumentation",
1011
+ "Describe the current state of OTel instrumentation in the project: which hook files exist, whether .env.neat is present, which OTel deps are installed.",
1012
+ { project: projectField },
1013
+ async (input) => neatDescribeProjectInstrumentation(client, { project: projectFor(input) })
1014
+ );
1015
+ registerTool(
1016
+ "neat_apply_extension",
1017
+ "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).",
1018
+ {
1019
+ library: z.string().describe('The library being instrumented, e.g. "@prisma/client"'),
1020
+ instrumentation_package: z.string().describe('The instrumentation npm package, e.g. "@prisma/instrumentation"'),
1021
+ version: z.string().describe('Semver range for the instrumentation package, e.g. "^6.0.0"'),
1022
+ registration_snippet: z.string().describe('The JS/TS snippet to splice into the instrumentations array, e.g. "instrumentations.push(new PrismaInstrumentation())"'),
1023
+ project: projectField
1024
+ },
1025
+ async (input) => neatApplyExtension(client, { ...input, project: projectFor(input) })
1026
+ );
1027
+ registerTool(
1028
+ "neat_dry_run_extension",
1029
+ "Preview what neat_apply_extension would do without making any changes. Returns the exact file diff, deps to add, and install command.",
1030
+ {
1031
+ library: z.string().describe('The library being instrumented, e.g. "@prisma/client"'),
1032
+ instrumentation_package: z.string().describe('The instrumentation npm package, e.g. "@prisma/instrumentation"'),
1033
+ version: z.string().describe('Semver range for the instrumentation package, e.g. "^6.0.0"'),
1034
+ registration_snippet: z.string().describe("The JS/TS snippet to splice into the instrumentations array"),
1035
+ project: projectField
1036
+ },
1037
+ async (input) => neatDryRunExtension(client, { ...input, project: projectFor(input) })
1038
+ );
1039
+ registerTool(
1040
+ "neat_rollback_extension",
1041
+ "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.",
1042
+ {
1043
+ library: z.string().describe("The library whose instrumentation should be rolled back"),
1044
+ project: projectField
1045
+ },
1046
+ async (input) => neatRollbackExtension(client, { ...input, project: projectFor(input) })
1047
+ );
843
1048
  var incidentsPollMs = process.env.NEAT_RESOURCE_POLL_MS ? Number(process.env.NEAT_RESOURCE_POLL_MS) : void 0;
844
1049
  var resourceRegistration = registerResources(server, client, {
845
1050
  ...incidentsPollMs !== void 0 ? { incidentsPollMs } : {},