chain-insights 0.2.16

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.
Files changed (153) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +165 -0
  3. package/bin/cli.js +10 -0
  4. package/bin/install.cjs +252 -0
  5. package/bin/mcp-proxy.cjs +10 -0
  6. package/dist/active-BSrxLKwn.mjs +50 -0
  7. package/dist/active-BSrxLKwn.mjs.map +1 -0
  8. package/dist/active-Dv7Tu-O4.cjs +68 -0
  9. package/dist/app-BjjuQM0B.mjs +155 -0
  10. package/dist/app-BjjuQM0B.mjs.map +1 -0
  11. package/dist/app-Dq1TdB6p.cjs +161 -0
  12. package/dist/artifact-server-DoxJ7fCx.cjs +47 -0
  13. package/dist/artifact-server-Dxz5YbuQ.mjs +48 -0
  14. package/dist/artifact-server-Dxz5YbuQ.mjs.map +1 -0
  15. package/dist/assets/bg-pattern.png +0 -0
  16. package/dist/assets/logo.png +0 -0
  17. package/dist/call-args-DQA2QcRA.cjs +27 -0
  18. package/dist/call-args-Lk_wOJxd.mjs +29 -0
  19. package/dist/call-args-Lk_wOJxd.mjs.map +1 -0
  20. package/dist/capabilities-CB97WMA5.cjs +83 -0
  21. package/dist/capabilities-DliMBim-.mjs +84 -0
  22. package/dist/capabilities-DliMBim-.mjs.map +1 -0
  23. package/dist/cases-By7INiOa.mjs +6 -0
  24. package/dist/cases-CDcNU91B.cjs +9 -0
  25. package/dist/chunk-CZWwpsFl.cjs +43 -0
  26. package/dist/cli.cjs +752 -0
  27. package/dist/cli.d.cts +1 -0
  28. package/dist/cli.d.mts +1 -0
  29. package/dist/cli.mjs +753 -0
  30. package/dist/cli.mjs.map +1 -0
  31. package/dist/client-D4Bq0rp9.mjs +111 -0
  32. package/dist/client-D4Bq0rp9.mjs.map +1 -0
  33. package/dist/client-D4fZgIaO.cjs +132 -0
  34. package/dist/config-Bmdl5hdk.cjs +67 -0
  35. package/dist/config-BwrBYmiC.mjs +44 -0
  36. package/dist/config-BwrBYmiC.mjs.map +1 -0
  37. package/dist/data-extractor-BNGj7ECT.cjs +347 -0
  38. package/dist/data-extractor-DFzsa5CS.mjs +336 -0
  39. package/dist/data-extractor-DFzsa5CS.mjs.map +1 -0
  40. package/dist/dossier-BsroDgD3.mjs +76 -0
  41. package/dist/dossier-BsroDgD3.mjs.map +1 -0
  42. package/dist/dossier-DtxREpPm.cjs +76 -0
  43. package/dist/evidence-BGcdKxuV.cjs +200 -0
  44. package/dist/evidence-BhvFW-y_.mjs +195 -0
  45. package/dist/evidence-BhvFW-y_.mjs.map +1 -0
  46. package/dist/format-Ce1RObVl.mjs +22 -0
  47. package/dist/format-Ce1RObVl.mjs.map +1 -0
  48. package/dist/format-DOrPvXEr.cjs +20 -0
  49. package/dist/frontmatter-D8wWCeOa.mjs +26 -0
  50. package/dist/frontmatter-D8wWCeOa.mjs.map +1 -0
  51. package/dist/frontmatter-DgAuai7E.cjs +35 -0
  52. package/dist/graph-normalizer-Cv9yK9Pg.mjs +130 -0
  53. package/dist/graph-normalizer-Cv9yK9Pg.mjs.map +1 -0
  54. package/dist/graph-normalizer-DeIj6Ses.cjs +133 -0
  55. package/dist/graph-reports-C4TBjCkM.mjs +63 -0
  56. package/dist/graph-reports-C4TBjCkM.mjs.map +1 -0
  57. package/dist/graph-reports-DU05YCei.cjs +64 -0
  58. package/dist/html-generator-CAv81IWH.cjs +85 -0
  59. package/dist/html-generator-V6Bp0uRb.mjs +68 -0
  60. package/dist/html-generator-V6Bp0uRb.mjs.map +1 -0
  61. package/dist/index.cjs +31 -0
  62. package/dist/index.d.cts +187 -0
  63. package/dist/index.d.cts.map +1 -0
  64. package/dist/index.d.mts +187 -0
  65. package/dist/index.d.mts.map +1 -0
  66. package/dist/index.mjs +9 -0
  67. package/dist/init-BjuFt54X.cjs +232 -0
  68. package/dist/init-CaOsHTIo.mjs +232 -0
  69. package/dist/init-CaOsHTIo.mjs.map +1 -0
  70. package/dist/mcp-proxy.cjs +1257 -0
  71. package/dist/mcp-proxy.d.cts +12 -0
  72. package/dist/mcp-proxy.d.cts.map +1 -0
  73. package/dist/mcp-proxy.d.mts +12 -0
  74. package/dist/mcp-proxy.d.mts.map +1 -0
  75. package/dist/mcp-proxy.mjs +1255 -0
  76. package/dist/mcp-proxy.mjs.map +1 -0
  77. package/dist/output-root-CFYms3ad.cjs +43 -0
  78. package/dist/output-root-CmWM7aV2.mjs +33 -0
  79. package/dist/output-root-CmWM7aV2.mjs.map +1 -0
  80. package/dist/parser-BUIWW1OH.cjs +182 -0
  81. package/dist/parser-DO0_SssG.mjs +182 -0
  82. package/dist/parser-DO0_SssG.mjs.map +1 -0
  83. package/dist/public-tools-D4UI-Zb0.mjs +2554 -0
  84. package/dist/public-tools-D4UI-Zb0.mjs.map +1 -0
  85. package/dist/public-tools-XSpkz2ky.cjs +2556 -0
  86. package/dist/resolver-C2ZS7oC8.mjs +201 -0
  87. package/dist/resolver-C2ZS7oC8.mjs.map +1 -0
  88. package/dist/resolver-zYbu4wDV.cjs +203 -0
  89. package/dist/rolldown-runtime-wcPFST8Q.mjs +13 -0
  90. package/dist/runner-1Eq55OYb.cjs +148 -0
  91. package/dist/runner-BhUHbiHG.mjs +149 -0
  92. package/dist/runner-BhUHbiHG.mjs.map +1 -0
  93. package/dist/schema-4XpzDFQM.cjs +55 -0
  94. package/dist/schema-8d0rVIdZ.mjs +37 -0
  95. package/dist/schema-8d0rVIdZ.mjs.map +1 -0
  96. package/dist/schema-cache-9CksD7tX.mjs +34 -0
  97. package/dist/schema-cache-9CksD7tX.mjs.map +1 -0
  98. package/dist/schema-cache-CgWRCN2N.cjs +36 -0
  99. package/dist/selector-CkFcTXzz.cjs +10 -0
  100. package/dist/selector-xjm6NTHI.mjs +12 -0
  101. package/dist/selector-xjm6NTHI.mjs.map +1 -0
  102. package/dist/server-BkM5xrXb.mjs +45 -0
  103. package/dist/server-BkM5xrXb.mjs.map +1 -0
  104. package/dist/server-DXowbpfi.cjs +54 -0
  105. package/dist/session-BpNylyuJ.cjs +115 -0
  106. package/dist/session-CcTgYxsj.mjs +115 -0
  107. package/dist/session-CcTgYxsj.mjs.map +1 -0
  108. package/dist/setup-DOpKPrlx.cjs +81 -0
  109. package/dist/setup-DyrWHuwQ.mjs +80 -0
  110. package/dist/setup-DyrWHuwQ.mjs.map +1 -0
  111. package/dist/store-BiUhQOIf.cjs +230 -0
  112. package/dist/store-BoWE-Gtl.mjs +225 -0
  113. package/dist/store-BoWE-Gtl.mjs.map +1 -0
  114. package/dist/templates/graph.html +1406 -0
  115. package/dist/tool-visibility-3Z_KvO9Q.mjs +28 -0
  116. package/dist/tool-visibility-3Z_KvO9Q.mjs.map +1 -0
  117. package/dist/tool-visibility-CwgY205r.cjs +36 -0
  118. package/dist/tools-Cp2jAAAb.mjs +100 -0
  119. package/dist/tools-Cp2jAAAb.mjs.map +1 -0
  120. package/dist/tools-f_vJUZAF.cjs +139 -0
  121. package/dist/topup-server-BZuQifvh.cjs +940 -0
  122. package/dist/topup-server-DUjyFftI.mjs +919 -0
  123. package/dist/topup-server-DUjyFftI.mjs.map +1 -0
  124. package/dist/version-1gP19Lhi.mjs +8 -0
  125. package/dist/version-1gP19Lhi.mjs.map +1 -0
  126. package/dist/version-BNGtdpmH.cjs +18 -0
  127. package/dist/viz-BlCJe6Tk.mjs +35 -0
  128. package/dist/viz-BlCJe6Tk.mjs.map +1 -0
  129. package/dist/viz-ClezVXrJ.cjs +44 -0
  130. package/dist/wallet-BMelXBYP.mjs +104 -0
  131. package/dist/wallet-BMelXBYP.mjs.map +1 -0
  132. package/dist/wallet-RnvvSpV2.cjs +146 -0
  133. package/docs/architecture.md +145 -0
  134. package/docs/contributing.md +68 -0
  135. package/docs/debugging.md +68 -0
  136. package/docs/development.md +44 -0
  137. package/docs/graph-tools.md +251 -0
  138. package/docs/images/graph-mcp-iframe.png +0 -0
  139. package/docs/images/graph-visualization.png +0 -0
  140. package/docs/images/topup-page.png +0 -0
  141. package/docs/investigation-workspaces.md +151 -0
  142. package/docs/mcp-proxy.md +180 -0
  143. package/package.json +59 -0
  144. package/skills/chain-insights-developer-experience/SKILL.md +101 -0
  145. package/skills/chain-insights-investigation/SKILL.md +285 -0
  146. package/skills/chain-insights-investigation/agents/openai.yaml +4 -0
  147. package/skills/chain-insights-investigation/scripts/run-target-uat.sh +197 -0
  148. package/skills/chain-insights-trace-funds/SKILL.md +249 -0
  149. package/skills/ci-case/SKILL.md +43 -0
  150. package/skills/ci-status/SKILL.md +45 -0
  151. package/skills/test-chain-insights-graphrag-mcp/SKILL.md +75 -0
  152. package/skills/test-chain-insights-graphrag-mcp/agents/openai.yaml +4 -0
  153. package/skills/test-chain-insights-graphrag-mcp/scripts/run-uat.sh +414 -0
