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.
- package/LICENSE +21 -0
- package/README.md +165 -0
- package/bin/cli.js +10 -0
- package/bin/install.cjs +252 -0
- package/bin/mcp-proxy.cjs +10 -0
- package/dist/active-BSrxLKwn.mjs +50 -0
- package/dist/active-BSrxLKwn.mjs.map +1 -0
- package/dist/active-Dv7Tu-O4.cjs +68 -0
- package/dist/app-BjjuQM0B.mjs +155 -0
- package/dist/app-BjjuQM0B.mjs.map +1 -0
- package/dist/app-Dq1TdB6p.cjs +161 -0
- package/dist/artifact-server-DoxJ7fCx.cjs +47 -0
- package/dist/artifact-server-Dxz5YbuQ.mjs +48 -0
- package/dist/artifact-server-Dxz5YbuQ.mjs.map +1 -0
- package/dist/assets/bg-pattern.png +0 -0
- package/dist/assets/logo.png +0 -0
- package/dist/call-args-DQA2QcRA.cjs +27 -0
- package/dist/call-args-Lk_wOJxd.mjs +29 -0
- package/dist/call-args-Lk_wOJxd.mjs.map +1 -0
- package/dist/capabilities-CB97WMA5.cjs +83 -0
- package/dist/capabilities-DliMBim-.mjs +84 -0
- package/dist/capabilities-DliMBim-.mjs.map +1 -0
- package/dist/cases-By7INiOa.mjs +6 -0
- package/dist/cases-CDcNU91B.cjs +9 -0
- package/dist/chunk-CZWwpsFl.cjs +43 -0
- package/dist/cli.cjs +752 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +753 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/client-D4Bq0rp9.mjs +111 -0
- package/dist/client-D4Bq0rp9.mjs.map +1 -0
- package/dist/client-D4fZgIaO.cjs +132 -0
- package/dist/config-Bmdl5hdk.cjs +67 -0
- package/dist/config-BwrBYmiC.mjs +44 -0
- package/dist/config-BwrBYmiC.mjs.map +1 -0
- package/dist/data-extractor-BNGj7ECT.cjs +347 -0
- package/dist/data-extractor-DFzsa5CS.mjs +336 -0
- package/dist/data-extractor-DFzsa5CS.mjs.map +1 -0
- package/dist/dossier-BsroDgD3.mjs +76 -0
- package/dist/dossier-BsroDgD3.mjs.map +1 -0
- package/dist/dossier-DtxREpPm.cjs +76 -0
- package/dist/evidence-BGcdKxuV.cjs +200 -0
- package/dist/evidence-BhvFW-y_.mjs +195 -0
- package/dist/evidence-BhvFW-y_.mjs.map +1 -0
- package/dist/format-Ce1RObVl.mjs +22 -0
- package/dist/format-Ce1RObVl.mjs.map +1 -0
- package/dist/format-DOrPvXEr.cjs +20 -0
- package/dist/frontmatter-D8wWCeOa.mjs +26 -0
- package/dist/frontmatter-D8wWCeOa.mjs.map +1 -0
- package/dist/frontmatter-DgAuai7E.cjs +35 -0
- package/dist/graph-normalizer-Cv9yK9Pg.mjs +130 -0
- package/dist/graph-normalizer-Cv9yK9Pg.mjs.map +1 -0
- package/dist/graph-normalizer-DeIj6Ses.cjs +133 -0
- package/dist/graph-reports-C4TBjCkM.mjs +63 -0
- package/dist/graph-reports-C4TBjCkM.mjs.map +1 -0
- package/dist/graph-reports-DU05YCei.cjs +64 -0
- package/dist/html-generator-CAv81IWH.cjs +85 -0
- package/dist/html-generator-V6Bp0uRb.mjs +68 -0
- package/dist/html-generator-V6Bp0uRb.mjs.map +1 -0
- package/dist/index.cjs +31 -0
- package/dist/index.d.cts +187 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +187 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +9 -0
- package/dist/init-BjuFt54X.cjs +232 -0
- package/dist/init-CaOsHTIo.mjs +232 -0
- package/dist/init-CaOsHTIo.mjs.map +1 -0
- package/dist/mcp-proxy.cjs +1257 -0
- package/dist/mcp-proxy.d.cts +12 -0
- package/dist/mcp-proxy.d.cts.map +1 -0
- package/dist/mcp-proxy.d.mts +12 -0
- package/dist/mcp-proxy.d.mts.map +1 -0
- package/dist/mcp-proxy.mjs +1255 -0
- package/dist/mcp-proxy.mjs.map +1 -0
- package/dist/output-root-CFYms3ad.cjs +43 -0
- package/dist/output-root-CmWM7aV2.mjs +33 -0
- package/dist/output-root-CmWM7aV2.mjs.map +1 -0
- package/dist/parser-BUIWW1OH.cjs +182 -0
- package/dist/parser-DO0_SssG.mjs +182 -0
- package/dist/parser-DO0_SssG.mjs.map +1 -0
- package/dist/public-tools-D4UI-Zb0.mjs +2554 -0
- package/dist/public-tools-D4UI-Zb0.mjs.map +1 -0
- package/dist/public-tools-XSpkz2ky.cjs +2556 -0
- package/dist/resolver-C2ZS7oC8.mjs +201 -0
- package/dist/resolver-C2ZS7oC8.mjs.map +1 -0
- package/dist/resolver-zYbu4wDV.cjs +203 -0
- package/dist/rolldown-runtime-wcPFST8Q.mjs +13 -0
- package/dist/runner-1Eq55OYb.cjs +148 -0
- package/dist/runner-BhUHbiHG.mjs +149 -0
- package/dist/runner-BhUHbiHG.mjs.map +1 -0
- package/dist/schema-4XpzDFQM.cjs +55 -0
- package/dist/schema-8d0rVIdZ.mjs +37 -0
- package/dist/schema-8d0rVIdZ.mjs.map +1 -0
- package/dist/schema-cache-9CksD7tX.mjs +34 -0
- package/dist/schema-cache-9CksD7tX.mjs.map +1 -0
- package/dist/schema-cache-CgWRCN2N.cjs +36 -0
- package/dist/selector-CkFcTXzz.cjs +10 -0
- package/dist/selector-xjm6NTHI.mjs +12 -0
- package/dist/selector-xjm6NTHI.mjs.map +1 -0
- package/dist/server-BkM5xrXb.mjs +45 -0
- package/dist/server-BkM5xrXb.mjs.map +1 -0
- package/dist/server-DXowbpfi.cjs +54 -0
- package/dist/session-BpNylyuJ.cjs +115 -0
- package/dist/session-CcTgYxsj.mjs +115 -0
- package/dist/session-CcTgYxsj.mjs.map +1 -0
- package/dist/setup-DOpKPrlx.cjs +81 -0
- package/dist/setup-DyrWHuwQ.mjs +80 -0
- package/dist/setup-DyrWHuwQ.mjs.map +1 -0
- package/dist/store-BiUhQOIf.cjs +230 -0
- package/dist/store-BoWE-Gtl.mjs +225 -0
- package/dist/store-BoWE-Gtl.mjs.map +1 -0
- package/dist/templates/graph.html +1406 -0
- package/dist/tool-visibility-3Z_KvO9Q.mjs +28 -0
- package/dist/tool-visibility-3Z_KvO9Q.mjs.map +1 -0
- package/dist/tool-visibility-CwgY205r.cjs +36 -0
- package/dist/tools-Cp2jAAAb.mjs +100 -0
- package/dist/tools-Cp2jAAAb.mjs.map +1 -0
- package/dist/tools-f_vJUZAF.cjs +139 -0
- package/dist/topup-server-BZuQifvh.cjs +940 -0
- package/dist/topup-server-DUjyFftI.mjs +919 -0
- package/dist/topup-server-DUjyFftI.mjs.map +1 -0
- package/dist/version-1gP19Lhi.mjs +8 -0
- package/dist/version-1gP19Lhi.mjs.map +1 -0
- package/dist/version-BNGtdpmH.cjs +18 -0
- package/dist/viz-BlCJe6Tk.mjs +35 -0
- package/dist/viz-BlCJe6Tk.mjs.map +1 -0
- package/dist/viz-ClezVXrJ.cjs +44 -0
- package/dist/wallet-BMelXBYP.mjs +104 -0
- package/dist/wallet-BMelXBYP.mjs.map +1 -0
- package/dist/wallet-RnvvSpV2.cjs +146 -0
- package/docs/architecture.md +145 -0
- package/docs/contributing.md +68 -0
- package/docs/debugging.md +68 -0
- package/docs/development.md +44 -0
- package/docs/graph-tools.md +251 -0
- package/docs/images/graph-mcp-iframe.png +0 -0
- package/docs/images/graph-visualization.png +0 -0
- package/docs/images/topup-page.png +0 -0
- package/docs/investigation-workspaces.md +151 -0
- package/docs/mcp-proxy.md +180 -0
- package/package.json +59 -0
- package/skills/chain-insights-developer-experience/SKILL.md +101 -0
- package/skills/chain-insights-investigation/SKILL.md +285 -0
- package/skills/chain-insights-investigation/agents/openai.yaml +4 -0
- package/skills/chain-insights-investigation/scripts/run-target-uat.sh +197 -0
- package/skills/chain-insights-trace-funds/SKILL.md +249 -0
- package/skills/ci-case/SKILL.md +43 -0
- package/skills/ci-status/SKILL.md +45 -0
- package/skills/test-chain-insights-graphrag-mcp/SKILL.md +75 -0
- package/skills/test-chain-insights-graphrag-mcp/agents/openai.yaml +4 -0
- 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;
|