@@ -0,0 +1,1257 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_chunk = require("./chunk-CZWwpsFl.cjs");
3
+ const require_version = require("./version-BNGtdpmH.cjs");
4
+ const require_tool_visibility = require("./tool-visibility-CwgY205r.cjs");
5
+ let node_url = require("node:url");
6
+ let node_path = require("node:path");
7
+ node_path = require_chunk.__toESM(node_path, 1);
8
+ let node_fs = require("node:fs");
9
+ let node_fs_promises = require("node:fs/promises");
10
+ let zod = require("zod");
11
+ zod = require_chunk.__toESM(zod, 1);
12
+ let _modelcontextprotocol_sdk_server_mcp_js = require("@modelcontextprotocol/sdk/server/mcp.js");
13
+ let _modelcontextprotocol_sdk_server_stdio_js = require("@modelcontextprotocol/sdk/server/stdio.js");
14
+ let _modelcontextprotocol_sdk_client_index_js = require("@modelcontextprotocol/sdk/client/index.js");
15
+ let _modelcontextprotocol_sdk_client_streamableHttp_js = require("@modelcontextprotocol/sdk/client/streamableHttp.js");
16
+ let _modelcontextprotocol_ext_apps_server = require("@modelcontextprotocol/ext-apps/server");
17
+ //#region src/mcp/proxy.ts
18
+ const LOCAL_TOOL_NAMES = new Set([
19
+ "balance",
20
+ "help",
21
+ "case_open",
22
+ "case_list",
23
+ "case_resume",
24
+ "case_add_evidence",
25
+ "case_verify_evidence",
26
+ "case_update_dossier",
27
+ "case_start_session",
28
+ "case_end_session"
29
+ ]);
30
+ const PUBLIC_GRAPHRAG_PROMPT_NAMES = new Set(["address-risk", "track-funds"]);
31
+ const GRAPH_RESOURCE_URI = "ui://chain-insights/graph";
32
+ const GRAPH_APP_TOOL_NAMES = new Set([
33
+ "address_risk",
34
+ "scam_topology",
35
+ "track_funds"
36
+ ]);
37
+ const GRAPH_ARRAY_KEYS = [
38
+ "nodes",
39
+ "edges",
40
+ "flows",
41
+ "edge_anchors"
42
+ ];
43
+ const __dirname$1 = node_path.default.dirname((0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href));
44
+ const COMMA_SEPARATED_ADDRESS_FIELDS = new Set(["trusted_addresses", "untrusted_addresses"]);
45
+ const KNOWN_PUBLIC_TOOL_REQUIRED_ARGS = {
46
+ address_risk: ["address", "network"],
47
+ scam_topology: [
48
+ "victim_address",
49
+ "incident_timestamp_ms",
50
+ "network"
51
+ ],
52
+ track_funds: ["trusted_addresses", "network"],
53
+ graph_query: ["query", "network"],
54
+ graph_query_batch: ["network", "queries"]
55
+ };
56
+ const KNOWN_PUBLIC_TOOL_DESCRIPTIONS = {
57
+ network_capabilities: "Return supported Chain Insights networks, capability layers, tool availability, data retention windows, and freshness. Use this before choosing network-specific tools.",
58
+ address_risk: "Screen one full blockchain address for AML risk, behavior patterns, neighborhood context, exchange exposure, and optional comparison with compare_address. This includes the exchange-behavior analysis formerly covered by money_flows_between_exchanges. Use this as the first tool for a single-address investigation. The tool returns an investigator-ready summary; preserve full addresses exactly.",
59
+ scam_topology: "Build victim-incident laundering topology from one victim/source address and the earliest known incident timestamp. Traversal uses one explicit activity policy: node_relative_only by default, or global_incident_only when requested. Repeated targets are kept as non-expanding convergence edges. Returns ML-ready scam_labels plus review context and a track_funds-compatible graph report: primary flows, deposits, reverse_leads. Victims, exchange endpoints, and generic labeled context nodes are not automatic scam labels; preserve full addresses exactly.",
60
+ track_funds: "Trace funds from trusted victim/source addresses through intermediaries to exchange deposit addresses. Use this when the user has a victim/source address or known untrusted/scammer addresses. The tool returns an investigator-ready fund-flow report and recommended next actions.",
61
+ graph_query: "Run a read-only GQL/Cypher query through the Chain Insights graph endpoint. Use USE live_topology for Memgraph RAM topology, USE archive_topology for StarRocks historical topology, and USE facts for StarRocks facts. Cross-backend correlated joins are limited by current MemGQL behavior; preserve full addresses exactly.",
62
+ graph_query_batch: "Run multiple read-only GQL/Cypher queries through the Chain Insights graph endpoint in one paid batch. Prefer this for related topology/facts reads."
63
+ };
64
+ const NETWORK_DESCRIPTION = "Required network to query. Do not guess; use network_capabilities or ask the user if missing.";
65
+ const REMOTE_GRAPH_TOOL_REQUEST_TIMEOUT_MS = 900 * 1e3;
66
+ const CHAIN_INSIGHTS_WORKFLOW = [
67
+ "Workflow:",
68
+ "1. If the user is starting or continuing an investigation, use case_open or case_list/case_resume first.",
69
+ "2. Do not call investigation tools until required arguments are known. Network is required; use network_capabilities to check supported networks, data layers, retention, and freshness, or ask the user if missing.",
70
+ "3. Use address_risk first for a single address when facts and topology are available. Use track_funds for victim/source fund tracing when topology is available. Use scam_topology when known victim incident ground truth should become ML-ready scam labels. Use graph_query(_batch) for the universal graph-language path over topology and facts.",
71
+ "4. After a material result, preserve it with case_add_evidence when a case is active or ask whether to create/select a case.",
72
+ "5. Use case_update_dossier for durable address/entity findings and case_start_session/case_end_session for session notes."
73
+ ].join("\n");
74
+ const GRAPH_SCHEMA_HINTS = [
75
+ "Graph query hints for network=bittensor:",
76
+ "- Common live topology node labels include Address and may include legacy enrichment labels. Do not depend on Exchange/Miner graph labels for correctness; use address properties such as labels and is_exchange when available.",
77
+ "- Address nodes are identity plus traversal hints. Lifetime/global address metrics live in USE facts as AddressFeature, not as topology semantics.",
78
+ "- Facts graph labels include Address, AddressLabel, AddressFeature, RiskScore, and Asset.",
79
+ "- Facts graph relationships include (:Address)-[:HAS_FEATURE]->(:AddressFeature), (:Address)-[:HAS_LABEL]->(:AddressLabel), and (:Address)-[:HAS_RISK_SCORE]->(:RiskScore).",
80
+ "- Risk and ML properties may appear as live hints, but source-of-truth risk rows are RiskScore facts.",
81
+ "- Common relationships include FLOWS_TO, OPERATED_FROM, SERVED_FROM, REGISTERED_NEURON, BELONGS_TO, SYBIL_CLUSTER, LAYERING_HOP, BURST_ACTIVITY, CYCLE_PARTICIPANT, SMURFING_CLUSTER.",
82
+ "- FLOWS_TO properties are scoped to the selected topology graph and commonly carry amount_sum, amount_usd_sum, tx_count, first_seen_timestamp, last_seen_timestamp, first_tx_id, last_tx_id. Confirm available fields through runtime schema before relying on them.",
83
+ "- Start schema discovery with MemGQL-safe property reads: MATCH (n:Address) WHERE n.address IS NOT NULL RETURN n.labels AS labels, n.address AS address LIMIT 20",
84
+ "- Relationship discovery: MATCH (:Address)-[r:FLOWS_TO]->(:Address) RETURN r.amount_sum AS amount_sum, r.amount_usd_sum AS amount_usd_sum LIMIT 20",
85
+ "- graph_query uses Memgraph Zero / MemGQL when available. Use USE live_topology for Memgraph RAM topology, USE archive_topology for StarRocks historical topology, and USE facts for StarRocks facts.",
86
+ "- Archive topology labels include Address and TopologySnapshot. Archived money-flow topology is represented as (:Address)-[:FLOWS_TO]->(:Address) relationships with period_granularity, period_start_date, and period_end_date.",
87
+ "- All graph_query calls are read-only. Never use CREATE, INSERT, MERGE, SET, DELETE, REMOVE, DROP, DETACH, ADD, CONNECT, DISCONNECT, ALTER, TRUNCATE, GRANT, or REVOKE.",
88
+ "- Warehouse facts live behind facts_*_view StarRocks views and are reached through USE facts graph patterns. Do not query core_*, ml_*, analyzers_*, synthetics_*, or _* tables directly."
89
+ ].join("\n");
90
+ const GRAPH_REPORT_HINTS = [
91
+ "Graph visualization behavior:",
92
+ "- Graph-backed tools return the investigator report as text content and keep raw graph data out of LLM-visible structuredContent.",
93
+ "- Raw graph data is stored locally under Chain Insights reports/graphs and exposed to the graph app as _meta.chainInsights.graph.url.",
94
+ "- The local graph report server is started automatically by the MCP server when a graph-backed tool returns a report URL; do not ask the user to run chain-insights serve for Claude Desktop graph iframes.",
95
+ "- If an iframe reports that a graph report fetch failed, retry the graph-backed tool call so Chain Insights can recreate the report URL and ensure the local report server is running."
96
+ ].join("\n");
97
+ const SERVER_INSTRUCTIONS = [
98
+ "Chain Insights is a local AML investigation workspace for AI agents.",
99
+ CHAIN_INSIGHTS_WORKFLOW,
100
+ GRAPH_REPORT_HINTS,
101
+ GRAPH_SCHEMA_HINTS,
102
+ "Presentation rules: preserve tool summaries as returned; never truncate blockchain addresses; use case tools to preserve evidence when a case exists."
103
+ ].join("\n\n");
104
+ function readGraphAppHtml() {
105
+ const candidates = [
106
+ node_path.default.resolve(__dirname$1, "templates", "graph.html"),
107
+ node_path.default.resolve(__dirname$1, "..", "templates", "graph.html"),
108
+ node_path.default.resolve(__dirname$1, "..", "viz", "templates", "graph.html")
109
+ ];
110
+ for (const candidate of candidates) try {
111
+ return (0, node_fs.readFileSync)(candidate, "utf8");
112
+ } catch (err) {
113
+ if (err.code !== "ENOENT") throw err;
114
+ }
115
+ throw new Error(`Graph MCP app template not found. Tried: ${candidates.join(", ")}`);
116
+ }
117
+ function graphArtifactOrigins(config) {
118
+ return [`http://127.0.0.1:${config.serverPort}`, `http://localhost:${config.serverPort}`];
119
+ }
120
+ function hasGraphApp(tool) {
121
+ const configuredUri = tool._meta?.ui;
122
+ if (configuredUri && typeof configuredUri === "object" && "resourceUri" in configuredUri && configuredUri.resourceUri === GRAPH_RESOURCE_URI) return true;
123
+ if (tool._meta?.["ui/resourceUri"] === GRAPH_RESOURCE_URI) return true;
124
+ if (GRAPH_APP_TOOL_NAMES.has(tool.name)) return true;
125
+ return JSON.stringify(tool.outputSchema ?? {}).includes("\"app_data\"");
126
+ }
127
+ function graphToolMeta(tool) {
128
+ const meta = { ...tool._meta ?? {} };
129
+ const ui = meta.ui && typeof meta.ui === "object" && !Array.isArray(meta.ui) ? { ...meta.ui } : {};
130
+ return {
131
+ ...meta,
132
+ ui: {
133
+ ...ui,
134
+ resourceUri: GRAPH_RESOURCE_URI
135
+ }
136
+ };
137
+ }
138
+ function knownPublicToolInputSchema(toolName) {
139
+ switch (toolName) {
140
+ case "address_risk": return {
141
+ address: zod.string().min(1).describe("Full blockchain address to screen"),
142
+ network: zod.string().min(1).describe(NETWORK_DESCRIPTION),
143
+ compare_address: zod.string().optional().describe("Optional second full address for comparison"),
144
+ include_attachments: zod.boolean().optional().describe("Include graph app report metadata")
145
+ };
146
+ case "track_funds": return {
147
+ trusted_addresses: zod.string().min(1).describe("Comma-separated full trusted victim addresses. Min 1, max 5."),
148
+ network: zod.string().min(1).describe(NETWORK_DESCRIPTION),
149
+ untrusted_addresses: zod.string().optional().describe("Comma-separated full untrusted/scammer addresses. Max 5."),
150
+ include_attachments: zod.boolean().optional().describe("Include graph app report metadata")
151
+ };
152
+ case "scam_topology": return {
153
+ network: zod.string().min(1).describe(NETWORK_DESCRIPTION),
154
+ victim_address: zod.string().min(1).describe("Full victim/source address that anchors the scam incident. Victims are not risky labels."),
155
+ incident_timestamp_ms: zod.number().min(0).describe("Earliest known incident transfer timestamp in milliseconds. Primary traversal uses node-relative wave-arrival filtering."),
156
+ max_hops: zod.number().int().min(1).max(64).optional().describe("Maximum forward expansion depth. Default 16."),
157
+ activity_policy: zod.enum(["node_relative_only", "global_incident_only"]).optional().describe("Traversal activity policy. Default node_relative_only."),
158
+ case_id: zod.string().optional().describe("Optional Chain Insights case ID. When provided, compact evidence is appended to the case manifest.")
159
+ };
160
+ case "graph_query": return {
161
+ query: zod.string().min(1).describe("Read-only GQL/Cypher query. Use USE live_topology for Memgraph RAM topology, USE archive_topology for StarRocks historical topology, and USE facts for StarRocks facts."),
162
+ network: zod.string().min(1).describe(NETWORK_DESCRIPTION)
163
+ };
164
+ case "graph_query_batch": return {
165
+ network: zod.string().min(1).describe(NETWORK_DESCRIPTION),
166
+ queries: zod.array(zod.object({
167
+ id: zod.string().optional(),
168
+ query: zod.string().min(1).describe("Read-only GQL/Cypher query")
169
+ })).min(1).max(20),
170
+ per_query_timeout_seconds: zod.number().int().min(1).max(600).optional()
171
+ };
172
+ default: return null;
173
+ }
174
+ }
175
+ function isRecord(value) {
176
+ return !!value && typeof value === "object" && !Array.isArray(value);
177
+ }
178
+ function redactLogValue(value) {
179
+ if (Array.isArray(value)) return value.map(redactLogValue);
180
+ if (!isRecord(value)) return value;
181
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => {
182
+ if (/token|secret|password|private.?key|authorization/i.test(key)) return [key, "[redacted]"];
183
+ return [key, redactLogValue(entry)];
184
+ }));
185
+ }
186
+ function errorForLog(err) {
187
+ const error = err;
188
+ return {
189
+ name: error.name ?? "Error",
190
+ message: error.message ?? String(err)
191
+ };
192
+ }
193
+ function sanitizeCypher(query) {
194
+ return query.replace(/\s+/g, " ").trim();
195
+ }
196
+ function cypherLogPayload(tool, args) {
197
+ if (!isRecord(args)) return null;
198
+ if (tool === "graph_query") return {
199
+ network: args.network,
200
+ queries: [{
201
+ id: tool,
202
+ query: typeof args.query === "string" ? sanitizeCypher(args.query) : args.query
203
+ }]
204
+ };
205
+ if (tool === "graph_query_batch") {
206
+ const queries = Array.isArray(args.queries) ? args.queries : [];
207
+ return {
208
+ network: args.network,
209
+ per_query_timeout_seconds: args.per_query_timeout_seconds,
210
+ query_count: queries.length,
211
+ queries: queries.map((entry, index) => isRecord(entry) ? {
212
+ id: typeof entry.id === "string" ? entry.id : `q${index + 1}`,
213
+ query: typeof entry.query === "string" ? sanitizeCypher(entry.query) : entry.query
214
+ } : {
215
+ id: `q${index + 1}`,
216
+ query: entry
217
+ })
218
+ };
219
+ }
220
+ return null;
221
+ }
222
+ function createMcpLogger(config) {
223
+ const disabled = process.env.CHAIN_INSIGHTS_MCP_LOG === "0";
224
+ const filePath = process.env.CHAIN_INSIGHTS_MCP_LOG_PATH?.trim() || node_path.default.join(config.dataDir, ".chain-insights", "runtime", "logs", "mcp-proxy.jsonl");
225
+ async function write(level, event, fields = {}) {
226
+ if (disabled) return;
227
+ try {
228
+ await (0, node_fs_promises.mkdir)(node_path.default.dirname(filePath), { recursive: true });
229
+ await (0, node_fs_promises.appendFile)(filePath, JSON.stringify({
230
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
231
+ level,
232
+ event,
233
+ pid: process.pid,
234
+ ...fields
235
+ }) + "\n", { mode: 384 });
236
+ } catch {}
237
+ }
238
+ return {
239
+ filePath,
240
+ info: (event, fields) => write("info", event, fields),
241
+ error: (event, fields) => write("error", event, fields)
242
+ };
243
+ }
244
+ function installToolLogging(server, logger) {
245
+ const existingRegisterTool = server.registerTool;
246
+ const originalRegisterTool = existingRegisterTool.bind(server);
247
+ const wrappedRegisterTool = ((name, config, handler) => {
248
+ const wrapped = async (args, extra) => {
249
+ const startedAt = Date.now();
250
+ await logger.info("tool.start", {
251
+ tool: name,
252
+ args: redactLogValue(args)
253
+ });
254
+ try {
255
+ const result = await handler(args, extra);
256
+ const isError = isRecord(result) && result.isError === true;
257
+ await logger.info("tool.end", {
258
+ tool: name,
259
+ duration_ms: Date.now() - startedAt,
260
+ is_error: isError
261
+ });
262
+ return result;
263
+ } catch (err) {
264
+ await logger.error("tool.throw", {
265
+ tool: name,
266
+ duration_ms: Date.now() - startedAt,
267
+ error: errorForLog(err)
268
+ });
269
+ throw err;
270
+ }
271
+ };
272
+ return originalRegisterTool(name, config, wrapped);
273
+ });
274
+ Object.assign(wrappedRegisterTool, existingRegisterTool);
275
+ server.registerTool = wrappedRegisterTool;
276
+ }
277
+ function installRemoteCypherLogging(remoteClient, logger) {
278
+ const existingCallTool = remoteClient.callTool;
279
+ const originalCallTool = existingCallTool.bind(remoteClient);
280
+ const wrappedCallTool = (async (...args) => {
281
+ const input = args[0];
282
+ const queryPayload = cypherLogPayload(input.name, input.arguments);
283
+ const startedAt = Date.now();
284
+ if (queryPayload) await logger.info("topology.start", {
285
+ tool: input.name,
286
+ ...queryPayload
287
+ });
288
+ try {
289
+ const result = await originalCallTool(...args);
290
+ if (queryPayload) await logger.info("topology.end", {
291
+ tool: input.name,
292
+ duration_ms: Date.now() - startedAt,
293
+ is_error: isRecord(result) && result.isError === true
294
+ });
295
+ return result;
296
+ } catch (err) {
297
+ if (queryPayload) await logger.error("cypher.throw", {
298
+ tool: input.name,
299
+ duration_ms: Date.now() - startedAt,
300
+ error: errorForLog(err)
301
+ });
302
+ throw err;
303
+ }
304
+ });
305
+ Object.assign(wrappedCallTool, existingCallTool);
306
+ remoteClient.callTool = wrappedCallTool;
307
+ }
308
+ function remoteToolRequestOptions(toolName) {
309
+ if (toolName === "graph_query" || toolName === "graph_query_batch") return {
310
+ timeout: REMOTE_GRAPH_TOOL_REQUEST_TIMEOUT_MS,
311
+ maxTotalTimeout: REMOTE_GRAPH_TOOL_REQUEST_TIMEOUT_MS
312
+ };
313
+ }
314
+ function isBlankArgument(value) {
315
+ if (value === void 0 || value === null) return true;
316
+ if (typeof value === "string") return value.trim() === "";
317
+ if (Array.isArray(value)) return value.length === 0 || value.every(isBlankArgument);
318
+ return false;
319
+ }
320
+ function normalizeRemoteToolArguments(toolName, args) {
321
+ const normalized = isRecord(args) ? { ...args } : {};
322
+ if (!(toolName in KNOWN_PUBLIC_TOOL_REQUIRED_ARGS)) return normalized;
323
+ for (const fieldName of COMMA_SEPARATED_ADDRESS_FIELDS) {
324
+ const value = normalized[fieldName];
325
+ if (Array.isArray(value)) normalized[fieldName] = value.map((entry) => String(entry).trim()).filter(Boolean).join(",");
326
+ }
327
+ return normalized;
328
+ }
329
+ function validateKnownPublicToolArguments(toolName, args) {
330
+ const requiredArgs = KNOWN_PUBLIC_TOOL_REQUIRED_ARGS[toolName];
331
+ if (!requiredArgs) return null;
332
+ for (const argName of requiredArgs) if (isBlankArgument(args[argName])) return `Missing required argument: ${argName}`;
333
+ return null;
334
+ }
335
+ function claudeFacingToolDescription(tool) {
336
+ const baseDescription = KNOWN_PUBLIC_TOOL_DESCRIPTIONS[tool.name] ?? tool.description ?? tool.name;
337
+ const requiredArgs = KNOWN_PUBLIC_TOOL_REQUIRED_ARGS[tool.name];
338
+ if (!requiredArgs) return baseDescription;
339
+ return [
340
+ baseDescription,
341
+ "",
342
+ `Required arguments: ${requiredArgs.join(", ")}.`,
343
+ "If the user did not provide the network, ask for it before calling this tool. Do not guess a default network."
344
+ ].join("\n");
345
+ }
346
+ function promptResult(text, description) {
347
+ return {
348
+ description,
349
+ messages: [{
350
+ role: "user",
351
+ content: {
352
+ type: "text",
353
+ text
354
+ }
355
+ }]
356
+ };
357
+ }
358
+ function compactPromptArguments(args) {
359
+ const compact = {};
360
+ for (const [key, value] of Object.entries(args)) if (typeof value === "string" && value.trim() !== "") compact[key] = value;
361
+ return compact;
362
+ }
363
+ function promptArgumentSchema(promptName, argument) {
364
+ const description = PUBLIC_GRAPHRAG_PROMPT_NAMES.has(promptName) && argument.name === "network" ? NETWORK_DESCRIPTION : argument.description ?? argument.name;
365
+ const schema = zod.string().describe(description);
366
+ if (PUBLIC_GRAPHRAG_PROMPT_NAMES.has(promptName) && argument.name === "network") return schema;
367
+ return argument.required === false ? schema.optional() : schema;
368
+ }
369
+ function registerRemotePrompt(server, remoteClient, prompt) {
370
+ const argsSchema = {};
371
+ for (const argument of prompt.arguments ?? []) argsSchema[argument.name] = promptArgumentSchema(prompt.name, argument);
372
+ server.registerPrompt(prompt.name, {
373
+ title: prompt.title,
374
+ description: prompt.description,
375
+ argsSchema
376
+ }, async (args) => remoteClient.getPrompt({
377
+ name: prompt.name,
378
+ arguments: compactPromptArguments(args)
379
+ }));
380
+ }
381
+ function registerLocalPrompts(server, remotePromptNames) {
382
+ if (!remotePromptNames.has("address-risk")) server.registerPrompt("address-risk", {
383
+ title: "Address Risk",
384
+ description: "Screen an address for AML risk, behavioral patterns, neighborhood profile, and exchange links.",
385
+ argsSchema: {
386
+ address: zod.string().describe("Full blockchain address to screen"),
387
+ network: zod.string().describe(NETWORK_DESCRIPTION)
388
+ }
389
+ }, async ({ address, network }) => promptResult([
390
+ `Use Chain Insights address_risk on ${network} for:`,
391
+ "",
392
+ `\`${address}\``,
393
+ "",
394
+ "Present the summary as-is. Do not add analysis, verdicts, or risk assessments; the tool output already contains the risk assessment."
395
+ ].join("\n"), "Address risk screening"));
396
+ if (!remotePromptNames.has("track-funds")) server.registerPrompt("track-funds", {
397
+ title: "Track Funds",
398
+ description: "Trace stolen funds from victim addresses through intermediaries to exchange deposit addresses.",
399
+ argsSchema: {
400
+ trusted_addresses: zod.string().describe("Victim/trusted addresses, comma-separated full addresses"),
401
+ untrusted_addresses: zod.string().optional().describe("Known scammer/untrusted addresses, comma-separated full addresses"),
402
+ network: zod.string().describe(NETWORK_DESCRIPTION)
403
+ }
404
+ }, async ({ trusted_addresses, untrusted_addresses, network }) => {
405
+ const untrusted = untrusted_addresses?.trim() ? `\nKnown untrusted addresses:\n${untrusted_addresses}\n` : "";
406
+ return promptResult([
407
+ `Use Chain Insights track_funds on ${network}.`,
408
+ "",
409
+ "Trusted victim addresses:",
410
+ trusted_addresses,
411
+ untrusted,
412
+ "Present the summary as-is and include recommended next actions exactly as returned."
413
+ ].join("\n"), "Trace stolen funds");
414
+ });
415
+ server.registerPrompt("graph-query", {
416
+ title: "Federated Graph Query",
417
+ description: "Run a read-only GQL/Cypher query through Chain Insights Memgraph Zero.",
418
+ argsSchema: {
419
+ query: zod.string().describe("Read-only GQL/Cypher query"),
420
+ network: zod.string().describe(NETWORK_DESCRIPTION)
421
+ }
422
+ }, async ({ query, network }) => promptResult([
423
+ `Use Chain Insights graph_query on ${network} with this read-only GQL/Cypher query:`,
424
+ "",
425
+ "```gql",
426
+ query,
427
+ "```",
428
+ "",
429
+ "Use USE live_topology for Memgraph RAM topology, USE archive_topology for StarRocks historical topology, and USE facts for StarRocks facts. Return full address properties; never shorten addresses with ellipses."
430
+ ].join("\n"), "Federated graph query"));
431
+ server.registerPrompt("graph-query-batch", {
432
+ title: "Federated Graph Query Batch",
433
+ description: "Run related read-only GQL/Cypher queries through Chain Insights Memgraph Zero in one paid batch.",
434
+ argsSchema: {
435
+ queries: zod.string().describe("JSON array of query objects with optional id and required query fields"),
436
+ network: zod.string().describe(NETWORK_DESCRIPTION),
437
+ per_query_timeout_seconds: zod.string().optional().describe("Optional integer timeout per query, 1-600 seconds")
438
+ }
439
+ }, async ({ queries, network, per_query_timeout_seconds }) => promptResult([
440
+ `Use Chain Insights graph_query_batch on ${network} with these read-only GQL/Cypher queries:`,
441
+ "",
442
+ "```json",
443
+ queries,
444
+ "```",
445
+ per_query_timeout_seconds ? `per_query_timeout_seconds: ${per_query_timeout_seconds}` : "",
446
+ "",
447
+ "Use USE live_topology for Memgraph RAM topology, USE archive_topology for StarRocks historical topology, and USE facts for StarRocks facts. Return full address properties; never shorten addresses with ellipses."
448
+ ].filter(Boolean).join("\n"), "Federated graph batch query"));
449
+ server.registerPrompt("balance", {
450
+ title: "Wallet Balance",
451
+ description: "Show the local Chain Insights payment wallet address and Base USDC balance.",
452
+ argsSchema: {}
453
+ }, async () => promptResult("Use Chain Insights balance. Show the wallet address, network, token, and balance exactly as returned.", "Wallet balance"));
454
+ server.registerPrompt("help", {
455
+ title: "Chain Insights Help",
456
+ description: "Show available Chain Insights tools and investigation case workflow.",
457
+ argsSchema: {}
458
+ }, async () => promptResult("Use Chain Insights help. Summarize the available tools and investigation case workflow without inventing capabilities.", "Chain Insights help"));
459
+ server.registerPrompt("open-investigation-case", {
460
+ title: "Open Investigation Case",
461
+ description: "Create a local Chain Insights case for an investigation.",
462
+ argsSchema: {
463
+ name: zod.string().describe("Case name"),
464
+ tags: zod.string().optional().describe("Comma-separated tags"),
465
+ description: zod.string().optional().describe("Brief investigation description")
466
+ }
467
+ }, async ({ name, tags, description }) => promptResult([
468
+ "Use Chain Insights case_open to create a local investigation case.",
469
+ "",
470
+ `name: \`${name}\``,
471
+ tags ? `tags: \`${tags}\`` : "",
472
+ description ? `description: ${description}` : ""
473
+ ].filter(Boolean).join("\n"), "Open investigation case"));
474
+ server.registerPrompt("resume-investigation-case", {
475
+ title: "Resume Investigation Case",
476
+ description: "Load local Chain Insights case context, evidence count, dossiers, and latest session.",
477
+ argsSchema: { case_id: zod.string().describe("Chain Insights case ID") }
478
+ }, async ({ case_id }) => promptResult(`Use Chain Insights case_resume for case_id: \`${case_id}\`. Continue from the returned context.`, "Resume investigation case"));
479
+ server.registerPrompt("save-investigation-evidence", {
480
+ title: "Save Investigation Evidence",
481
+ description: "Append a tool result or analyst note to a local Chain Insights case evidence manifest.",
482
+ argsSchema: {
483
+ case_id: zod.string().describe("Chain Insights case ID"),
484
+ source: zod.string().describe("Tool or source name")
485
+ }
486
+ }, async ({ case_id, source }) => promptResult([
487
+ "Use Chain Insights case_add_evidence after the next relevant tool result.",
488
+ "",
489
+ `case_id: \`${case_id}\``,
490
+ `source: \`${source}\``,
491
+ "content: use the exact report or note that should become evidence."
492
+ ].join("\n"), "Save investigation evidence"));
493
+ }
494
+ function hasGraphArrayFields(value) {
495
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
496
+ const record = value;
497
+ return GRAPH_ARRAY_KEYS.some((key) => Array.isArray(record[key]));
498
+ }
499
+ function sanitizeStructuredContentForGraphPayload(structuredContent) {
500
+ if (!structuredContent) return void 0;
501
+ return sanitizeStructuredValue(structuredContent);
502
+ }
503
+ function sanitizeStructuredValue(value) {
504
+ if (!value || typeof value !== "object" || Array.isArray(value)) return value;
505
+ const sanitized = {};
506
+ for (const [key, childValue] of Object.entries(value)) {
507
+ if (key === "app_data") continue;
508
+ if (GRAPH_ARRAY_KEYS.includes(key) && Array.isArray(childValue)) continue;
509
+ sanitized[key] = sanitizeStructuredValue(childValue);
510
+ }
511
+ return sanitized;
512
+ }
513
+ function getRemoteGraphPayload(result) {
514
+ const chainInsights = result._meta?.chainInsights;
515
+ if (!chainInsights || typeof chainInsights !== "object" || Array.isArray(chainInsights)) return null;
516
+ const graph = chainInsights.graph;
517
+ if (graph === void 0) return null;
518
+ if (!graph || typeof graph !== "object" || Array.isArray(graph)) throw new Error("Invalid remote graph payload");
519
+ const graphRecord = graph;
520
+ if (!("data" in graphRecord)) {
521
+ if ("url" in graphRecord || hasGraphArrayFields(graphRecord)) throw new Error("Invalid remote graph payload");
522
+ return null;
523
+ }
524
+ const data = graphRecord.data;
525
+ if (!data || typeof data !== "object" || Array.isArray(data)) throw new Error("Invalid remote graph payload");
526
+ return data;
527
+ }
528
+ async function normalizeRemoteToolResult(result, config, toolName = "remote-graph") {
529
+ const graphPayload = getRemoteGraphPayload(result);
530
+ const meta = { ...result._meta ?? {} };
531
+ if (graphPayload) {
532
+ const { writeGraphReport } = await Promise.resolve().then(() => require("./graph-reports-DU05YCei.cjs"));
533
+ const { ensureArtifactServer } = await Promise.resolve().then(() => require("./artifact-server-DoxJ7fCx.cjs"));
534
+ const report = await writeGraphReport(graphPayload, {
535
+ serverPort: config.serverPort,
536
+ slug: toolName || "remote-graph"
537
+ });
538
+ await ensureArtifactServer(config.serverPort);
539
+ meta.chainInsights = {
540
+ ...meta.chainInsights ?? {},
541
+ graph: {
542
+ schema: report.schema,
543
+ url: report.url
544
+ }
545
+ };
546
+ }
547
+ return {
548
+ content: result.content ?? [],
549
+ structuredContent: sanitizeStructuredContentForGraphPayload(result.structuredContent),
550
+ _meta: Object.keys(meta).length > 0 ? meta : void 0,
551
+ isError: result.isError
552
+ };
553
+ }
554
+ /**
555
+ * Core proxy logic — exported so tests can inject dependencies directly.
556
+ * The IIFE at the bottom calls this with real dependencies.
557
+ *
558
+ * stdout purity: NEVER write to stdout in this file. Use console.error() or process.stderr.write() only.
559
+ * All diagnostic output goes to console.error() or process.stderr.write().
560
+ */
561
+ async function createProxy() {
562
+ const { loadConfig } = await Promise.resolve().then(() => require("./config-Bmdl5hdk.cjs")).then((n) => n.config_exports);
563
+ const { activeDataDir, findActiveWorkspace } = await Promise.resolve().then(() => require("./active-Dv7Tu-O4.cjs")).then((n) => n.active_exports);
564
+ const { createConfiguredGraphMcpFetch, resolveGraphMcpEndpoint } = await Promise.resolve().then(() => require("./client-D4fZgIaO.cjs")).then((n) => n.client_exports);
565
+ const { loadSchema, saveSchema } = await Promise.resolve().then(() => require("./schema-cache-CgWRCN2N.cjs"));
566
+ const loadedConfig = await loadConfig();
567
+ const activeWorkspace = findActiveWorkspace();
568
+ const config = {
569
+ ...loadedConfig,
570
+ dataDir: activeDataDir(loadedConfig.dataDir)
571
+ };
572
+ const logger = createMcpLogger(config);
573
+ await logger.info("proxy.start", {
574
+ data_dir: config.dataDir,
575
+ workspace_root: activeWorkspace?.root,
576
+ graph_mcp_mode: config.graphMcpMode,
577
+ graph_mcp_endpoint: resolveGraphMcpEndpoint(config),
578
+ log_path: logger.filePath
579
+ });
580
+ const mcpFetch = await createConfiguredGraphMcpFetch(config);
581
+ const graphMcpEndpoint = resolveGraphMcpEndpoint(config);
582
+ const remoteClient = new _modelcontextprotocol_sdk_client_index_js.Client({
583
+ name: "chain-insights-proxy-client",
584
+ version: require_version.PACKAGE_VERSION
585
+ });
586
+ let remoteConnected = false;
587
+ let remoteUnavailableMessage;
588
+ try {
589
+ await remoteClient.connect(new _modelcontextprotocol_sdk_client_streamableHttp_js.StreamableHTTPClientTransport(new URL(graphMcpEndpoint), { fetch: mcpFetch }));
590
+ remoteConnected = true;
591
+ await logger.info("remote.connect", {
592
+ transport: "streamable_http",
593
+ endpoint: graphMcpEndpoint
594
+ });
595
+ } catch {
596
+ await logger.error("remote.connect_failed", {
597
+ transport: "streamable_http",
598
+ endpoint: graphMcpEndpoint
599
+ });
600
+ try {
601
+ const { SSEClientTransport } = await import("@modelcontextprotocol/sdk/client/sse.js");
602
+ await remoteClient.connect(new SSEClientTransport(new URL(graphMcpEndpoint), { fetch: mcpFetch }));
603
+ remoteConnected = true;
604
+ await logger.info("remote.connect", {
605
+ transport: "sse",
606
+ endpoint: graphMcpEndpoint
607
+ });
608
+ } catch (err2) {
609
+ await logger.error("remote.connect_failed", {
610
+ transport: "sse",
611
+ endpoint: graphMcpEndpoint,
612
+ error: errorForLog(err2)
613
+ });
614
+ remoteUnavailableMessage = `Graph MCP unreachable at ${graphMcpEndpoint}: ${err2.message}`;
615
+ process.stderr.write(`Chain Insights MCP graph tools unavailable: ${remoteUnavailableMessage}. Local Chain Insights tools are still available.\n`);
616
+ }
617
+ }
618
+ if (remoteConnected) installRemoteCypherLogging(remoteClient, logger);
619
+ let tools = await loadSchema(graphMcpEndpoint);
620
+ if (!tools && remoteConnected) {
621
+ tools = (await remoteClient.listTools()).tools;
622
+ await saveSchema(tools, graphMcpEndpoint);
623
+ await logger.info("schema.tools_loaded", {
624
+ source: "remote",
625
+ count: tools.length
626
+ });
627
+ } else if (tools) await logger.info("schema.tools_loaded", {
628
+ source: "cache",
629
+ count: tools.length
630
+ });
631
+ else {
632
+ tools = [];
633
+ await logger.info("schema.tools_loaded", {
634
+ source: "unavailable",
635
+ count: 0
636
+ });
637
+ }
638
+ const remoteToolNames = new Set((tools ?? []).map((tool) => tool.name));
639
+ const server = new _modelcontextprotocol_sdk_server_mcp_js.McpServer({
640
+ name: "chain-insights",
641
+ version: require_version.PACKAGE_VERSION
642
+ }, { instructions: SERVER_INSTRUCTIONS });
643
+ installToolLogging(server, logger);
644
+ const remotePrompts = [];
645
+ if (remoteConnected) try {
646
+ const promptResult = await remoteClient.listPrompts();
647
+ for (const prompt of promptResult.prompts) if (PUBLIC_GRAPHRAG_PROMPT_NAMES.has(prompt.name)) remotePrompts.push(prompt);
648
+ } catch (err) {
649
+ await logger.error("remote.prompts_failed", {
650
+ endpoint: graphMcpEndpoint,
651
+ error: errorForLog(err)
652
+ });
653
+ process.stderr.write(`Chain Insights MCP prompt passthrough unavailable at ${graphMcpEndpoint}: ${err.message}\n`);
654
+ }
655
+ const remotePromptNames = new Set(remotePrompts.map((prompt) => prompt.name));
656
+ for (const prompt of remotePrompts) registerRemotePrompt(server, remoteClient, prompt);
657
+ registerLocalPrompts(server, remotePromptNames);
658
+ const caseToolError = (label, err) => ({
659
+ content: [{
660
+ type: "text",
661
+ text: `${label} failed: ${err.message}`
662
+ }],
663
+ isError: true
664
+ });
665
+ const parseTags = (tags) => {
666
+ if (Array.isArray(tags)) return tags.map((tag) => tag.trim()).filter(Boolean);
667
+ if (typeof tags === "string") return tags.split(",").map((tag) => tag.trim()).filter(Boolean);
668
+ return [];
669
+ };
670
+ server.registerTool("balance", {
671
+ description: "Show the local Chain Insights payment wallet address and Base USDC balance.",
672
+ inputSchema: zod.object({}).passthrough()
673
+ }, async () => {
674
+ try {
675
+ const { getWalletAccount, getWalletBalanceText } = await Promise.resolve().then(() => require("./tools-f_vJUZAF.cjs")).then((n) => n.tools_exports);
676
+ return {
677
+ content: [{
678
+ type: "text",
679
+ text: await getWalletBalanceText(await getWalletAccount())
680
+ }],
681
+ isError: false
682
+ };
683
+ } catch (err) {
684
+ return {
685
+ content: [{
686
+ type: "text",
687
+ text: `Balance failed: ${err.message}`
688
+ }],
689
+ isError: true
690
+ };
691
+ }
692
+ });
693
+ (0, _modelcontextprotocol_ext_apps_server.registerAppResource)(server, "Fund Flow Graph", GRAPH_RESOURCE_URI, {
694
+ description: "Interactive D3 force-directed graph for fund flow and pattern visualization. It loads local graph report URLs returned in _meta.chainInsights.graph.url.",
695
+ _meta: { ui: { csp: {
696
+ resourceDomains: graphArtifactOrigins(config),
697
+ connectDomains: graphArtifactOrigins(config)
698
+ } } }
699
+ }, async () => ({ contents: [{
700
+ uri: GRAPH_RESOURCE_URI,
701
+ mimeType: _modelcontextprotocol_ext_apps_server.RESOURCE_MIME_TYPE,
702
+ text: readGraphAppHtml(),
703
+ _meta: { ui: { csp: {
704
+ resourceDomains: graphArtifactOrigins(config),
705
+ connectDomains: graphArtifactOrigins(config)
706
+ } } }
707
+ }] }));
708
+ server.registerTool("case_open", {
709
+ description: "Create a local Chain Insights investigation case. Use this before saving evidence, dossiers, or session notes for a new investigation.",
710
+ inputSchema: {
711
+ name: zod.string().min(1).describe("Case name"),
712
+ tags: zod.union([zod.string(), zod.array(zod.string())]).optional().describe("Comma-separated tags or string array"),
713
+ description: zod.string().optional().describe("Brief investigation description")
714
+ },
715
+ annotations: {
716
+ readOnlyHint: false,
717
+ destructiveHint: false,
718
+ idempotentHint: false,
719
+ openWorldHint: false
720
+ }
721
+ }, async ({ name, tags, description }) => {
722
+ try {
723
+ const { CaseStore } = await Promise.resolve().then(() => require("./cases-CDcNU91B.cjs"));
724
+ const created = await CaseStore.create({
725
+ name,
726
+ tags: parseTags(tags),
727
+ description: description ?? ""
728
+ });
729
+ const { casesRoot } = await Promise.resolve().then(() => require("./store-BiUhQOIf.cjs"));
730
+ return {
731
+ content: [{
732
+ type: "text",
733
+ text: JSON.stringify({
734
+ case_id: created.id,
735
+ name: created.name,
736
+ status: created.status,
737
+ tags: created.tags,
738
+ directory: `${node_path.default.join(casesRoot(), created.id)}/`
739
+ }, null, 2)
740
+ }],
741
+ isError: false
742
+ };
743
+ } catch (err) {
744
+ return caseToolError("Case open", err);
745
+ }
746
+ });
747
+ server.registerTool("case_list", {
748
+ description: "List local Chain Insights investigation cases. Use before resuming when the user does not provide a case ID.",
749
+ inputSchema: { status: zod.enum([
750
+ "open",
751
+ "active",
752
+ "suspended",
753
+ "closed"
754
+ ]).optional().describe("Optional status filter") },
755
+ annotations: {
756
+ readOnlyHint: true,
757
+ destructiveHint: false,
758
+ idempotentHint: true,
759
+ openWorldHint: false
760
+ }
761
+ }, async ({ status }) => {
762
+ try {
763
+ const { CaseStore } = await Promise.resolve().then(() => require("./cases-CDcNU91B.cjs"));
764
+ const cases = await CaseStore.list();
765
+ const filtered = status ? cases.filter((entry) => entry.status === status) : cases;
766
+ return {
767
+ content: [{
768
+ type: "text",
769
+ text: JSON.stringify({ cases: filtered }, null, 2)
770
+ }],
771
+ isError: false
772
+ };
773
+ } catch (err) {
774
+ return caseToolError("Case list", err);
775
+ }
776
+ });
777
+ server.registerTool("case_resume", {
778
+ description: "Load local Chain Insights case context: metadata, evidence count, dossier summaries, and latest session notes.",
779
+ inputSchema: { case_id: zod.string().min(1).describe("Chain Insights case ID") },
780
+ annotations: {
781
+ readOnlyHint: true,
782
+ destructiveHint: false,
783
+ idempotentHint: true,
784
+ openWorldHint: false
785
+ }
786
+ }, async ({ case_id }) => {
787
+ try {
788
+ const { CaseStore } = await Promise.resolve().then(() => require("./cases-CDcNU91B.cjs"));
789
+ const context = await CaseStore.loadContext(case_id);
790
+ return {
791
+ content: [{
792
+ type: "text",
793
+ text: JSON.stringify(context, null, 2)
794
+ }],
795
+ isError: false
796
+ };
797
+ } catch (err) {
798
+ return caseToolError("Case resume", err);
799
+ }
800
+ });
801
+ server.registerTool("case_add_evidence", {
802
+ description: "Append a tool result or analyst note to a local case evidence manifest. Use after address_risk, track_funds, graph_query, or manual findings that should be preserved.",
803
+ inputSchema: {
804
+ case_id: zod.string().min(1).describe("Chain Insights case ID"),
805
+ source: zod.string().min(1).describe("Source tool or evidence origin"),
806
+ content: zod.string().min(1).describe("Evidence markdown/text to store"),
807
+ query_params: zod.string().optional().describe("Original query parameters, for example \"network=bittensor address=...\"")
808
+ },
809
+ annotations: {
810
+ readOnlyHint: false,
811
+ destructiveHint: false,
812
+ idempotentHint: false,
813
+ openWorldHint: false
814
+ }
815
+ }, async ({ case_id, source, content, query_params }) => {
816
+ try {
817
+ const { EvidenceStore } = await Promise.resolve().then(() => require("./cases-CDcNU91B.cjs"));
818
+ const saved = await EvidenceStore.append(case_id, {
819
+ source,
820
+ content,
821
+ queryParams: query_params ?? ""
822
+ });
823
+ return {
824
+ content: [{
825
+ type: "text",
826
+ text: JSON.stringify(saved, null, 2)
827
+ }],
828
+ isError: false
829
+ };
830
+ } catch (err) {
831
+ return caseToolError("Evidence append", err);
832
+ }
833
+ });
834
+ server.registerTool("case_verify_evidence", {
835
+ description: "Verify a local case evidence manifest and report tampered or missing evidence files.",
836
+ inputSchema: { case_id: zod.string().min(1).describe("Chain Insights case ID") },
837
+ annotations: {
838
+ readOnlyHint: true,
839
+ destructiveHint: false,
840
+ idempotentHint: true,
841
+ openWorldHint: false
842
+ }
843
+ }, async ({ case_id }) => {
844
+ try {
845
+ const { EvidenceStore } = await Promise.resolve().then(() => require("./cases-CDcNU91B.cjs"));
846
+ const result = await EvidenceStore.verifyManifest(case_id);
847
+ return {
848
+ content: [{
849
+ type: "text",
850
+ text: JSON.stringify(result, null, 2)
851
+ }],
852
+ isError: false
853
+ };
854
+ } catch (err) {
855
+ return caseToolError("Evidence verify", err);
856
+ }
857
+ });
858
+ server.registerTool("case_update_dossier", {
859
+ description: "Append a finding to an address/entity dossier inside a local Chain Insights case.",
860
+ inputSchema: {
861
+ case_id: zod.string().min(1).describe("Chain Insights case ID"),
862
+ address: zod.string().min(1).describe("Full address or entity identifier"),
863
+ finding: zod.string().min(1).describe("Finding to append"),
864
+ entity_type: zod.enum([
865
+ "eoa",
866
+ "contract",
867
+ "exchange",
868
+ "mixer",
869
+ "unknown"
870
+ ]).optional().describe("Entity type")
871
+ },
872
+ annotations: {
873
+ readOnlyHint: false,
874
+ destructiveHint: false,
875
+ idempotentHint: false,
876
+ openWorldHint: false
877
+ }
878
+ }, async ({ case_id, address, finding, entity_type }) => {
879
+ try {
880
+ const { DossierStore } = await Promise.resolve().then(() => require("./cases-CDcNU91B.cjs"));
881
+ await DossierStore.appendFinding(case_id, address, finding, entity_type ?? "unknown");
882
+ return {
883
+ content: [{
884
+ type: "text",
885
+ text: JSON.stringify({
886
+ case_id,
887
+ address,
888
+ updated: true
889
+ }, null, 2)
890
+ }],
891
+ isError: false
892
+ };
893
+ } catch (err) {
894
+ return caseToolError("Dossier update", err);
895
+ }
896
+ });
897
+ server.registerTool("case_start_session", {
898
+ description: "Start a local investigation session file for a Chain Insights case.",
899
+ inputSchema: { case_id: zod.string().min(1).describe("Chain Insights case ID") },
900
+ annotations: {
901
+ readOnlyHint: false,
902
+ destructiveHint: false,
903
+ idempotentHint: false,
904
+ openWorldHint: false
905
+ }
906
+ }, async ({ case_id }) => {
907
+ try {
908
+ const { SessionStore } = await Promise.resolve().then(() => require("./cases-CDcNU91B.cjs"));
909
+ const session = await SessionStore.start(case_id);
910
+ return {
911
+ content: [{
912
+ type: "text",
913
+ text: JSON.stringify(session, null, 2)
914
+ }],
915
+ isError: false
916
+ };
917
+ } catch (err) {
918
+ return caseToolError("Session start", err);
919
+ }
920
+ });
921
+ server.registerTool("case_end_session", {
922
+ description: "End the latest local investigation session for a Chain Insights case with findings and next steps.",
923
+ inputSchema: {
924
+ case_id: zod.string().min(1).describe("Chain Insights case ID"),
925
+ findings: zod.string().optional().describe("Key findings from this session"),
926
+ next_steps: zod.string().optional().describe("Next investigation steps")
927
+ },
928
+ annotations: {
929
+ readOnlyHint: false,
930
+ destructiveHint: false,
931
+ idempotentHint: false,
932
+ openWorldHint: false
933
+ }
934
+ }, async ({ case_id, findings, next_steps }) => {
935
+ try {
936
+ const { SessionStore } = await Promise.resolve().then(() => require("./cases-CDcNU91B.cjs"));
937
+ await SessionStore.end(case_id, {
938
+ findings: findings ?? "",
939
+ nextSteps: next_steps ?? ""
940
+ });
941
+ await SessionStore.archiveOldSessions(case_id);
942
+ return {
943
+ content: [{
944
+ type: "text",
945
+ text: JSON.stringify({
946
+ case_id,
947
+ ended: true
948
+ }, null, 2)
949
+ }],
950
+ isError: false
951
+ };
952
+ } catch (err) {
953
+ return caseToolError("Session end", err);
954
+ }
955
+ });
956
+ if (!remoteToolNames.has("address_risk")) (0, _modelcontextprotocol_ext_apps_server.registerAppTool)(server, "address_risk", {
957
+ title: "Address Risk",
958
+ description: KNOWN_PUBLIC_TOOL_DESCRIPTIONS.address_risk,
959
+ inputSchema: {
960
+ address: zod.string().min(1).describe("Full blockchain address to screen"),
961
+ network: zod.string().min(1).describe(NETWORK_DESCRIPTION),
962
+ compare_address: zod.string().optional().describe("Optional second full address for comparison"),
963
+ include_attachments: zod.boolean().optional().describe("Include graph app report metadata")
964
+ },
965
+ _meta: { ui: { resourceUri: GRAPH_RESOURCE_URI } },
966
+ annotations: {
967
+ readOnlyHint: true,
968
+ destructiveHint: false,
969
+ idempotentHint: true,
970
+ openWorldHint: true
971
+ }
972
+ }, async ({ address, network, compare_address }) => {
973
+ try {
974
+ if (!remoteConnected) return {
975
+ content: [{
976
+ type: "text",
977
+ text: `${remoteUnavailableMessage ?? `Graph MCP is not connected at ${graphMcpEndpoint}`}. Restart the Chain Insights MCP proxy after the endpoint is reachable.`
978
+ }],
979
+ isError: true
980
+ };
981
+ const { addressRisk } = await Promise.resolve().then(() => require("./public-tools-XSpkz2ky.cjs"));
982
+ const { writeGraphReport } = await Promise.resolve().then(() => require("./graph-reports-DU05YCei.cjs"));
983
+ const { ensureArtifactServer } = await Promise.resolve().then(() => require("./artifact-server-DoxJ7fCx.cjs"));
984
+ const result = await addressRisk(remoteClient, {
985
+ address,
986
+ network,
987
+ compareAddress: compare_address
988
+ });
989
+ const report = await writeGraphReport(result.graphData, {
990
+ serverPort: config.serverPort,
991
+ slug: `address-risk-${network}-${address}`
992
+ });
993
+ await ensureArtifactServer(config.serverPort);
994
+ return {
995
+ content: [{
996
+ type: "text",
997
+ text: result.summaryText
998
+ }],
999
+ structuredContent: result.structuredContent,
1000
+ _meta: { chainInsights: { graph: {
1001
+ schema: report.schema,
1002
+ url: report.url
1003
+ } } },
1004
+ isError: false
1005
+ };
1006
+ } catch (err) {
1007
+ return {
1008
+ content: [{
1009
+ type: "text",
1010
+ text: `Address risk failed: ${err.message}`
1011
+ }],
1012
+ isError: true
1013
+ };
1014
+ }
1015
+ });
1016
+ if (!remoteToolNames.has("track_funds")) (0, _modelcontextprotocol_ext_apps_server.registerAppTool)(server, "track_funds", {
1017
+ title: "Track Funds",
1018
+ description: KNOWN_PUBLIC_TOOL_DESCRIPTIONS.track_funds,
1019
+ inputSchema: {
1020
+ trusted_addresses: zod.union([zod.string().min(1), zod.array(zod.string().min(1))]).describe("Comma-separated full trusted victim addresses, or an array. Min 1, max 5."),
1021
+ network: zod.string().min(1).describe(NETWORK_DESCRIPTION),
1022
+ untrusted_addresses: zod.union([zod.string(), zod.array(zod.string())]).optional().describe("Known scammer/untrusted addresses. Max 5."),
1023
+ include_attachments: zod.boolean().optional().describe("Include graph app report metadata"),
1024
+ case_id: zod.string().optional().describe("Optional Chain Insights case ID. When provided, compact evidence is appended to the case manifest."),
1025
+ max_hops: zod.number().int().min(1).max(5).optional(),
1026
+ per_address_limit: zod.number().int().min(1).max(10).optional(),
1027
+ min_amount_sum: zod.number().min(0).optional()
1028
+ },
1029
+ _meta: { ui: { resourceUri: GRAPH_RESOURCE_URI } },
1030
+ annotations: {
1031
+ readOnlyHint: false,
1032
+ destructiveHint: false,
1033
+ idempotentHint: false,
1034
+ openWorldHint: true
1035
+ }
1036
+ }, async ({ trusted_addresses, untrusted_addresses, network, case_id, max_hops, per_address_limit, min_amount_sum }) => {
1037
+ try {
1038
+ if (!remoteConnected) return {
1039
+ content: [{
1040
+ type: "text",
1041
+ text: `${remoteUnavailableMessage ?? `Graph MCP is not connected at ${graphMcpEndpoint}`}. Restart the Chain Insights MCP proxy after the endpoint is reachable.`
1042
+ }],
1043
+ isError: true
1044
+ };
1045
+ const { trackFunds } = await Promise.resolve().then(() => require("./public-tools-XSpkz2ky.cjs"));
1046
+ const { writeGraphReport } = await Promise.resolve().then(() => require("./graph-reports-DU05YCei.cjs"));
1047
+ const { ensureArtifactServer } = await Promise.resolve().then(() => require("./artifact-server-DoxJ7fCx.cjs"));
1048
+ const result = await trackFunds(remoteClient, config, {
1049
+ trustedAddresses: trusted_addresses,
1050
+ untrustedAddresses: untrusted_addresses,
1051
+ network,
1052
+ caseId: case_id,
1053
+ maxHops: max_hops,
1054
+ perAddressLimit: per_address_limit,
1055
+ minAmountSum: min_amount_sum
1056
+ });
1057
+ const report = await writeGraphReport(result.graphData, {
1058
+ serverPort: config.serverPort,
1059
+ slug: `track-funds-${network}`
1060
+ });
1061
+ await ensureArtifactServer(config.serverPort);
1062
+ return {
1063
+ content: [{
1064
+ type: "text",
1065
+ text: result.summaryText
1066
+ }],
1067
+ structuredContent: result.structuredContent,
1068
+ _meta: { chainInsights: { graph: {
1069
+ schema: report.schema,
1070
+ url: report.url
1071
+ } } },
1072
+ isError: false
1073
+ };
1074
+ } catch (err) {
1075
+ return {
1076
+ content: [{
1077
+ type: "text",
1078
+ text: `Track funds failed: ${err.message}`
1079
+ }],
1080
+ isError: true
1081
+ };
1082
+ }
1083
+ });
1084
+ if (!remoteToolNames.has("scam_topology")) (0, _modelcontextprotocol_ext_apps_server.registerAppTool)(server, "scam_topology", {
1085
+ title: "Scam Topology",
1086
+ description: KNOWN_PUBLIC_TOOL_DESCRIPTIONS.scam_topology,
1087
+ inputSchema: {
1088
+ network: zod.string().min(1).describe(NETWORK_DESCRIPTION),
1089
+ victim_address: zod.string().min(1).describe("Full victim/source address that anchors the scam incident. Victims are not risky labels."),
1090
+ incident_timestamp_ms: zod.number().min(0).describe("Earliest known incident transfer timestamp in milliseconds. Primary traversal uses node-relative wave-arrival filtering."),
1091
+ max_hops: zod.number().int().min(1).max(64).optional().describe("Maximum forward expansion depth. Default 16."),
1092
+ activity_policy: zod.enum(["node_relative_only", "global_incident_only"]).optional().describe("Traversal activity policy. Default node_relative_only."),
1093
+ case_id: zod.string().optional().describe("Optional Chain Insights case ID. When provided, compact evidence is appended to the case manifest.")
1094
+ },
1095
+ _meta: { ui: { resourceUri: GRAPH_RESOURCE_URI } },
1096
+ annotations: {
1097
+ readOnlyHint: false,
1098
+ destructiveHint: false,
1099
+ idempotentHint: false,
1100
+ openWorldHint: true
1101
+ }
1102
+ }, async ({ victim_address, incident_timestamp_ms, network, max_hops, activity_policy, case_id }) => {
1103
+ try {
1104
+ if (!remoteConnected) return {
1105
+ content: [{
1106
+ type: "text",
1107
+ text: `${remoteUnavailableMessage ?? `Graph MCP is not connected at ${graphMcpEndpoint}`}. Restart the Chain Insights MCP proxy after the endpoint is reachable.`
1108
+ }],
1109
+ isError: true
1110
+ };
1111
+ const { scamTopology } = await Promise.resolve().then(() => require("./public-tools-XSpkz2ky.cjs"));
1112
+ const { writeGraphReport } = await Promise.resolve().then(() => require("./graph-reports-DU05YCei.cjs"));
1113
+ const { ensureArtifactServer } = await Promise.resolve().then(() => require("./artifact-server-DoxJ7fCx.cjs"));
1114
+ const result = await scamTopology(remoteClient, config, {
1115
+ victimAddress: victim_address,
1116
+ network,
1117
+ maxHops: max_hops,
1118
+ incidentTimestampMs: incident_timestamp_ms,
1119
+ activityPolicyMode: activity_policy,
1120
+ caseId: case_id
1121
+ });
1122
+ const report = await writeGraphReport(result.graphData, {
1123
+ serverPort: config.serverPort,
1124
+ slug: `scam-topology-${network}`
1125
+ });
1126
+ await ensureArtifactServer(config.serverPort);
1127
+ return {
1128
+ content: [{
1129
+ type: "text",
1130
+ text: result.summaryText
1131
+ }],
1132
+ structuredContent: result.structuredContent,
1133
+ _meta: { chainInsights: { graph: {
1134
+ schema: report.schema,
1135
+ url: report.url
1136
+ } } },
1137
+ isError: false
1138
+ };
1139
+ } catch (err) {
1140
+ return {
1141
+ content: [{
1142
+ type: "text",
1143
+ text: `Scam topology failed: ${err.message}`
1144
+ }],
1145
+ isError: true
1146
+ };
1147
+ }
1148
+ });
1149
+ server.registerTool("help", {
1150
+ description: "Show Chain Insights overview, available tools, and investigation workflow.",
1151
+ inputSchema: zod.object({}).passthrough()
1152
+ }, async () => ({
1153
+ content: [{
1154
+ type: "text",
1155
+ text: [
1156
+ "Chain Insights AML investigation workspace for AI agents.",
1157
+ "",
1158
+ CHAIN_INSIGHTS_WORKFLOW,
1159
+ "",
1160
+ "Investigation tools:",
1161
+ "- network_capabilities: inspect supported networks, data layers, tool availability, retention windows, and freshness.",
1162
+ "- address_risk: screen a full address for AML risk, behavior, neighborhood, exchange exposure, and optional compare_address connection checks.",
1163
+ "- track_funds: trace up to five trusted/victim addresses plus up to five known untrusted/scammer addresses through intermediaries to exchange deposit addresses.",
1164
+ "- scam_topology: derive ML-ready scam_labels from one victim incident address and incident_timestamp_ms.",
1165
+ "- graph_query: run read-only GQL/Cypher through the universal graph endpoint. Use USE live_topology, USE archive_topology, or USE facts.",
1166
+ "- graph_query_batch: run related read-only graph-language queries through one paid graph call.",
1167
+ "",
1168
+ "Case workflow tools:",
1169
+ "- case_open: create a local case before preserving evidence.",
1170
+ "- case_list: list local cases.",
1171
+ "- case_resume: load case context, evidence count, dossiers, and latest session.",
1172
+ "- case_add_evidence: append a report or note to the case evidence manifest.",
1173
+ "- case_verify_evidence: verify saved evidence integrity.",
1174
+ "- case_update_dossier: add a finding to an address/entity dossier.",
1175
+ "- case_start_session and case_end_session: record session notes.",
1176
+ "",
1177
+ "Wallet tools:",
1178
+ "- balance: show the local payment wallet address and Base USDC balance.",
1179
+ "- help: show this overview.",
1180
+ "",
1181
+ GRAPH_REPORT_HINTS,
1182
+ "",
1183
+ GRAPH_SCHEMA_HINTS
1184
+ ].join("\n")
1185
+ }],
1186
+ isError: false
1187
+ }));
1188
+ for (const tool of tools ?? []) {
1189
+ if (require_tool_visibility.HIDDEN_REMOTE_TOOL_NAMES.has(tool.name)) continue;
1190
+ if (LOCAL_TOOL_NAMES.has(tool.name)) continue;
1191
+ const inputSchema = knownPublicToolInputSchema(tool.name) ?? zod.object({}).passthrough();
1192
+ const handler = async (args) => {
1193
+ try {
1194
+ if (!remoteConnected) return {
1195
+ content: [{
1196
+ type: "text",
1197
+ text: `${remoteUnavailableMessage ?? `Graph MCP is not connected at ${graphMcpEndpoint}`}. Restart the Chain Insights MCP proxy after the endpoint is reachable.`
1198
+ }],
1199
+ isError: true
1200
+ };
1201
+ const normalizedArgs = normalizeRemoteToolArguments(tool.name, args);
1202
+ const validationError = validateKnownPublicToolArguments(tool.name, normalizedArgs);
1203
+ if (validationError) return {
1204
+ content: [{
1205
+ type: "text",
1206
+ text: validationError
1207
+ }],
1208
+ isError: true
1209
+ };
1210
+ const request = {
1211
+ name: tool.name,
1212
+ arguments: normalizedArgs
1213
+ };
1214
+ const requestOptions = remoteToolRequestOptions(tool.name);
1215
+ return await normalizeRemoteToolResult(requestOptions ? await remoteClient.callTool(request, void 0, requestOptions) : await remoteClient.callTool(request), config, tool.name);
1216
+ } catch (err) {
1217
+ return {
1218
+ content: [{
1219
+ type: "text",
1220
+ text: `MCP call failed: ${err.message}`
1221
+ }],
1222
+ isError: true
1223
+ };
1224
+ }
1225
+ };
1226
+ const toolConfig = {
1227
+ title: tool.title,
1228
+ description: claudeFacingToolDescription(tool),
1229
+ inputSchema
1230
+ };
1231
+ if (hasGraphApp(tool)) (0, _modelcontextprotocol_ext_apps_server.registerAppTool)(server, tool.name, {
1232
+ ...toolConfig,
1233
+ _meta: graphToolMeta(tool)
1234
+ }, handler);
1235
+ else server.registerTool(tool.name, toolConfig, handler);
1236
+ }
1237
+ const transport = new _modelcontextprotocol_sdk_server_stdio_js.StdioServerTransport();
1238
+ await server.connect(transport);
1239
+ await logger.info("proxy.ready", { tools: [...LOCAL_TOOL_NAMES, ...(tools ?? []).map((tool) => tool.name).filter((name) => !require_tool_visibility.HIDDEN_REMOTE_TOOL_NAMES.has(name) && !LOCAL_TOOL_NAMES.has(name))].length });
1240
+ const shutdown = async () => {
1241
+ await logger.info("proxy.shutdown");
1242
+ transport.close();
1243
+ process.exit(0);
1244
+ };
1245
+ process.on("SIGINT", () => {
1246
+ shutdown();
1247
+ });
1248
+ process.on("SIGTERM", () => {
1249
+ shutdown();
1250
+ });
1251
+ }
1252
+ if (process.argv[1] && require("url").pathToFileURL(__filename).href.includes(process.argv[1].replace(/\\/g, "/"))) createProxy().catch((err) => {
1253
+ process.stderr.write(`Chain Insights MCP proxy startup failed: ${err.message}\n`);
1254
+ process.exit(1);
1255
+ });
1256
+ //#endregion
1257
+ exports.createProxy = createProxy;