chain-insights 0.52.0 → 0.53.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/mcp-proxy.cjs +28 -1
- package/dist/mcp-proxy.d.cts.map +1 -1
- package/dist/mcp-proxy.d.mts.map +1 -1
- package/dist/mcp-proxy.mjs +28 -1
- package/dist/mcp-proxy.mjs.map +1 -1
- package/dist/{skill-refresh-CWgEw-tQ.cjs → skill-refresh-DQZ2gox4.cjs} +8 -0
- package/dist/{skill-refresh-jGwQWlOH.mjs → skill-refresh-Dya9LQKN.mjs} +9 -2
- package/dist/skill-refresh-Dya9LQKN.mjs.map +1 -0
- package/package.json +1 -1
- package/dist/skill-refresh-jGwQWlOH.mjs.map +0 -1
package/dist/mcp-proxy.cjs
CHANGED
|
@@ -156,6 +156,7 @@ const LOCAL_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
|
156
156
|
"meta_usage_status",
|
|
157
157
|
"meta_subscription_status",
|
|
158
158
|
"meta_help",
|
|
159
|
+
"meta_rules",
|
|
159
160
|
"wallet_balance",
|
|
160
161
|
"wallet_topup",
|
|
161
162
|
require_graph_schema.GRAPH_EXPAND_TOOL
|
|
@@ -212,6 +213,7 @@ const KNOWN_PUBLIC_TOOL_DESCRIPTIONS = {
|
|
|
212
213
|
meta_usage_status: "Return the caller's public free graph_query quota for the current UTC day.",
|
|
213
214
|
meta_subscription_status: "Return the caller's CIA subscription window end, daily allowance, consumption, and tier.",
|
|
214
215
|
meta_help: "Show a short guide to Chain Insights tools and workflow.",
|
|
216
|
+
meta_rules: "Return the Chain Insights rules: how to write graph_query and graph_query_batch reads that are served, how to name columns so the answer is drawn as a graph, a time series or a table, and how to write the answer. Call it once per chat, before the first graph query, when the chain-insights-cypher skill is not loaded. Takes no arguments: send {}.",
|
|
215
217
|
wallet_balance: "Show the local Chain Insights payment wallet address, payment network, token, and amount.",
|
|
216
218
|
graph_query: `Run a read-only GQL/Cypher query through the Chain Insights graph endpoint. ${GRAPH_LAYERS_TEXT} ${ROUTING_NOT_SERVED} Preserve full addresses exactly, and write every address in full, all 42 characters, in your answer: never shorten one with ... or ….`,
|
|
217
219
|
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. For a time series across days, send one USE facts total per day with the query id set to the day, \"YYYY-MM-DD\": the view joins them into one series. Write every address in full in your answer, all 42 characters."
|
|
@@ -233,6 +235,7 @@ const PICTURE_RULES = [
|
|
|
233
235
|
"- The host draws the picture from the answer itself. Do not draw it again with another tool, such as a visualize or widget tool: the user would see the same answer drawn twice."
|
|
234
236
|
].join("\n");
|
|
235
237
|
const PICTURE_HELP_LINE = "In hosts that draw views, a graph_query answer with from_address and to_address columns draws a graph, a day or time column with numbers draws a time series (across days: a graph_query_batch of one-day totals, each query id the day), and any other rows draw a table.";
|
|
238
|
+
const RULES_FIRST = "Rules first: if the chain-insights-cypher skill is not loaded in this chat, call meta_rules once (send {}) before the first graph_query or graph_query_batch, and follow what it returns. When the skill is loaded, do not call meta_rules.";
|
|
236
239
|
const ANSWER_RULES = [
|
|
237
240
|
"Answer rules, for every reply to the user:",
|
|
238
241
|
"1. Write every address in full: 0x plus 40 hex characters, 42 in all, in sentences, lists, tables and summaries. Never shorten one with ... or …. The drawn picture may shorten its labels; your text never does.",
|
|
@@ -282,6 +285,7 @@ const GRAPH_SCHEMA_HINTS = [
|
|
|
282
285
|
const ROUTING_HEAD = routingHead();
|
|
283
286
|
const SERVER_INSTRUCTIONS = [
|
|
284
287
|
"Chain Insights is an AML and graph-analysis MCP server for AI agents.",
|
|
288
|
+
RULES_FIRST,
|
|
285
289
|
ANSWER_RULES,
|
|
286
290
|
CHAIN_INSIGHTS_WORKFLOW,
|
|
287
291
|
ROUTING_HEAD,
|
|
@@ -291,6 +295,7 @@ const SERVER_INSTRUCTIONS = [
|
|
|
291
295
|
].join("\n\n");
|
|
292
296
|
const STATELESS_SERVER_INSTRUCTIONS = [
|
|
293
297
|
"Chain Insights is running as a stateless AML proxy for a host application.",
|
|
298
|
+
RULES_FIRST,
|
|
294
299
|
ANSWER_RULES,
|
|
295
300
|
"Call graph_query or graph_query_batch with network=robinhood. meta_network_capabilities takes no arguments (send {}); call it only to check which tools and layers are live.",
|
|
296
301
|
ROUTING_HEAD,
|
|
@@ -729,7 +734,7 @@ async function createProxy() {
|
|
|
729
734
|
return;
|
|
730
735
|
}
|
|
731
736
|
if (process.env["CHAIN_INSIGHTS_SKILL_REFRESH"] !== "0") try {
|
|
732
|
-
const { refreshInstalledSkills } = await Promise.resolve().then(() => require("./skill-refresh-
|
|
737
|
+
const { refreshInstalledSkills } = await Promise.resolve().then(() => require("./skill-refresh-DQZ2gox4.cjs"));
|
|
733
738
|
const refresh = refreshInstalledSkills(require_print_result.PACKAGE_VERSION);
|
|
734
739
|
if (refresh.refreshed.length > 0 || refresh.failed.length > 0) await logger.info("skills.refresh", {
|
|
735
740
|
version: require_print_result.PACKAGE_VERSION,
|
|
@@ -1022,6 +1027,26 @@ async function createProxy() {
|
|
|
1022
1027
|
annotations: VIEW_TOOL_ANNOTATIONS,
|
|
1023
1028
|
_meta: APP_ONLY_TOOL_META
|
|
1024
1029
|
}, async (args) => require_graph_schema.handleGraphExpand(args, flowsDependencies));
|
|
1030
|
+
server.registerTool("meta_rules", {
|
|
1031
|
+
title: "Chain Insights Rules",
|
|
1032
|
+
description: KNOWN_PUBLIC_TOOL_DESCRIPTIONS.meta_rules,
|
|
1033
|
+
inputSchema: EMPTY_INPUT_SCHEMA,
|
|
1034
|
+
annotations: {
|
|
1035
|
+
readOnlyHint: true,
|
|
1036
|
+
destructiveHint: false,
|
|
1037
|
+
idempotentHint: true,
|
|
1038
|
+
openWorldHint: false
|
|
1039
|
+
}
|
|
1040
|
+
}, async () => {
|
|
1041
|
+
const { readPackagedSkillRules } = await Promise.resolve().then(() => require("./skill-refresh-DQZ2gox4.cjs"));
|
|
1042
|
+
return {
|
|
1043
|
+
content: [{
|
|
1044
|
+
type: "text",
|
|
1045
|
+
text: readPackagedSkillRules()
|
|
1046
|
+
}],
|
|
1047
|
+
isError: false
|
|
1048
|
+
};
|
|
1049
|
+
});
|
|
1025
1050
|
server.registerTool("meta_help", {
|
|
1026
1051
|
title: "Chain Insights Help",
|
|
1027
1052
|
description: KNOWN_PUBLIC_TOOL_DESCRIPTIONS.meta_help,
|
|
@@ -1052,6 +1077,7 @@ async function createProxy() {
|
|
|
1052
1077
|
"",
|
|
1053
1078
|
"Wallet tools:",
|
|
1054
1079
|
"- wallet_balance: show the local payment wallet address, payment network, token, and amount.",
|
|
1080
|
+
"- meta_rules: the full query, picture and answer rules. Call once per chat when the chain-insights-cypher skill is not loaded.",
|
|
1055
1081
|
"- meta_help: show this overview."
|
|
1056
1082
|
].join("\n") : [
|
|
1057
1083
|
"Chain Insights stateless AML proxy for host applications.",
|
|
@@ -1063,6 +1089,7 @@ async function createProxy() {
|
|
|
1063
1089
|
"- meta_subscription_status: check the caller CIA subscription window end, daily allowance, consumption, and tier.",
|
|
1064
1090
|
"- graph_query: run read-only GQL/Cypher through the universal graph endpoint. Use USE topology or USE facts.",
|
|
1065
1091
|
"- graph_query_batch: run related read-only graph-language queries through one paid graph call.",
|
|
1092
|
+
"- meta_rules: the full query, picture and answer rules. Call once per chat when the chain-insights-cypher skill is not loaded.",
|
|
1066
1093
|
"",
|
|
1067
1094
|
PICTURE_HELP_LINE
|
|
1068
1095
|
].join("\n")
|
package/dist/mcp-proxy.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-proxy.d.cts","names":[],"sources":["../src/mcp/proxy.ts"],"mappings":";;
|
|
1
|
+
{"version":3,"file":"mcp-proxy.d.cts","names":[],"sources":["../src/mcp/proxy.ts"],"mappings":";;qBAiEa;qBACA;;;;;wBAqBG;YAkBJ;wBAEI,oBAAoB,MAAK,OAAO,aAA2B;KAkCtE,iBAAiB,eAAe,EAAE;wBAwHvB,2BAA2B,mBAAmB;;;;;;;;wBA+lBxC,eAAe"}
|
package/dist/mcp-proxy.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-proxy.d.mts","names":[],"sources":["../src/mcp/proxy.ts"],"mappings":";;
|
|
1
|
+
{"version":3,"file":"mcp-proxy.d.mts","names":[],"sources":["../src/mcp/proxy.ts"],"mappings":";;qBAiEa;qBACA;;;;;wBAqBG;YAkBJ;wBAEI,oBAAoB,MAAK,OAAO,aAA2B;KAkCtE,iBAAiB,eAAe,EAAE;wBAwHvB,2BAA2B,mBAAmB;;;;;;;;wBA+lBxC,eAAe"}
|
package/dist/mcp-proxy.mjs
CHANGED
|
@@ -152,6 +152,7 @@ const LOCAL_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
|
152
152
|
"meta_usage_status",
|
|
153
153
|
"meta_subscription_status",
|
|
154
154
|
"meta_help",
|
|
155
|
+
"meta_rules",
|
|
155
156
|
"wallet_balance",
|
|
156
157
|
"wallet_topup",
|
|
157
158
|
GRAPH_EXPAND_TOOL
|
|
@@ -208,6 +209,7 @@ const KNOWN_PUBLIC_TOOL_DESCRIPTIONS = {
|
|
|
208
209
|
meta_usage_status: "Return the caller's public free graph_query quota for the current UTC day.",
|
|
209
210
|
meta_subscription_status: "Return the caller's CIA subscription window end, daily allowance, consumption, and tier.",
|
|
210
211
|
meta_help: "Show a short guide to Chain Insights tools and workflow.",
|
|
212
|
+
meta_rules: "Return the Chain Insights rules: how to write graph_query and graph_query_batch reads that are served, how to name columns so the answer is drawn as a graph, a time series or a table, and how to write the answer. Call it once per chat, before the first graph query, when the chain-insights-cypher skill is not loaded. Takes no arguments: send {}.",
|
|
211
213
|
wallet_balance: "Show the local Chain Insights payment wallet address, payment network, token, and amount.",
|
|
212
214
|
graph_query: `Run a read-only GQL/Cypher query through the Chain Insights graph endpoint. ${GRAPH_LAYERS_TEXT} ${ROUTING_NOT_SERVED} Preserve full addresses exactly, and write every address in full, all 42 characters, in your answer: never shorten one with ... or ….`,
|
|
213
215
|
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. For a time series across days, send one USE facts total per day with the query id set to the day, \"YYYY-MM-DD\": the view joins them into one series. Write every address in full in your answer, all 42 characters."
|
|
@@ -229,6 +231,7 @@ const PICTURE_RULES = [
|
|
|
229
231
|
"- The host draws the picture from the answer itself. Do not draw it again with another tool, such as a visualize or widget tool: the user would see the same answer drawn twice."
|
|
230
232
|
].join("\n");
|
|
231
233
|
const PICTURE_HELP_LINE = "In hosts that draw views, a graph_query answer with from_address and to_address columns draws a graph, a day or time column with numbers draws a time series (across days: a graph_query_batch of one-day totals, each query id the day), and any other rows draw a table.";
|
|
234
|
+
const RULES_FIRST = "Rules first: if the chain-insights-cypher skill is not loaded in this chat, call meta_rules once (send {}) before the first graph_query or graph_query_batch, and follow what it returns. When the skill is loaded, do not call meta_rules.";
|
|
232
235
|
const ANSWER_RULES = [
|
|
233
236
|
"Answer rules, for every reply to the user:",
|
|
234
237
|
"1. Write every address in full: 0x plus 40 hex characters, 42 in all, in sentences, lists, tables and summaries. Never shorten one with ... or …. The drawn picture may shorten its labels; your text never does.",
|
|
@@ -278,6 +281,7 @@ const GRAPH_SCHEMA_HINTS = [
|
|
|
278
281
|
const ROUTING_HEAD = routingHead();
|
|
279
282
|
const SERVER_INSTRUCTIONS = [
|
|
280
283
|
"Chain Insights is an AML and graph-analysis MCP server for AI agents.",
|
|
284
|
+
RULES_FIRST,
|
|
281
285
|
ANSWER_RULES,
|
|
282
286
|
CHAIN_INSIGHTS_WORKFLOW,
|
|
283
287
|
ROUTING_HEAD,
|
|
@@ -287,6 +291,7 @@ const SERVER_INSTRUCTIONS = [
|
|
|
287
291
|
].join("\n\n");
|
|
288
292
|
const STATELESS_SERVER_INSTRUCTIONS = [
|
|
289
293
|
"Chain Insights is running as a stateless AML proxy for a host application.",
|
|
294
|
+
RULES_FIRST,
|
|
290
295
|
ANSWER_RULES,
|
|
291
296
|
"Call graph_query or graph_query_batch with network=robinhood. meta_network_capabilities takes no arguments (send {}); call it only to check which tools and layers are live.",
|
|
292
297
|
ROUTING_HEAD,
|
|
@@ -725,7 +730,7 @@ async function createProxy() {
|
|
|
725
730
|
return;
|
|
726
731
|
}
|
|
727
732
|
if (process.env["CHAIN_INSIGHTS_SKILL_REFRESH"] !== "0") try {
|
|
728
|
-
const { refreshInstalledSkills } = await import("./skill-refresh-
|
|
733
|
+
const { refreshInstalledSkills } = await import("./skill-refresh-Dya9LQKN.mjs");
|
|
729
734
|
const refresh = refreshInstalledSkills(PACKAGE_VERSION);
|
|
730
735
|
if (refresh.refreshed.length > 0 || refresh.failed.length > 0) await logger.info("skills.refresh", {
|
|
731
736
|
version: PACKAGE_VERSION,
|
|
@@ -1018,6 +1023,26 @@ async function createProxy() {
|
|
|
1018
1023
|
annotations: VIEW_TOOL_ANNOTATIONS,
|
|
1019
1024
|
_meta: APP_ONLY_TOOL_META
|
|
1020
1025
|
}, async (args) => handleGraphExpand(args, flowsDependencies));
|
|
1026
|
+
server.registerTool("meta_rules", {
|
|
1027
|
+
title: "Chain Insights Rules",
|
|
1028
|
+
description: KNOWN_PUBLIC_TOOL_DESCRIPTIONS.meta_rules,
|
|
1029
|
+
inputSchema: EMPTY_INPUT_SCHEMA,
|
|
1030
|
+
annotations: {
|
|
1031
|
+
readOnlyHint: true,
|
|
1032
|
+
destructiveHint: false,
|
|
1033
|
+
idempotentHint: true,
|
|
1034
|
+
openWorldHint: false
|
|
1035
|
+
}
|
|
1036
|
+
}, async () => {
|
|
1037
|
+
const { readPackagedSkillRules } = await import("./skill-refresh-Dya9LQKN.mjs");
|
|
1038
|
+
return {
|
|
1039
|
+
content: [{
|
|
1040
|
+
type: "text",
|
|
1041
|
+
text: readPackagedSkillRules()
|
|
1042
|
+
}],
|
|
1043
|
+
isError: false
|
|
1044
|
+
};
|
|
1045
|
+
});
|
|
1021
1046
|
server.registerTool("meta_help", {
|
|
1022
1047
|
title: "Chain Insights Help",
|
|
1023
1048
|
description: KNOWN_PUBLIC_TOOL_DESCRIPTIONS.meta_help,
|
|
@@ -1048,6 +1073,7 @@ async function createProxy() {
|
|
|
1048
1073
|
"",
|
|
1049
1074
|
"Wallet tools:",
|
|
1050
1075
|
"- wallet_balance: show the local payment wallet address, payment network, token, and amount.",
|
|
1076
|
+
"- meta_rules: the full query, picture and answer rules. Call once per chat when the chain-insights-cypher skill is not loaded.",
|
|
1051
1077
|
"- meta_help: show this overview."
|
|
1052
1078
|
].join("\n") : [
|
|
1053
1079
|
"Chain Insights stateless AML proxy for host applications.",
|
|
@@ -1059,6 +1085,7 @@ async function createProxy() {
|
|
|
1059
1085
|
"- meta_subscription_status: check the caller CIA subscription window end, daily allowance, consumption, and tier.",
|
|
1060
1086
|
"- graph_query: run read-only GQL/Cypher through the universal graph endpoint. Use USE topology or USE facts.",
|
|
1061
1087
|
"- graph_query_batch: run related read-only graph-language queries through one paid graph call.",
|
|
1088
|
+
"- meta_rules: the full query, picture and answer rules. Call once per chat when the chain-insights-cypher skill is not loaded.",
|
|
1062
1089
|
"",
|
|
1063
1090
|
PICTURE_HELP_LINE
|
|
1064
1091
|
].join("\n")
|
package/dist/mcp-proxy.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-proxy.mjs","names":[],"sources":["../src/mcp/rate-limit.ts","../src/mcp/layer-routing.ts","../src/mcp/proxy.ts"],"sourcesContent":["/**\n * The graph endpoint limits requests per address. Its refusal arrives as a\n * thrown HTTP error, \"too many requests from this address; retry after 3\n * seconds\", not as a typed tool reply. Every proxied tool reads only, and a\n * refused request is not billed, so one wait and one resend are safe.\n *\n * When the resend is refused too, or the endpoint asks for a longer wait than\n * the proxy holds a call for, the caller gets the refusal envelope the graph\n * server uses for every other limit: class `capacity`. The skills tell an\n * agent what to do with that class.\n */\n\n/** The longest wait the proxy holds a call for before it sends it once more. */\nexport const MAX_RETRY_WAIT_MS = 10_000\n/** The wait when the endpoint names none. The skills say at least 5 seconds. */\nexport const DEFAULT_RETRY_WAIT_MS = 5_000\nconst MIN_RETRY_WAIT_MS = 1_000\n\nconst RATE_LIMITED = /\\b429\\b|too many requests|rate.?limit/i\nconst RETRY_AFTER = /retry after (\\d+) seconds?/i\n\nfunction messageOf(err: unknown): string {\n return err instanceof Error ? err.message : String(err)\n}\n\n/**\n * The wait the endpoint asks for, in milliseconds, when err is its rate\n * limit, and null for any other failure.\n */\nexport function rateLimitWaitMs(err: unknown): number | null {\n const code = err && typeof err === 'object' ? (err as { code?: unknown }).code : undefined\n const message = messageOf(err)\n if (code !== 429 && !RATE_LIMITED.test(message)) return null\n const seconds = RETRY_AFTER.exec(message)?.[1]\n return seconds ? Math.max(Number(seconds) * 1000, MIN_RETRY_WAIT_MS) : DEFAULT_RETRY_WAIT_MS\n}\n\nexport type Sleep = (ms: number) => Promise<void>\n\nconst defaultSleep: Sleep = (ms) => new Promise((done) => setTimeout(done, ms))\n\n/**\n * Runs call. When the endpoint refuses it for the rate limit and asks for a\n * wait of at most MAX_RETRY_WAIT_MS, waits that long and runs it once more.\n * Any other failure, and the second refusal, is thrown to the caller.\n */\nexport async function withRateLimitRetry<T>(\n call: () => Promise<T>,\n sleep: Sleep = defaultSleep\n): Promise<T> {\n try {\n return await call()\n } catch (err) {\n const wait = rateLimitWaitMs(err)\n if (wait === null || wait > MAX_RETRY_WAIT_MS) throw err\n await sleep(wait)\n return await call()\n }\n}\n\n/**\n * The reply for a call the endpoint refused for the rate limit: the text the\n * agent reads and the `capacity` envelope the skills act on. Null when err is\n * not the rate limit.\n */\nexport function rateLimitedResult(toolName: string, err: unknown) {\n const wait = rateLimitWaitMs(err)\n if (wait === null) return null\n const seconds = Math.ceil(wait / 1000)\n const fix = `Wait ${seconds} seconds, then send the same request once.`\n return {\n content: [\n {\n type: 'text' as const,\n text: `${toolName} was not run: Chain Insights Graph is limiting requests from this address. ${fix}`,\n },\n ],\n structuredContent: {\n schema: 'chain-insights.result.v1',\n tool: toolName,\n hint: null,\n error_detail: {\n code: 'rate_limited',\n rule: 'requests_per_address',\n class: 'capacity',\n fix,\n example: '',\n },\n },\n isError: true,\n }\n}\n","// The one source of the routing rule of the three read layers, and of the move\n// that each class of refusal asks of an agent.\n//\n// Three places teach them: the graph hints that the MCP proxy serves\n// (GRAPH_SCHEMA_HINTS in proxy.ts builds its two routing hints from this file),\n// the `chain-insights-cypher` skill and docs/graph-tools.md. The skill and the\n// guide are Markdown and hold the lines word for word. tests/layer-routing.test.ts\n// fails when one of the three differs from this file by one word.\n//\n// The rule follows the graph server's own: topology searches outward from one\n// known address, while facts and chain look up one known thing. The wording asks\n// the agent to say first what the question names (an address, a pair with a day,\n// a transaction, a block), because a question that names none of them is served\n// on no layer today. Nothing here is a limit. A limit that the server publishes\n// is read from `meta_network_capabilities`, and the text names the field, never\n// the number.\n//\n// `USE chain` serves the four labels of the server's chain catalogue and no\n// other: a transaction by its `tx_id`, a block by its `block_height` or its\n// `block_hash`, an address at one block (`Address`, with an optional `at_block`),\n// and the head, which takes no key. A kind (`:Account`, `:Contract`) is never a\n// lookup label: the `Address` lookup reads it as `is_contract` and `nonce`.\n// tests/fixtures/chain-catalogue.json pins the labels, and\n// tests/chain-catalogue-pin.test.ts holds every chain recipe and the chain\n// routing line to them. The depth below which a past block is served is a limit\n// that the server publishes, so the text names its field and never its number.\n\n/** The routing rule, in one sentence. The server states it in the same words. */\nexport const ROUTING_RULE =\n 'Route by what the question names: an address, a pair with a day, a transaction or a block. Topology searches outward from an address, while facts and chain look up one known thing.'\n\n/** The rule in three lines, one for each layer. */\nexport const ROUTING_LINES = [\n 'I know an address and want its links, senders, receivers, hops or a route: `USE topology`, anchored on that address.',\n 'I know both addresses of a pair and one day, and want the rows of that day: `USE facts`.',\n \"I want the chain's own record of a transaction by its `tx_id`, a block by its `block_height` or `block_hash`, an address at one block (`Address`), or the head: `USE chain`.\",\n] as const\n\n/** The chain layer reads one address at one block. A search for counterparties stays on topology. */\nexport const ROUTING_ADDRESS_ON_CHAIN =\n '`USE chain` with `Address` reads the balance, the nonce or the kind of one address, now or at a past block with `at_block`. A past block must be at least `chain_admission.at_block_min_depth` blocks below the tip. To find the counterparties of an address, search `USE topology`, then read the pair and one day on `USE facts`.'\n\n/** What to do when two layers fit. */\nexport const ROUTING_TWO_FIT =\n 'A transaction is a chain question: read it on `USE chain` by its `tx_id`. Chain gives its `block_date`; the transfers between a pair on that day are a `USE facts` read.'\n\n/** The key that each layer hands to the next. Each key keeps its name. */\nexport const ROUTING_HANDOFF =\n 'The layers hand each other keys, and each key keeps its name. Topology gives the pair and the first and last seen time. Facts gives the `tx_id` and the `block_height`. Chain takes an `address`, a `tx_id`, a `block_height` or a `block_hash`, and gives the `block_date` of a transaction or a block.'\n\n/** A whole-chain question is a time-bounded topology search; a server without search mode refuses it once. */\nexport const ROUTING_NOT_SERVED =\n 'A question about the whole chain (recent activity, the biggest senders, a top list) is a `USE topology` search: bound it by time with `f.last_seen_timestamp >= <epoch ms>` on the `FLOWS_TO` link, then sort and `LIMIT`. A search has 10 s. If the server refuses it with `anchor_missing`, it does not serve searches yet: say so in one line and ask for an address.'\n\n/** What a refusal carries, and which part of it decides the move. */\nexport const MOVE_INTRO =\n 'A refused, killed, busy or failed query comes back with `error_detail`: `code`, `rule`, `class`, `fix` and `example`. The `class` decides your next move.'\n\n/** The move for each class of the refusal envelope. */\nexport const MOVE_BY_CLASS = {\n refused: 'Class `refused`: read `fix`, rewrite the query from `example`, and send it once.',\n killed: 'Class `killed`: narrow the query (fewer properties, rows or hops) and send it once.',\n capacity: 'Class `capacity`: wait at least 5 seconds, then send the same query once.',\n failed: 'Class `failed`: tell the user what is down. Do not retry.',\n} as const\n\n/** A `fix` that names another layer. */\nexport const MOVE_OTHER_LAYER = 'A `fix` that names another layer means move to that layer.'\n\n/** One try, then the user. Never a loop. */\nexport const MOVE_ONCE =\n 'One rewrite or one retry for each query. When it is refused or busy again, stop and tell the user. Quote the `fix` text and send nothing more for that question.'\n\n/** After a refusal or a kill the server has already said no. */\nexport const MOVE_NEVER_SAME = 'Never send the same text again after `refused` or `killed`.'\n\n/** A batch has good members that already ran. */\nexport const MOVE_BATCH = 'In a batch, send again only the members that came back `capacity`.'\n\n/** The routing sentences, in the order a reader meets them. */\nexport function routingLines(): string[] {\n return [\n ROUTING_RULE,\n ...ROUTING_LINES,\n ROUTING_ADDRESS_ON_CHAIN,\n ROUTING_TWO_FIT,\n ROUTING_HANDOFF,\n ROUTING_NOT_SERVED,\n ]\n}\n\n/** The move-by-class sentences, in the order a reader meets them. */\nexport function moveLines(): string[] {\n return [\n MOVE_INTRO,\n MOVE_BY_CLASS.refused,\n MOVE_BY_CLASS.killed,\n MOVE_BY_CLASS.capacity,\n MOVE_BY_CLASS.failed,\n MOVE_OTHER_LAYER,\n MOVE_ONCE,\n MOVE_NEVER_SAME,\n MOVE_BATCH,\n ]\n}\n\n/**\n * The routing rule and its three lines, with the question that no layer serves,\n * as one paragraph. The proxy serves it first among its instructions: a host\n * that keeps only the start of the instructions (Claude Code keeps 2 KB) still\n * shows the agent where each kind of question goes, and which kind goes nowhere.\n */\nexport function routingHead(): string {\n return `Pick the layer first. ${ROUTING_RULE} 1. ${ROUTING_LINES[0]} 2. ${ROUTING_LINES[1]} 3. ${ROUTING_LINES[2]} ${ROUTING_NOT_SERVED}`\n}\n\n/**\n * The two hints that the MCP proxy serves for routing and for refusals. Each is\n * one line of the served instructions.\n */\nexport function routingHintLines(): string[] {\n return [\n `- Pick the layer first. ${ROUTING_RULE} 1. ${ROUTING_LINES[0]} 2. ${ROUTING_LINES[1]} 3. ${ROUTING_LINES[2]} ${ROUTING_NOT_SERVED} ${ROUTING_ADDRESS_ON_CHAIN} ${ROUTING_TWO_FIT} ${ROUTING_HANDOFF} The lookups that USE chain serves are listed in chain_admission.lookups of meta_network_capabilities. Read every limit that the server publishes from meta_network_capabilities (chain_admission and, when the server sends them, topology_admission and facts_admission). Never write a limit down.`,\n `- Act on a refusal by its class. ${moveLines().join(' ')}`,\n ]\n}\n","import { readFileSync } from 'node:fs'\nimport { appendFile, mkdir } from 'node:fs/promises'\nimport path from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'\nimport { Client } from '@modelcontextprotocol/sdk/client/index.js'\nimport { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'\nimport type { ContentBlock, GetPromptResult } from '@modelcontextprotocol/sdk/types.js'\nimport * as z from 'zod'\nimport type { InvestigatorConfig } from '../config/schema.js'\nimport { PACKAGE_VERSION } from '../version.js'\nimport type { McpTool } from './schema-cache.js'\nimport { rateLimitedResult, withRateLimitRetry } from './rate-limit.js'\nimport {\n HIDDEN_REMOTE_TOOL_NAMES,\n PUBLIC_MCP_TOOL_ALLOWED_ARGS,\n PUBLIC_MCP_TOOL_REQUIRED_ARGS,\n isAppOnlyTool,\n} from './tool-visibility.js'\nimport { PaymentRequiredError } from './client.js'\nimport { primitiveBackendUsageStatus } from './usage-status.js'\nimport { unavailableSubscriptionStatus } from './subscription-status.js'\nimport { mirrorGraphNetworkCapabilities } from './capabilities.js'\nimport { ROUTING_NOT_SERVED, routingHead, routingHintLines } from './layer-routing.js'\nimport {\n GraphSchemaError,\n META_SCHEMA_DESCRIPTION,\n META_SCHEMA_TITLE,\n handleMetaSchema,\n type GraphSchemaDependencies,\n} from './graph-schema.js'\nimport { actionLogSignalsFromResult, appendActionLog } from './action-log.js'\nimport {\n FLOWS_MAX_OFFSET,\n GRAPH_EXPAND_DESCRIPTION,\n GRAPH_EXPAND_TITLE,\n GRAPH_EXPAND_TOOL,\n handleGraphExpand,\n type FlowsDependencies,\n type GraphQueryAnswer,\n} from './flows.js'\n\nconst LOCAL_TOOL_NAMES = new Set([\n 'meta_network_capabilities',\n 'meta_schema',\n 'meta_usage_status',\n 'meta_subscription_status',\n 'meta_help',\n 'wallet_balance',\n 'wallet_topup',\n GRAPH_EXPAND_TOOL,\n])\n// Local tools only the view calls (visibility [\"app\"]): never named to the model.\nconst APP_ONLY_LOCAL_TOOL_NAMES = new Set([GRAPH_EXPAND_TOOL])\nconst GRAPH_ARRAY_KEYS = ['nodes', 'edges', 'flows', 'edge_anchors'] as const\n\n// The Chain Insights view: one HTML file, served by this proxy and by nothing\n// else. The graph endpoint serves no view; a view, a ui:// resource or a\n// _meta.ui it advertises is never forwarded. The tool result says which view\n// the file draws: a graph, a chart or a table from the columns of a graph_query\n// answer, the balance (meta_usage_status, meta_subscription_status), or the\n// answer of a click (graph_expand). The metadata below is the wire contract of\n// the Claude views, character for character.\nexport const CLAUDE_VIEW_URI = 'ui://chain-insights/view'\nexport const MCP_APP_MIME_TYPE = 'text/html;profile=mcp-app'\n// A tool whose answer the view draws: the resource under the current key and\n// the legacy flat key hosts still read.\nconst VIEW_TOOL_META = {\n ui: { resourceUri: CLAUDE_VIEW_URI },\n 'ui/resourceUri': CLAUDE_VIEW_URI,\n}\n// A tool only the view calls: the host keeps it away from the model.\nconst APP_ONLY_TOOL_META = { ui: { resourceUri: CLAUDE_VIEW_URI, visibility: ['app'] } }\nconst VIEW_RESOURCE_META = {\n ui: { prefersBorder: false, csp: { resourceDomains: ['https://assets.claude.ai'] } },\n}\nconst VIEW_TOOL_ANNOTATIONS = { readOnlyHint: true }\nconst CLAUDE_VIEW_FILE = 'claude-view.html'\n\nlet claudeViewHtml: string | undefined\n\n/**\n * The view file shipped in the package: dist/apps/claude-view.html next to the\n * built proxy, or src/mcp/apps/claude-view.html when run from source. Read once.\n */\nexport function readClaudeViewHtml(): string {\n if (claudeViewHtml !== undefined) return claudeViewHtml\n const here = path.dirname(fileURLToPath(import.meta.url))\n const candidates = [\n path.join(here, 'apps', CLAUDE_VIEW_FILE),\n path.join(here, '..', 'src', 'mcp', 'apps', CLAUDE_VIEW_FILE),\n ]\n for (const candidate of candidates) {\n try {\n claudeViewHtml = readFileSync(candidate, 'utf8')\n return claudeViewHtml\n } catch {\n /* try the next place */\n }\n }\n throw new Error(`The Chain Insights view file ${CLAUDE_VIEW_FILE} is missing from the package`)\n}\n\nexport type McpProxyMode = 'workspace' | 'stateless'\n\nexport function resolveMcpProxyMode(env: NodeJS.ProcessEnv = process.env): McpProxyMode {\n const raw = env['CHAIN_INSIGHTS_MCP_PROXY_MODE']?.trim().toLowerCase()\n if (!raw || raw === 'stateless') return 'stateless'\n if (raw === 'workspace') return 'workspace'\n if (raw === 'no-workspace' || raw === 'workspace-less') return 'stateless'\n throw new Error(`CHAIN_INSIGHTS_MCP_PROXY_MODE must be workspace or stateless; got \"${raw}\"`)\n}\n\nconst GRAPH_LAYERS_TEXT =\n \"Use USE topology for topology (address/FLOWS_TO/OPERATED_BY/LINKED graph with SWAPPED, ADDED_LIQUIDITY, REMOVED_LIQUIDITY, BRIDGED and the Pool label, unified recent+historical) and USE facts for bounded TRANSFER, SWAP, LIQUIDITY_ADD, LIQUIDITY_REMOVE and BRIDGE_CROSSING rows and enrichment, and USE chain for the chain node's own record of one known transaction, block, address or the head.\"\n\nconst KNOWN_PUBLIC_TOOL_DESCRIPTIONS: Record<string, string> = {\n meta_network_capabilities:\n 'Return the current Chain Insights network and tool support matrix. Takes no arguments: send {}.',\n meta_schema: META_SCHEMA_DESCRIPTION,\n meta_usage_status: \"Return the caller's public free graph_query quota for the current UTC day.\",\n meta_subscription_status:\n \"Return the caller's CIA subscription window end, daily allowance, consumption, and tier.\",\n meta_help: 'Show a short guide to Chain Insights tools and workflow.',\n wallet_balance:\n 'Show the local Chain Insights payment wallet address, payment network, token, and amount.',\n graph_query: `Run a read-only GQL/Cypher query through the Chain Insights graph endpoint. ${GRAPH_LAYERS_TEXT} ${ROUTING_NOT_SERVED} Preserve full addresses exactly, and write every address in full, all 42 characters, in your answer: never shorten one with ... or ….`,\n graph_query_batch:\n 'Run multiple read-only GQL/Cypher queries through the Chain Insights graph endpoint in one paid batch. Prefer this for related topology/facts reads. For a time series across days, send one USE facts total per day with the query id set to the day, \"YYYY-MM-DD\": the view joins them into one series. Write every address in full in your answer, all 42 characters.',\n}\n// Titles for proxied tools whose graph endpoint definition carries none.\nconst KNOWN_PUBLIC_TOOL_TITLES: Record<string, string> = {\n graph_query: 'Graph Query',\n graph_query_batch: 'Graph Query Batch',\n}\nconst FALLBACK_GRAPH_PRIMITIVE_TOOL_NAMES = ['graph_query', 'graph_query_batch'] as const\n\ntype ToolInputShape = Record<string, z.ZodTypeAny>\ntype ToolHandler = (args: unknown, extra?: unknown) => Promise<unknown> | unknown\ntype ToolRegistrationConfig = Parameters<McpServer['registerTool']>[1]\ntype ToolCallInput = { name: string; arguments?: Record<string, unknown> }\ntype RemoteToolCaller = {\n callTool: Client['callTool']\n}\nconst NETWORK_DESCRIPTION =\n 'Network to query: robinhood, the one public network. CIA does not pick a default network, so always pass it. meta_network_capabilities (no arguments) lists the live networks.'\nconst NETWORK_SCHEMA = z.string().min(1).describe(NETWORK_DESCRIPTION)\n\nconst EMPTY_INPUT_SCHEMA = z.strictObject({})\nconst REMOTE_GRAPH_TOOL_REQUEST_TIMEOUT_MS = 15 * 60 * 1000\n\n// What the Chain Insights view draws from a graph_query answer, told by column\n// name. The same rules are in skills/chain-insights-cypher/SKILL.md.\nconst PICTURE_RULES = [\n 'Pictures: hosts that draw views, such as Claude Desktop and the Codex app, draw a graph_query answer from its column names. No other tool is needed. Choose the picture by the question: who sent to whom, flows and routes are a graph; how something changed over time is a time series; a list, a lookup or the details of one thing is a table.',\n '- Rows with from_address and to_address columns draw a graph of at most 5,000 addresses. Optional columns: amount_usd_sum, tx_count, first_seen_timestamp, last_seen_timestamp, link_kind, from_labels, to_labels. Example: USE topology MATCH (a:Address {address: $addr})-[f:FLOWS_TO]->(b:Address) WHERE NOT a:Pool RETURN a.address AS from_address, b.address AS to_address, f.amount_usd_sum AS amount_usd_sum, f.tx_count AS tx_count, f.last_seen_timestamp AS last_seen_timestamp ORDER BY f.last_seen_timestamp DESC LIMIT 25',\n '- Rows with a day, date, hour, week, month or *_timestamp column, number columns, no address in any value and at least 3 distinct times draw a time series. Within one day, the transfers of a known pair: USE facts MATCH (a:Address {address: $from})-[t:TRANSFER]->(b:Address {address: $to}) WHERE t.block_date = \"YYYY-MM-DD\" RETURN t.block_timestamp AS block_timestamp, t.amount_usd AS amount_usd LIMIT 200',\n '- Across days: facts reads one day at a time, so send graph_query_batch with one query per day (3 to 20 days), each query id the day as \"YYYY-MM-DD\", each returning one total row: USE facts MATCH (a:Address {address: $from})-[t:TRANSFER]->(b:Address {address: $to}) WHERE t.block_date = \"YYYY-MM-DD\" RETURN count(t) AS tx_count, sum(t.amount_usd) AS amount_usd_sum. The view joins the days into one time series. Do not group by day or name several days in one read: both are refused.',\n '- Any other rows draw a table.',\n '- The host draws the picture from the answer itself. Do not draw it again with another tool, such as a visualize or widget tool: the user would see the same answer drawn twice.',\n].join('\\n')\n\n// The same rule in one sentence, for meta_help.\nconst PICTURE_HELP_LINE =\n 'In hosts that draw views, a graph_query answer with from_address and to_address columns draws a graph, a day or time column with numbers draws a time series (across days: a graph_query_batch of one-day totals, each query id the day), and any other rows draw a table.'\n\n// What every reply to the user follows. First in the instructions, so a host\n// that keeps only their start still has them; the skill says the same.\nconst ANSWER_RULES = [\n 'Answer rules, for every reply to the user:',\n '1. Write every address in full: 0x plus 40 hex characters, 42 in all, in sentences, lists, tables and summaries. Never shorten one with ... or …. The drawn picture may shorten its labels; your text never does.',\n '2. Do not print the query, its USE line, its LIMIT, the epoch numbers you computed, or a Ref: line, unless the user asks how a number was found.',\n '3. Tokens are not dollars: a transfer moves tokens worth an amount at the day price. Say \"tokens worth about 183 USD on 2026-10-06\", never \"sent 183 USD\".',\n '4. Never draw a graph or a chart in text, ASCII or Mermaid: the host draws the picture from the columns. Write a short answer beside it.',\n].join('\\n')\n\nconst CHAIN_INSIGHTS_WORKFLOW = [\n 'Workflow:',\n '1. Do not call investigation tools until required arguments are known. Network is required; use meta_network_capabilities to check supported networks and available tools, or ask the user if missing.',\n '2. Use graph_query(_batch) for every question about addresses, money flow and transactions.',\n '3. Preserve tool summaries and structured facts as returned. Keep full blockchain addresses intact.',\n].join('\\n')\n\nconst GRAPH_SCHEMA_HINTS = [\n 'Graph query hints:',\n '- Pass network=robinhood, the one public network, on every graph tool call. CIA does not pick a default network. meta_network_capabilities takes no arguments (send {}); call it only to check which tools and layers are live.',\n ...routingHintLines(),\n '- Coverage is per kind of data. coverage.complete_through_block is the floor of USE facts: below it every raw relationship is indexed. graph_progress.complete_through_block is the floor of USE topology, and graph_progress.layers gives each link kind its own height. An empty answer above the floor may mean not indexed yet, not absent on chain. An absent chain_tip_block means the indexers are catching up: read freshness.max_data_age_seconds for the lag.',\n \"- The graph is address-grain. The only topology money node label is Address, keyed by the raw chain-native H160 address on EVM networks, for example 0x1874a43d7c6d888f9eda3d22a3a49704e3cadb24. There is no separate identity key. network is the query's network on every node and relationship, never stored, except on :Chain, where it names the remote chain.\",\n '- An Address carries a kind from chain facts. topology carries :Account or :Contract as a second label on an Address (:Contract:SmartAccount for a contract wallet), with is_contract kept beside :Contract, and an address that only received has none. facts serves no kind, and a facts read that names :Account or :Contract is refused: ask USE topology or USE chain. chain Address returns is_contract, nonce and delegated_to at the newest block, or at the block named by at_block. block_timestamp is epoch milliseconds on every layer.',\n '- Address nodes carry address, network, labels, and the role flags is_exchange, is_scam, is_victim and is_sanctioned. (:Address)-[:LINKED]-(:Address) is an undirected ownership-overlay edge (basis derived/associated, plus confidence, source_event, declared_owner, owner_state) asserting the two addresses are controlled by the same actor. LINKED is served on the topology graph only. Enumerate LINKED neighbors with MATCH (a:Address {address: $addr})-[l:LINKED]-(b:Address) RETURN b.address, b.network, l.basis, l.confidence LIMIT 25.',\n '- Labels hold role words, never detector names: Exchange (plus the exchange name), Scam, Victim and Sanctioned (plus the entity name). Each role is also a node label and a flag on the Address: :Exchange and is_exchange follow the exchange label, :Scam and is_scam the risk label, :Victim and is_victim the protection label, :Sanctioned and is_sanctioned the sanctioned label. Each flag is present only when true and absent otherwise, never false: test IS NOT NULL or IS NULL, not = false. A withdrawn label removes its node label and its flag.',\n '- Address nodes carry base activity rollups: degree_in/degree_out/degree_total (distinct counterparty addresses), tx_in_count/tx_out_count/tx_total_count, total_in_usd/total_out_usd/total_volume_usd, net_flow_usd (in minus out; positive = net receiver) — all computed from external flows only — and first_activity_timestamp/last_activity_timestamp/activity_span_days, which include all flows (self-loops included). FLOWS_TO edges carry exactly tx_count, amount_usd_sum (total money flow, token and native value merged), first_seen_timestamp, last_seen_timestamp. Lifetime aggregates are the only serving window. Averages are computed inline (amount_usd_sum / toFloat(tx_count)). tx_count counts token and native transfers plus internal native transfers (a contract sending ETH during a call), and amount_usd_sum prices them all. USE facts TRANSFER lists all three, so the rows of a pair are the transfers its link counts, up to the height the link was built to. A link covers all time and a USE facts read covers one day: read the transfers of a pair one day at a time. A transaction anchor of a pair resolves through USE facts, on the UTC day of the first_seen_timestamp or last_seen_timestamp of the link: MATCH (a:Address {address: $from})-[t:TRANSFER]->(b:Address {address: $to}) WHERE t.block_date = \"YYYY-MM-DD\" RETURN t.tx_id, t.block_timestamp LIMIT 1.',\n '- For actor-level exposure (AC11), UNION FLOWS_TO and SWAPPED reachability over one visible LINKED hop instead of expanding through the LINKED edge itself: MATCH (a:Address {address: $addr})-[:LINKED]-(owned:Address)-[r:FLOWS_TO|SWAPPED]-(b:Address) WHERE NOT a:Pool AND NOT owned:Pool AND owned.address <> b.address AND a.address <> b.address RETURN owned.address, b.address, type(r), coalesce(r.amount_usd_sum, r.bought_usd) LIMIT 50.',\n '- Labels and per-label risk live on the address node: the labels array plus three parallel lists, label_risk_labels, label_risk_levels and label_risk_updated_timestamps, where entry i of each list is one label row. No overall risk score is served. USE facts serves bounded single-event rows (TRANSFER, SWAP, LIQUIDITY_ADD, LIQUIDITY_REMOVE and BRIDGE_CROSSING edges) only; lifetime address metrics (degrees, totals, activity window) are node properties on USE topology.',\n '- (from:Address)-[t:TRANSFER]->(to:Address) on USE facts returns individual transfer rows, not aggregates, with properties amount, amount_usd, asset_symbol, asset_contract, tx_id, block_height, block_timestamp, event_index, edge_index, kind, price_usd, and price_missing. Every TRANSFER row has a kind: token, native or internal. An internal row is ETH a contract sends while it runs a call, such as the ETH leg of a wrap. Add t.kind = \"internal\" to the pair and the day to read only the internal rows. A facts read names an address pair with one day, whether it is a TRANSFER row-select or a count()/sum() aggregate: both endpoint addresses as {address: \"...\"} on from and to, with WHERE t.block_date = \"YYYY-MM-DD\". A transaction is read on USE chain by its tx_id, not on facts. t.block_timestamp bounds in epoch milliseconds may narrow the day to a time window. One address, a day alone, a window of days, a block range or a bare LIMIT is not enough. A facts read holds at most 200 rows in a reply, has one relationship and no hop, and takes no ORDER BY: sort the page yourself.',\n '- Facts graph labels include Address; the TRANSFER, SWAP, LIQUIDITY_ADD, LIQUIDITY_REMOVE and BRIDGE_CROSSING relationships each connect two Address nodes. Facts address keys match topology address values exactly.',\n '- Topology relationships include FLOWS_TO, SWAPPED, OPERATED_BY, LINKED, and RISK_PROXIMITY between Address nodes, ADDED_LIQUIDITY and REMOVED_LIQUIDITY to and from Pool nodes, and BRIDGED to and from Chain nodes.',\n \"- (:Address)-[:OPERATED_BY]->(:Address) is the directed owner-to-operator edge: the transaction sender that moved the owner's tokens (ERC-20/721), or the event operator (ERC-1155); not the approved spender. Aggregate properties: tx_count, amount_usd_sum, first_seen_timestamp, last_seen_timestamp, and optional token_standard (ERC20/ERC721/ERC1155; absent when the pair is mixed-standard). Valuation fields: valuation_tracked_count, valued_count and the reason counters missing_valuation_price_count, unknown_quantity_count, unrepresentable_quantity_count and usd_range_count. valuation_complete is true when valued_count equals valuation_tracked_count, valuation_coverage_ratio is valued_count divided by valuation_tracked_count, and amount_usd_sum counts a transfer with no USD value as 0, so it is a floor unless valuation_complete is true. One edge per owner/operator pair; direct transfers with no operator create none; an owner never operates for itself. Topology-only, and a topology fact — not a risk label. Probe: MATCH (owner:Address)-[operation:OPERATED_BY]->(operator:Address {address: $addr}) RETURN owner.address AS owner, operation.tx_count AS tx_count ORDER BY operation.tx_count DESC LIMIT 10.\",\n '- FLOWS_TO properties are tx_count, amount_usd_sum, first_seen_timestamp, last_seen_timestamp. pair_key and synced_through_height are internal sync bookkeeping, not to be queried. synced_through_height is also on the other summed links (SWAPPED, OPERATED_BY, ADDED_LIQUIDITY, REMOVED_LIQUIDITY, BRIDGED, APPROVED, SPONSORED, BUNDLED, SIGNED_FOR) and pair_key on SWAPPED: bookkeeping there too. Tx ids of every transfer, internal ones included, come from USE facts TRANSFER. Averages come from inline arithmetic. FLOWS_TO into and out of pools is kept; the pool trace rule below governs a trace that reaches one.',\n '- Swaps, liquidity and bridges on USE topology are lifetime totals per pair; single events are USE facts rows. (:Address)-[:SWAPPED]->(:Address) joins a swap payer to a different recipient, one edge per payer, recipient, sold_asset and bought_asset, with swap_count, sold_amount_raw, bought_amount_raw, sold_usd, bought_usd, pools, families, strength, first_seen_height and last_seen_height. sold_usd and bought_usd sum the routes on the edge at the day price: a route side with no price adds 0, so 0 can mean no price. Do not read 0 as worth nothing. The USE facts SWAP row says which side had no price (sold_price_missing, bought_price_missing). strength is swap (the whole route is proven: payer, recipient, assets, exact raw amounts and conservation) or swap_like (the shape is a swap but the pool code is not proven). Today no route is swap: the swap reader reads transaction receipts only, with no execution trace, so every served route is swap_like, with reason unknown_pool_code and families unknown. A Uniswap V2 or V3 swap reads this way, and unknown does not mean the protocol is unsupported. A filter on strength = \"swap\" or on a known family matches nothing. A route that could not be paired (swap_unsplit) is never served: it has no payer, recipient or pool, makes no SWAPPED edge and no SWAP row, and exists in the warehouse only. Every Uniswap v4 swap is swap_unsplit today, by design, so v4 swaps are hidden. A self swap makes no edge, and a missing SWAPPED edge is not proof that no swap happened. Swap attribution is read from SWAPPED, the aggregate (strength, pools, families), or from the USE facts SWAP row, one route. FLOWS_TO carries value only.',\n \"- Pool is a second label on an Address: the pool of a swap route or liquidity event, with liquidity_added_usd, liquidity_removed_usd, liquidity_net_usd, provider_count and receiver_count. (:Address)-[:ADDED_LIQUIDITY]->(:Pool) and (:Pool)-[:REMOVED_LIQUIDITY]->(:Address) join named providers and receivers to the pool, with event_count, totals_raw, usd, unpriced_count, first_seen_height and last_seen_height; REMOVED_LIQUIDITY adds fees_raw, receiver_added_usd (USD the receiver itself added to the same pool) and receiver_provided. A receiver's profit from a pool is usd minus receiver_added_usd: receiver_provided false is a drain by a stranger, true with a large profit is the creator's rug pull.\",\n '- (:Address)-[:BRIDGED]->(:Chain) is outbound bridge use and (:Chain)-[:BRIDGED]->(:Address) inbound, with kinds, events, totals_raw, first_height, last_height and last_bridge_event_id. A Chain node (network, address) is the remote bridge endpoint, never an Address, so no FLOWS_TO walk passes through it.',\n '- Pool trace rule, for every trace: 1. Enter a `:Pool` on any edge. 2. Leave a `:Pool` only on `REMOVED_LIQUIDITY`, to the address the liquidity was paid to. 3. Never leave a `:Pool` on `FLOWS_TO`. 4. Across a swap, follow `SWAPPED` from payer to recipient, between two different addresses. Do not walk through the pool. Rug-pull probe: MATCH (victim:Address {address: $addr})-[paid:FLOWS_TO]->(pool:Pool)-[removal:REMOVED_LIQUIDITY]->(receiver:Address) WHERE NOT victim:Pool AND receiver.address <> victim.address RETURN pool.address AS pool_address, receiver.address AS receiver_address, removal.usd AS removed_usd, removal.receiver_added_usd AS receiver_added_usd, removal.receiver_provided AS receiver_provided LIMIT 25.',\n '- USE facts serves the single events. (payer:Address)-[s:SWAP]->(recipient:Address) is one row per swap route, self swaps included, with strength, reason, route_id and the sold_ and bought_ asset, amount and usd columns. (provider:Address)-[:LIQUIDITY_ADD]->(pool:Address) and (pool:Address)-[:LIQUIDITY_REMOVE]->(receiver:Address) are one row per liquidity event, with party_state and evidence_state. (sender:Address)-[c:BRIDGE_CROSSING]->(recipient:Address) is one row per bridge event. SWAP, LIQUIDITY_* and BRIDGE_CROSSING take the same rule as TRANSFER: an address pair with one day. USD comes from the daily price services, never from a swap: with no price, USD is empty and the matching price_missing column is true (price_missing on TRANSFER, sold_price_missing and bought_price_missing on SWAP, amount0_price_missing and amount1_price_missing on LIQUIDITY_*). block_timestamp on TRANSFER, SWAP and LIQUIDITY_* rows is epoch milliseconds, in filters and in results; BRIDGE_CROSSING has none.',\n '- A USE facts SWAP row carries no route: it has no pools and no families column. A read that names one of them, to return it, to filter on it or to order by it, is refused. The route of a swap stays a topology question. For the pools of the swaps of an address, read SWAPPED.pools and SWAPPED.families on USE topology, anchored on the payer or the recipient. SWAPPED has one link per payer, recipient, sold asset and bought asset, so its pools cover every route on the link, not one route.',\n '- Traversal rule: for BFS, fixed-hop fallback, shortest-path, or manual FLOWS_TO traversal, exchange hot wallets are terminal endpoints only. Do not expand from, through, or classify exchange nodes as deposit, suspect, or intermediate candidates; filter every non-terminal node with is_exchange IS NULL. is_exchange is absent unless true, so a labelled node with no is_exchange is walked through, and is_scam, is_victim and is_sanctioned do not end a walk. At a Pool, follow the pool trace rule above.',\n '- Pool guard: a trace walks FLOWS_TO and SWAPPED, so it crosses a swap from payer to recipient without passing through the pool. A walk may end at a Pool, but never starts at one or passes through one: its start and every address in its middle stay off a Pool. A fixed-hop walk adds WHERE NOT src:Pool AND NOT mid:Pool, each its own AND term, never inside an OR. A quantified or shortest-path walk puts the guards inside the path pattern, on the start and on up to 4 guarded hops before one last hop: MATCH p = SHORTEST 1 (a:Address {address: $from} WHERE NOT a:Pool) (()-[:FLOWS_TO|SWAPPED]-(via:Address) WHERE NOT via:Pool){0,4} ()-[:FLOWS_TO|SWAPPED]-(b:Address {address: $to}) RETURN [n IN nodes(p) | n.address] AS route LIMIT 5. ANY SHORTEST takes the same pattern. A route search asks for one path. An open target from one address: MATCH SHORTEST 1 (a:Address {address: $addr} WHERE NOT a:Pool) (()-[:FLOWS_TO|SWAPPED]-(via:Address) WHERE NOT via:Pool){0,4} ()-[:FLOWS_TO|SWAPPED]-(b:Address) RETURN b.address LIMIT 50. Use these shapes as written, changing only the addresses and the RETURN. A WHERE placed after a SHORTEST pattern runs after the shortest route is chosen, so it drops a route that crosses a pool instead of finding the route that avoids it.',\n '- Call meta_schema {network} first when you need field names; it is cached for 24 hours. The two discovery reads below are the fallback.',\n '- Start schema discovery with endpoint-safe property reads: MATCH (n:Address) RETURN n.address AS address, n.network AS network, n.labels AS labels, n.last_activity_timestamp AS last_activity_timestamp LIMIT 20',\n '- Relationship discovery: MATCH (:Address)-[r:FLOWS_TO]->(:Address) RETURN r.amount_usd_sum AS amount_usd_sum, r.tx_count AS tx_count LIMIT 20',\n \"- Anchor every topology read that filters on a link property: put an address in its pattern. Without one, a read starts from every link of the type it names, and you should not count on the filter to narrow that: WHERE x.strength = 'swap' on SWAPPED checks every SWAPPED link, LIMIT stops the read only after enough rows match, and a filter that matches few or none can run to the 60 s topology limit and fail with query_timeout. Example: MATCH (a:Address {address: $addr})-[x:SWAPPED]->(b:Address) WHERE x.swap_count >= 2 RETURN b.address, x.swap_count LIMIT 25. Pick an address with few links: degree_out and degree_in are a rough guide, because they count neighbours, not links, and an address with hundreds of thousands of neighbours can fail the same way. Discovery probes with LIMIT and no filter stay valid. The queries of one batch share a 100 s budget; USE facts queries stop at 30 s.\",\n '- graph_query uses the active Chain Insights graph endpoint. Select the graph with USE topology for topology (address/FLOWS_TO/OPERATED_BY/LINKED graph with SWAPPED, ADDED_LIQUIDITY, REMOVED_LIQUIDITY, BRIDGED and the Pool label, unified recent+historical) and USE facts for bounded TRANSFER, SWAP, LIQUIDITY_ADD, LIQUIDITY_REMOVE and BRIDGE_CROSSING rows and enrichment, and USE chain for one keyed lookup on the chain node: a transaction by tx_id, a block by block_height or block_hash, an address at one block, or the head. On topology, address is the node grain, not the topology name.',\n '- 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.',\n '- Use USE facts graph patterns for fact and enrichment reads. Do not query internal table namespaces directly.',\n].join('\\n')\n\n// The routing paragraph comes first: a host that keeps only the start of the\n// instructions must still show where each kind of question goes.\nconst ROUTING_HEAD = routingHead()\n\nconst SERVER_INSTRUCTIONS = [\n 'Chain Insights is an AML and graph-analysis MCP server for AI agents.',\n ANSWER_RULES,\n CHAIN_INSIGHTS_WORKFLOW,\n ROUTING_HEAD,\n PICTURE_RULES,\n GRAPH_SCHEMA_HINTS,\n 'Presentation rules: preserve tool summaries as returned; never truncate blockchain addresses or identity_resolution audit mappings.',\n].join('\\n\\n')\n\nconst STATELESS_SERVER_INSTRUCTIONS = [\n 'Chain Insights is running as a stateless AML proxy for a host application.',\n ANSWER_RULES,\n 'Call graph_query or graph_query_batch with network=robinhood. meta_network_capabilities takes no arguments (send {}); call it only to check which tools and layers are live.',\n ROUTING_HEAD,\n PICTURE_RULES,\n 'Use wallet_balance to inspect the local payment wallet when payment setup is needed.',\n GRAPH_SCHEMA_HINTS,\n 'Presentation rules: preserve tool summaries as returned; never truncate blockchain addresses or identity_resolution audit mappings.',\n].join('\\n\\n')\n\n// Exported so a test can prove, for EVERY public tool, that each declared\n// schema argument also appears in PUBLIC_MCP_TOOL_ALLOWED_ARGS. An argument\n// present here but missing there is silently stripped by\n// normalizeRemoteToolArguments and the caller never learns their override was\n// ignored — the failure mode that shipped with `time_scope`.\nexport function knownPublicToolInputSchema(toolName: string): ToolInputShape | null {\n switch (toolName) {\n case 'graph_query':\n return {\n query: z.string().min(1).describe(`Read-only GQL/Cypher query. ${GRAPH_LAYERS_TEXT}`),\n network: NETWORK_SCHEMA,\n }\n case 'graph_query_batch':\n return {\n network: NETWORK_SCHEMA,\n queries: z\n .array(\n z.object({\n id: z.string().optional(),\n query: z.string().min(1).describe('Read-only GQL/Cypher query'),\n })\n )\n .min(1)\n .max(20),\n per_query_timeout_seconds: z.number().int().min(1).max(600).optional(),\n }\n default:\n return null\n }\n}\n\nfunction fallbackGraphPrimitiveTools(): McpTool[] {\n return FALLBACK_GRAPH_PRIMITIVE_TOOL_NAMES.map((name) => ({\n name,\n description: KNOWN_PUBLIC_TOOL_DESCRIPTIONS[name],\n }))\n}\n\n/**\n * Local payment wallet address for tools that address the caller on the\n * server (subscription_status). Returns null — never throws — when no wallet\n * is configured; callers degrade to an unavailable-shape result.\n */\n/**\n * Local payment wallet address for the top-up view: null when no wallet file\n * exists, and an error when the file exists but cannot be read. Reads the\n * address only; nothing is signed or sent.\n */\nasync function localTopupWalletAddress(): Promise<string | null> {\n const { isWalletConfigured } = await import('../wallet/index.js')\n if (!(await isWalletConfigured())) return null\n const { getWalletAccount } = await import('../wallet/tools.js')\n return (await getWalletAccount()).address\n}\n\nasync function localSubscriptionWalletAddress(): Promise<string | null> {\n try {\n const { getWalletAccount } = await import('../wallet/tools.js')\n const account = await getWalletAccount()\n return account.address\n } catch {\n return null\n }\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return !!value && typeof value === 'object' && !Array.isArray(value)\n}\n\nfunction redactLogValue(value: unknown): unknown {\n if (Array.isArray(value)) return value.map(redactLogValue)\n if (!isRecord(value)) return value\n return Object.fromEntries(\n Object.entries(value).map(([key, entry]) => {\n if (/token|secret|password|private.?key|authorization/i.test(key)) return [key, '[redacted]']\n return [key, redactLogValue(entry)]\n })\n )\n}\n\nfunction errorForLog(err: unknown): Record<string, unknown> {\n const error = err as Error\n return {\n name: error.name ?? 'Error',\n message: error.message ?? String(err),\n }\n}\n\nfunction sanitizeCypher(query: string): string {\n return query.replace(/\\s+/g, ' ').trim()\n}\n\nfunction cypherLogPayload(tool: string, args: unknown): Record<string, unknown> | null {\n if (!isRecord(args)) return null\n if (tool === 'graph_query') {\n return {\n network: args.network,\n queries: [\n {\n id: tool,\n query: typeof args.query === 'string' ? sanitizeCypher(args.query) : args.query,\n },\n ],\n }\n }\n if (tool === 'graph_query_batch') {\n const queries = Array.isArray(args.queries) ? args.queries : []\n return {\n network: args.network,\n per_query_timeout_seconds: args.per_query_timeout_seconds,\n query_count: queries.length,\n queries: queries.map((entry, index) =>\n isRecord(entry)\n ? {\n id: typeof entry.id === 'string' ? entry.id : `q${index + 1}`,\n query: typeof entry.query === 'string' ? sanitizeCypher(entry.query) : entry.query,\n }\n : { id: `q${index + 1}`, query: entry }\n ),\n }\n }\n return null\n}\n\nfunction createMcpLogger(config: Pick<InvestigatorConfig, 'dataDir'>) {\n const disabled = process.env.CHAIN_INSIGHTS_MCP_LOG === '0'\n const filePath =\n process.env.CHAIN_INSIGHTS_MCP_LOG_PATH?.trim() ||\n path.join(config.dataDir, '.chain-insights', 'runtime', 'logs', 'mcp-proxy.jsonl')\n\n async function write(\n level: 'info' | 'error',\n event: string,\n fields: Record<string, unknown> = {}\n ): Promise<void> {\n if (disabled) return\n try {\n await mkdir(path.dirname(filePath), { recursive: true })\n await appendFile(\n filePath,\n JSON.stringify({\n ts: new Date().toISOString(),\n level,\n event,\n pid: process.pid,\n ...fields,\n }) + '\\n',\n { mode: 0o600 }\n )\n } catch {\n // Logging must never break the stdio MCP server.\n }\n }\n\n return {\n filePath,\n info: (event: string, fields?: Record<string, unknown>) => write('info', event, fields),\n error: (event: string, fields?: Record<string, unknown>) => write('error', event, fields),\n }\n}\n\nfunction installToolLogging(server: McpServer, logger: ReturnType<typeof createMcpLogger>): void {\n const existingRegisterTool = server.registerTool\n const originalRegisterTool = existingRegisterTool.bind(server)\n const wrappedRegisterTool = ((\n name: string,\n config: ToolRegistrationConfig,\n handler: ToolHandler\n ) => {\n const wrapped: ToolHandler = async (args, extra) => {\n const startedAt = Date.now()\n await logger.info('tool.start', {\n tool: name,\n args: redactLogValue(args),\n })\n try {\n const result = await handler(args, extra)\n const isError = isRecord(result) && result.isError === true\n await logger.info('tool.end', {\n tool: name,\n duration_ms: Date.now() - startedAt,\n is_error: isError,\n })\n return result\n } catch (err) {\n await logger.error('tool.throw', {\n tool: name,\n duration_ms: Date.now() - startedAt,\n error: errorForLog(err),\n })\n throw err\n }\n }\n return originalRegisterTool(name, config, wrapped as never)\n }) as typeof server.registerTool\n Object.assign(wrappedRegisterTool, existingRegisterTool)\n server.registerTool = wrappedRegisterTool\n}\n\nfunction installRemoteCypherLogging(\n remoteClient: RemoteToolCaller,\n logger: ReturnType<typeof createMcpLogger>\n): void {\n const existingCallTool = remoteClient.callTool\n const originalCallTool = existingCallTool.bind(remoteClient)\n const wrappedCallTool = (async (...args: Parameters<Client['callTool']>) => {\n const input = args[0] as ToolCallInput\n const queryPayload = cypherLogPayload(input.name, input.arguments)\n const toolArgs = input.arguments ?? {}\n const startedAt = Date.now()\n if (queryPayload) {\n await logger.info('topology.start', {\n tool: input.name,\n ...queryPayload,\n })\n }\n try {\n const result = await originalCallTool(...args)\n if (queryPayload) {\n await logger.info('topology.end', {\n tool: input.name,\n duration_ms: Date.now() - startedAt,\n is_error: isRecord(result) && result.isError === true,\n })\n }\n const { warnings, search_limits } = actionLogSignalsFromResult(result)\n await appendActionLog({\n timestamp: startedAt,\n tool: input.name,\n args: toolArgs,\n outcome: 'ok',\n duration_ms: Date.now() - startedAt,\n warnings,\n search_limits,\n })\n return result\n } catch (err) {\n if (queryPayload) {\n await logger.error('cypher.throw', {\n tool: input.name,\n duration_ms: Date.now() - startedAt,\n error: errorForLog(err),\n })\n }\n await appendActionLog({\n timestamp: startedAt,\n tool: input.name,\n args: toolArgs,\n outcome: 'error',\n duration_ms: Date.now() - startedAt,\n error: (err as Error).message,\n })\n throw err\n }\n }) as typeof remoteClient.callTool\n Object.assign(wrappedCallTool, existingCallTool)\n remoteClient.callTool = wrappedCallTool\n}\n\nfunction remoteToolRequestOptions(toolName: string): Parameters<Client['callTool']>[2] | undefined {\n if (toolName === 'graph_query' || toolName === 'graph_query_batch') {\n return {\n timeout: REMOTE_GRAPH_TOOL_REQUEST_TIMEOUT_MS,\n maxTotalTimeout: REMOTE_GRAPH_TOOL_REQUEST_TIMEOUT_MS,\n }\n }\n return undefined\n}\n\nfunction isBlankArgument(value: unknown): boolean {\n if (value === undefined || value === null) return true\n if (typeof value === 'string') return value.trim() === ''\n if (Array.isArray(value)) return value.length === 0 || value.every(isBlankArgument)\n return false\n}\n\nfunction normalizeRemoteToolArguments(toolName: string, args: unknown): Record<string, unknown> {\n const normalized = isRecord(args) ? { ...args } : {}\n if (!(toolName in PUBLIC_MCP_TOOL_REQUIRED_ARGS)) return normalized\n\n const allowedArgs = PUBLIC_MCP_TOOL_ALLOWED_ARGS[toolName]\n if (!allowedArgs) return normalized\n return Object.fromEntries(Object.entries(normalized).filter(([key]) => allowedArgs.includes(key)))\n}\n\nfunction validateKnownPublicToolArguments(\n toolName: string,\n args: Record<string, unknown>\n): string | null {\n const requiredArgs = PUBLIC_MCP_TOOL_REQUIRED_ARGS[toolName]\n if (!requiredArgs) return null\n\n for (const argName of requiredArgs) {\n if (isBlankArgument(args[argName])) {\n return `Missing required argument: ${argName}`\n }\n }\n\n return null\n}\n\nfunction claudeFacingToolDescription(tool: McpTool): string {\n const baseDescription = KNOWN_PUBLIC_TOOL_DESCRIPTIONS[tool.name] ?? tool.description ?? tool.name\n const requiredArgs = PUBLIC_MCP_TOOL_REQUIRED_ARGS[tool.name]\n if (!requiredArgs) return baseDescription\n return [\n baseDescription,\n '',\n `Required arguments: ${requiredArgs.join(', ')}.`,\n 'If the user did not provide the network, ask for it before calling this tool. Do not guess a default network.',\n ].join('\\n')\n}\n\nfunction knownPublicToolAnnotations(toolName: string): Record<string, boolean> | undefined {\n if (\n toolName === 'graph_query' ||\n toolName === 'graph_query_batch' ||\n toolName.startsWith('aml_')\n ) {\n return {\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: true,\n }\n }\n return undefined\n}\n\ntype RemoteToolResult = {\n content?: ContentBlock[]\n structuredContent?: Record<string, unknown>\n _meta?: Record<string, unknown>\n isError?: boolean\n}\n\n/**\n * The text a call to the graph endpoint answers with when it throws: payment\n * guidance for a payment failure, the error otherwise. Shared by every proxied\n * tool and by the money-flow reads.\n */\nfunction remoteCallFailureText(toolName: string, err: unknown): string {\n if (err instanceof PaymentRequiredError) return err.message\n const msg = (err as Error).message ?? String(err)\n if (/\\b402\\b/.test(msg) || msg.toLowerCase().includes('payment')) {\n return (\n `Payment required for ${toolName}. This tool costs USDC on Base via x402 micropayments. ` +\n 'Next steps: run `cia wallet ready` to check funding and finish one-time payment setup, ' +\n 'run `cia wallet topup` if it says the wallet needs USDC, ' +\n 'or `cia access-key set <key>` if you have been given test access.'\n )\n }\n return `MCP call failed: ${msg}`\n}\n\nfunction promptResult(text: string, description?: string): GetPromptResult {\n return {\n description,\n messages: [\n {\n role: 'user',\n content: {\n type: 'text',\n text,\n },\n },\n ],\n }\n}\n\nfunction registerLocalPrompts(server: McpServer): void {\n server.registerPrompt(\n 'meta-network-capabilities',\n {\n title: 'Network Capabilities',\n description: 'Inspect supported networks and available tools before selecting a network.',\n argsSchema: {},\n },\n async () =>\n promptResult(\n 'Use Chain Insights meta_network_capabilities. Report only the supported networks and available tools exactly as returned; do not infer unsupported networks.',\n 'Network capabilities'\n )\n )\n\n server.registerPrompt(\n 'meta-schema',\n {\n title: 'Graph Schema',\n description: 'Read the live graph schema of one network before writing a query.',\n argsSchema: { network: NETWORK_SCHEMA },\n },\n async ({ network }) =>\n promptResult(\n `Use Chain Insights meta_schema with network ${network}. Report the labels, link types and fields exactly as returned; do not invent fields.`,\n 'Graph schema'\n )\n )\n\n server.registerPrompt(\n 'meta-usage-status',\n {\n title: 'Usage Status',\n description: \"Check the caller's public free graph_query quota.\",\n argsSchema: {},\n },\n async () =>\n promptResult(\n 'Use Chain Insights meta_usage_status. Report the quota fields exactly as returned.',\n 'Usage status'\n )\n )\n\n server.registerPrompt(\n 'meta-subscription-status',\n {\n title: 'Subscription Status',\n description: \"Check the caller's CIA subscription window, daily allowance, and tier.\",\n argsSchema: {},\n },\n async () =>\n promptResult(\n 'Use Chain Insights meta_subscription_status. Report the subscription facts exactly as returned.',\n 'Subscription status'\n )\n )\n\n server.registerPrompt(\n 'graph-query',\n {\n title: 'Graph Query',\n description: 'Run a read-only GQL/Cypher query through the Chain Insights graph endpoint.',\n argsSchema: {\n network: NETWORK_SCHEMA,\n query: z.string().describe('Read-only GQL/Cypher query'),\n },\n },\n async ({ network, query }) =>\n promptResult(\n [\n `Use Chain Insights graph_query on ${network} with this read-only GQL/Cypher query:`,\n '',\n '```gql',\n query,\n '```',\n '',\n `${GRAPH_LAYERS_TEXT} If you need schema context, first run small discovery queries such as MATCH (a:Address) RETURN a.address AS address, keys(a) AS address_properties LIMIT 5 and MATCH (:Address)-[r:FLOWS_TO]->(:Address) RETURN keys(r) AS flow_properties LIMIT 5. Return the full address when available; never shorten addresses with ellipses.`,\n ].join('\\n'),\n 'Graph query'\n )\n )\n\n server.registerPrompt(\n 'graph-query-batch',\n {\n title: 'Graph Query Batch',\n description:\n 'Run related read-only GQL/Cypher queries through the Chain Insights graph endpoint in one paid batch.',\n argsSchema: {\n network: NETWORK_SCHEMA,\n queries: z\n .string()\n .describe('JSON array of query objects with optional id and required query fields'),\n per_query_timeout_seconds: z\n .string()\n .optional()\n .describe('Optional integer timeout per query, 1-600 seconds'),\n },\n },\n async ({ network, queries, per_query_timeout_seconds }) =>\n promptResult(\n [\n `Use Chain Insights graph_query_batch on ${network} with these read-only GQL/Cypher queries:`,\n '',\n '```json',\n queries,\n '```',\n per_query_timeout_seconds\n ? `per_query_timeout_seconds: ${per_query_timeout_seconds}`\n : '',\n '',\n `${GRAPH_LAYERS_TEXT} If you need schema context, first run small discovery queries such as MATCH (a:Address) RETURN a.address AS address, keys(a) AS address_properties LIMIT 5 and MATCH (:Address)-[r:FLOWS_TO]->(:Address) RETURN keys(r) AS flow_properties LIMIT 5. Return the full address when available; never shorten addresses with ellipses.`,\n ]\n .filter(Boolean)\n .join('\\n'),\n 'Graph query batch'\n )\n )\n\n server.registerPrompt(\n 'wallet-balance',\n {\n title: 'Wallet Balance',\n description:\n 'Show the local Chain Insights payment wallet address, payment network, token, and amount.',\n argsSchema: {},\n },\n async () =>\n promptResult(\n 'Use Chain Insights wallet_balance. Show the wallet address, payment network, token, and amount exactly as returned.',\n 'Wallet balance'\n )\n )\n\n server.registerPrompt(\n 'meta-help',\n {\n title: 'Chain Insights Help',\n description: 'Show available Chain Insights tools and workflow guidance.',\n argsSchema: {},\n },\n async () =>\n promptResult(\n 'Use Chain Insights meta_help. Summarize the available tools and workflow guidance without inventing capabilities.',\n 'Chain Insights help'\n )\n )\n}\n\nfunction sanitizeStructuredContentForGraphPayload(\n structuredContent: Record<string, unknown> | undefined\n): Record<string, unknown> | undefined {\n if (!structuredContent) return undefined\n return sanitizeStructuredValue(structuredContent) as Record<string, unknown>\n}\n\nfunction sanitizeStructuredValue(value: unknown): unknown {\n if (!value || typeof value !== 'object' || Array.isArray(value)) return value\n\n const sanitized: Record<string, unknown> = {}\n for (const [key, childValue] of Object.entries(value)) {\n if (key === 'app_data') continue\n if (\n GRAPH_ARRAY_KEYS.includes(key as (typeof GRAPH_ARRAY_KEYS)[number]) &&\n Array.isArray(childValue)\n ) {\n continue\n }\n sanitized[key] = sanitizeStructuredValue(childValue)\n }\n\n return sanitized\n}\n\n// The remote result _meta passes through, MCP Apps keys (ui, ui/resourceUri)\n// included. Only the old graph-report envelope (chainInsights.graph) is dropped.\nfunction sanitizeRemoteMeta(metaValue: RemoteToolResult['_meta']): RemoteToolResult['_meta'] {\n if (!metaValue || typeof metaValue !== 'object' || Array.isArray(metaValue)) return undefined\n\n const meta = { ...metaValue } as Record<string, unknown>\n\n const chainInsights = meta.chainInsights\n if (chainInsights && typeof chainInsights === 'object' && !Array.isArray(chainInsights)) {\n const { graph: _graph, ...withoutGraph } = chainInsights as Record<string, unknown>\n if (Object.keys(withoutGraph).length > 0) meta.chainInsights = withoutGraph\n else delete meta.chainInsights\n }\n\n return Object.keys(meta).length > 0 ? (meta as RemoteToolResult['_meta']) : undefined\n}\n\n// Raw graph arrays and app_data are stripped from the aml_* answers only, the\n// graph-report tools of the old viewer. Every other answer (graph_query,\n// graph_query_batch) keeps its structuredContent exactly as the hosted server\n// returned it: the views read it.\nfunction stripsGraphPayload(toolName: string): boolean {\n return toolName.startsWith('aml_')\n}\n\nfunction normalizeRemoteToolResult(toolName: string, result: RemoteToolResult) {\n return {\n content: result.content ?? [],\n structuredContent: stripsGraphPayload(toolName)\n ? sanitizeStructuredContentForGraphPayload(result.structuredContent)\n : result.structuredContent,\n _meta: sanitizeRemoteMeta(result._meta),\n isError: result.isError,\n }\n}\n\nfunction cleanNetworkCapabilities(value: unknown) {\n const structuredContent = isRecord(value) ? value.structuredContent : undefined\n const facts = isRecord(structuredContent) ? structuredContent.facts : undefined\n const capabilities = isRecord(facts) ? facts.capabilities : undefined\n const networks =\n isRecord(capabilities) && Array.isArray(capabilities.networks) ? capabilities.networks : []\n\n return {\n schema: 'chain-insights.result.v1' as const,\n tool: 'meta_network_capabilities',\n hint: null,\n facts: {\n capabilities: mirrorGraphNetworkCapabilities({ networks }),\n },\n }\n}\n\nfunction jsonTextResult(structuredContent: Record<string, unknown>) {\n return {\n content: [{ type: 'text' as const, text: JSON.stringify(structuredContent, null, 2) }],\n structuredContent,\n isError: false,\n }\n}\n\n/**\n * Core proxy logic — exported so tests can inject dependencies directly.\n * The IIFE at the bottom calls this with real dependencies.\n *\n * stdout purity: NEVER write to stdout in this file. Use console.error() or process.stderr.write() only.\n * All diagnostic output goes to console.error() or process.stderr.write().\n */\nexport async function createProxy(): Promise<void> {\n // Lazy imports to avoid module-load side effects (critical for stdio proxy)\n const { loadConfig } = await import('../config/index.js')\n const { activeDataDir, findActiveWorkspace } = await import('../workspace/active.js')\n const { createConfiguredGraphMcpFetch, resolveGraphMcpEndpoint } = await import('./client.js')\n const { loadSchema, saveSchema } = await import('./schema-cache.js')\n\n const proxyMode = resolveMcpProxyMode()\n const workspaceArtifactsEnabled = proxyMode === 'workspace'\n const loadedConfig = await loadConfig()\n const activeWorkspace = workspaceArtifactsEnabled ? findActiveWorkspace() : null\n const config = {\n ...loadedConfig,\n dataDir: workspaceArtifactsEnabled ? activeDataDir(loadedConfig.dataDir) : loadedConfig.dataDir,\n }\n const logger = createMcpLogger(config)\n await logger.info('proxy.start', {\n data_dir: config.dataDir,\n workspace_root: activeWorkspace?.root,\n proxy_mode: proxyMode,\n graph_mcp_mode: config.graphMcpMode,\n graph_mcp_endpoint: resolveGraphMcpEndpoint(config),\n log_path: logger.filePath,\n })\n const graphMcpEndpoint = resolveGraphMcpEndpoint(config)\n\n // Inside Claude Desktop the Claude Code copy cannot draw the views. When\n // Claude Desktop's own drawing copy is configured, this copy lists no tools\n // so that each tool appears once (src/mcp/claude-desktop.ts).\n const { defersToClaudeDesktop, DEFERRED_SERVER_INSTRUCTIONS } =\n await import('./claude-desktop.js')\n if (defersToClaudeDesktop()) {\n await logger.info('proxy.deferred_to_claude_desktop', { version: PACKAGE_VERSION })\n const deferred = new McpServer(\n { name: 'chain-insights', version: PACKAGE_VERSION },\n { instructions: DEFERRED_SERVER_INSTRUCTIONS }\n )\n await deferred.connect(new StdioServerTransport())\n return\n }\n\n // npm runs no setup step on upgrade: bring the skill copies setup installed\n // up to this release. A failure here never stops the proxy.\n // CHAIN_INSIGHTS_SKILL_REFRESH=0 turns it off for people who manage the skill themselves.\n if (process.env['CHAIN_INSIGHTS_SKILL_REFRESH'] !== '0') {\n try {\n const { refreshInstalledSkills } = await import('../skill-refresh.js')\n const refresh = refreshInstalledSkills(PACKAGE_VERSION)\n if (refresh.refreshed.length > 0 || refresh.failed.length > 0) {\n await logger.info('skills.refresh', { version: PACKAGE_VERSION, ...refresh })\n }\n } catch (error) {\n await logger.info('skills.refresh', {\n version: PACKAGE_VERSION,\n error: error instanceof Error ? error.message : String(error),\n })\n }\n }\n\n // Build remote MCP client. The local Chain Insights MCP surface must still\n // start when the graph endpoint is temporarily unavailable so agents can use\n // help and wallet tools.\n const remoteClient = new Client({ name: 'chain-insights-proxy-client', version: PACKAGE_VERSION })\n let remoteConnected = false\n let remoteUnavailableMessage: string | undefined\n let mcpFetch: typeof fetch | undefined\n\n try {\n mcpFetch = await createConfiguredGraphMcpFetch(config)\n } catch (err) {\n await logger.error('remote.fetch_setup_failed', {\n endpoint: graphMcpEndpoint,\n error: errorForLog(err),\n })\n remoteUnavailableMessage = `Chain Insights Graph setup unavailable at ${graphMcpEndpoint}: ${(err as Error).message}`\n process.stderr.write(\n `Chain Insights MCP graph tools unavailable: ${remoteUnavailableMessage}. Local Chain Insights tools are still available.\\n`\n )\n }\n\n if (mcpFetch) {\n try {\n await remoteClient.connect(\n new StreamableHTTPClientTransport(new URL(graphMcpEndpoint), { fetch: mcpFetch })\n )\n remoteConnected = true\n await logger.info('remote.connect', {\n transport: 'streamable_http',\n endpoint: graphMcpEndpoint,\n })\n } catch {\n await logger.error('remote.connect_failed', {\n transport: 'streamable_http',\n endpoint: graphMcpEndpoint,\n })\n // StreamableHTTP failed — try SSE fallback (assumption A1 from RESEARCH.md)\n try {\n const { SSEClientTransport } = await import('@modelcontextprotocol/sdk/client/sse.js')\n await remoteClient.connect(\n new SSEClientTransport(new URL(graphMcpEndpoint), { fetch: mcpFetch })\n )\n remoteConnected = true\n await logger.info('remote.connect', {\n transport: 'sse',\n endpoint: graphMcpEndpoint,\n })\n } catch (err2) {\n await logger.error('remote.connect_failed', {\n transport: 'sse',\n endpoint: graphMcpEndpoint,\n error: errorForLog(err2),\n })\n remoteUnavailableMessage = `Chain Insights Graph unreachable at ${graphMcpEndpoint}: ${(err2 as Error).message}`\n process.stderr.write(\n `Chain Insights MCP graph tools unavailable: ${remoteUnavailableMessage}. Local Chain Insights tools are still available.\\n`\n )\n }\n }\n }\n if (remoteConnected)\n installRemoteCypherLogging(remoteClient as unknown as RemoteToolCaller, logger)\n\n // Schema cache check — skip remote listTools call on cache hit\n let tools: McpTool[] | null = await loadSchema(graphMcpEndpoint)\n\n if (!tools && remoteConnected) {\n // Cache miss — fetch tools from remote (client is already connected above)\n const result = await remoteClient.listTools()\n tools = result.tools as McpTool[]\n await saveSchema(tools, graphMcpEndpoint)\n await logger.info('schema.tools_loaded', {\n source: 'remote',\n count: tools.length,\n })\n } else if (tools) {\n await logger.info('schema.tools_loaded', {\n source: 'cache',\n count: tools.length,\n })\n } else {\n tools = fallbackGraphPrimitiveTools()\n await logger.info('schema.tools_loaded', {\n source: 'unavailable',\n count: tools.length,\n })\n }\n const remoteToolNames = new Set((tools ?? []).map((tool) => tool.name))\n\n // Build local stdio proxy server\n const server = new McpServer(\n { name: 'chain-insights', version: PACKAGE_VERSION },\n {\n instructions: workspaceArtifactsEnabled ? SERVER_INSTRUCTIONS : STATELESS_SERVER_INSTRUCTIONS,\n }\n )\n installToolLogging(server, logger)\n\n if (remoteConnected) {\n try {\n await remoteClient.listPrompts()\n } catch (err) {\n await logger.error('remote.prompts_failed', {\n endpoint: graphMcpEndpoint,\n error: errorForLog(err),\n })\n process.stderr.write(\n `Chain Insights MCP remote prompt metadata unavailable at ${graphMcpEndpoint}: ${(err as Error).message}\\n`\n )\n }\n }\n\n registerLocalPrompts(server)\n\n server.registerTool(\n 'meta_network_capabilities',\n {\n title: 'Network Capabilities',\n description: KNOWN_PUBLIC_TOOL_DESCRIPTIONS.meta_network_capabilities,\n inputSchema: EMPTY_INPUT_SCHEMA,\n annotations: {\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false,\n },\n },\n async () => {\n if (remoteConnected && remoteToolNames.has('network_capabilities')) {\n try {\n const result = await remoteClient.callTool({\n name: 'network_capabilities',\n arguments: {},\n })\n return jsonTextResult(cleanNetworkCapabilities(result))\n } catch (err) {\n return {\n content: [\n {\n type: 'text' as const,\n text: `Network capabilities failed: ${(err as Error).message}`,\n },\n ],\n isError: true,\n }\n }\n }\n return jsonTextResult(cleanNetworkCapabilities(undefined))\n }\n )\n\n // meta_schema: the live graph schema of one network, built from the catalog\n // statements of the graph endpoint and kept on disk for 24 hours. A build reads\n // through the proxy's one remote session; a cache hit reads nothing.\n const schemaDependencies: GraphSchemaDependencies = {\n endpoint: graphMcpEndpoint,\n withClient: async (fn) => {\n if (!remoteConnected) {\n throw new GraphSchemaError(\n `${remoteUnavailableMessage ?? `Chain Insights Graph is not connected at ${graphMcpEndpoint}`}. Restart the Chain Insights MCP proxy after the endpoint is reachable.`\n )\n }\n return fn(remoteClient)\n },\n describeFailure: (err) => remoteCallFailureText('meta_schema', err),\n }\n server.registerTool(\n 'meta_schema',\n {\n title: META_SCHEMA_TITLE,\n description: KNOWN_PUBLIC_TOOL_DESCRIPTIONS.meta_schema,\n inputSchema: {\n network: NETWORK_SCHEMA,\n refresh: z\n .boolean()\n .optional()\n .describe('Rebuild the schema now instead of reading the 24 hour cache.'),\n },\n annotations: {\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: true,\n },\n },\n async (args) => handleMetaSchema(args, schemaDependencies)\n )\n\n server.registerTool(\n 'meta_usage_status',\n {\n title: 'Usage Status',\n description: KNOWN_PUBLIC_TOOL_DESCRIPTIONS.meta_usage_status,\n inputSchema: EMPTY_INPUT_SCHEMA,\n _meta: VIEW_TOOL_META,\n annotations: {\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: true,\n },\n },\n async () => {\n try {\n if (!remoteConnected) {\n return {\n content: [\n {\n type: 'text' as const,\n text: `${remoteUnavailableMessage ?? `Chain Insights Graph is not connected at ${graphMcpEndpoint}`}. Restart the Chain Insights MCP proxy after the endpoint is reachable.`,\n },\n ],\n isError: true,\n }\n }\n if (!remoteToolNames.has('usage_status')) {\n return jsonTextResult(primitiveBackendUsageStatus(graphMcpEndpoint))\n }\n const result = (await remoteClient.callTool({\n name: 'usage_status',\n arguments: {},\n })) as RemoteToolResult\n const structuredContent = isRecord(result.structuredContent)\n ? { ...result.structuredContent, tool: 'meta_usage_status' }\n : undefined\n return {\n content: structuredContent\n ? [{ type: 'text' as const, text: JSON.stringify(structuredContent, null, 2) }]\n : (result.content ?? []),\n structuredContent,\n _meta: result._meta,\n isError: result.isError,\n }\n } catch (err) {\n return {\n content: [\n { type: 'text' as const, text: `Usage status failed: ${(err as Error).message}` },\n ],\n isError: true,\n }\n }\n }\n )\n\n server.registerTool(\n 'meta_subscription_status',\n {\n title: 'Subscription Status',\n description: KNOWN_PUBLIC_TOOL_DESCRIPTIONS.meta_subscription_status,\n inputSchema: EMPTY_INPUT_SCHEMA,\n _meta: VIEW_TOOL_META,\n annotations: {\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: true,\n },\n },\n async () => {\n // Local proxy shape discipline: the tool is always present and never\n // throws. When the server tool is absent or errors, the facts explain\n // the unavailability.\n try {\n if (!remoteConnected) {\n return jsonTextResult(\n unavailableSubscriptionStatus(\n graphMcpEndpoint,\n remoteUnavailableMessage ??\n `Chain Insights Graph is not connected at ${graphMcpEndpoint}`\n )\n )\n }\n if (!remoteToolNames.has('subscription_status')) {\n return jsonTextResult(\n unavailableSubscriptionStatus(\n graphMcpEndpoint,\n 'The graph backend exposes primitive graph tools but no subscription_status tool.'\n )\n )\n }\n const walletAddress = await localSubscriptionWalletAddress()\n if (!walletAddress) {\n return jsonTextResult(\n unavailableSubscriptionStatus(\n graphMcpEndpoint,\n 'No local payment wallet is configured; run `cia wallet create` or `cia wallet import` first.'\n )\n )\n }\n const result = (await remoteClient.callTool({\n name: 'subscription_status',\n arguments: { wallet: walletAddress },\n })) as RemoteToolResult\n if (result.isError === true) {\n const firstText = Array.isArray(result.content)\n ? result.content.find(\n (block): block is Extract<ContentBlock, { type: 'text' }> =>\n block.type === 'text' && typeof block.text === 'string'\n )\n : undefined\n const reason = firstText?.text\n ? `subscription_status failed: ${firstText.text}`\n : 'subscription_status failed: the server returned an error'\n return jsonTextResult(unavailableSubscriptionStatus(graphMcpEndpoint, reason))\n }\n const structuredContent = isRecord(result.structuredContent)\n ? { ...result.structuredContent, tool: 'meta_subscription_status' }\n : undefined\n return {\n content: structuredContent\n ? [{ type: 'text' as const, text: JSON.stringify(structuredContent, null, 2) }]\n : (result.content ?? []),\n structuredContent,\n _meta: result._meta,\n isError: result.isError,\n }\n } catch (err) {\n return jsonTextResult(\n unavailableSubscriptionStatus(\n graphMcpEndpoint,\n `subscription_status failed: ${(err as Error).message}`\n )\n )\n }\n }\n )\n\n server.registerTool(\n 'wallet_balance',\n {\n title: 'Wallet Balance',\n description: KNOWN_PUBLIC_TOOL_DESCRIPTIONS.wallet_balance,\n inputSchema: EMPTY_INPUT_SCHEMA,\n annotations: {\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: true,\n },\n },\n async () => {\n try {\n const { formatWalletBalanceResult, getWalletAccount, getWalletBalanceResult } =\n await import('../wallet/tools.js')\n const account = await getWalletAccount()\n const structuredContent = await getWalletBalanceResult(account)\n return {\n content: [{ type: 'text' as const, text: formatWalletBalanceResult(structuredContent) }],\n structuredContent: structuredContent as unknown as Record<string, unknown>,\n isError: false,\n }\n } catch (err) {\n return {\n content: [{ type: 'text' as const, text: `Balance failed: ${(err as Error).message}` }],\n isError: true,\n }\n }\n }\n )\n // graph_expand, the one dedicated tool, for the view only (visibility\n // [\"app\"]): a node click is at most five anchored topology graph_query reads\n // (the node, then two chunks a side), a link click one facts graph_query\n // read, all sent through the remote client, the\n // path and payment wrapping a graph_query call takes, so each read is billed\n // as a graph query.\n const flowsDependencies: FlowsDependencies = {\n graphQuery: async (args) => {\n const options = remoteToolRequestOptions('graph_query')\n return (await withRateLimitRetry(() =>\n remoteClient.callTool({ name: 'graph_query', arguments: args }, undefined, options)\n )) as GraphQueryAnswer\n },\n describeFailure: (err) => remoteCallFailureText('graph_query', err),\n unavailable: () =>\n remoteConnected\n ? undefined\n : `${remoteUnavailableMessage ?? `Chain Insights Graph is not connected at ${graphMcpEndpoint}`}. Restart the Chain Insights MCP proxy after the endpoint is reachable.`,\n }\n server.registerTool(\n GRAPH_EXPAND_TOOL,\n {\n title: GRAPH_EXPAND_TITLE,\n description: GRAPH_EXPAND_DESCRIPTION,\n inputSchema: {\n network: z.string(),\n address: z.string().optional(),\n in_offset: z.number().int().min(0).max(FLOWS_MAX_OFFSET).optional(),\n out_offset: z.number().int().min(0).max(FLOWS_MAX_OFFSET).optional(),\n from: z.string().optional(),\n to: z.string().optional(),\n day: z.string().optional(),\n },\n annotations: VIEW_TOOL_ANNOTATIONS,\n _meta: APP_ONLY_TOOL_META,\n },\n async (args) => handleGraphExpand(args, flowsDependencies)\n )\n\n server.registerTool(\n 'meta_help',\n {\n title: 'Chain Insights Help',\n description: KNOWN_PUBLIC_TOOL_DESCRIPTIONS.meta_help,\n inputSchema: EMPTY_INPUT_SCHEMA,\n annotations: {\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false,\n },\n },\n async () => ({\n content: [\n {\n type: 'text' as const,\n text: workspaceArtifactsEnabled\n ? [\n 'Chain Insights helps AI agents run AML investigation workflows.',\n '',\n CHAIN_INSIGHTS_WORKFLOW,\n '',\n 'Investigation tools:',\n '- meta_network_capabilities: supported networks and available tools. Takes no arguments.',\n '- meta_schema: read the live graph schema of one network: labels, link types and field names. Cached for 24 hours.',\n '- meta_usage_status: check the caller public free graph_query quota.',\n '- meta_subscription_status: check the caller CIA subscription window end, daily allowance, consumption, and tier.',\n '- graph_query: run read-only GQL/Cypher through the universal graph endpoint. Use USE topology or USE facts.',\n '- graph_query_batch: run related read-only graph-language queries through one paid graph call.',\n '',\n PICTURE_HELP_LINE,\n '',\n 'Wallet tools:',\n '- wallet_balance: show the local payment wallet address, payment network, token, and amount.',\n '- meta_help: show this overview.',\n ].join('\\n')\n : [\n 'Chain Insights stateless AML proxy for host applications.',\n '',\n 'Available graph-backed tools:',\n '- meta_network_capabilities: supported networks and available tools. Takes no arguments.',\n '- meta_schema: read the live graph schema of one network: labels, link types and field names. Cached for 24 hours.',\n '- meta_usage_status: check the caller public free graph_query quota.',\n '- meta_subscription_status: check the caller CIA subscription window end, daily allowance, consumption, and tier.',\n '- graph_query: run read-only GQL/Cypher through the universal graph endpoint. Use USE topology or USE facts.',\n '- graph_query_batch: run related read-only graph-language queries through one paid graph call.',\n '',\n PICTURE_HELP_LINE,\n ].join('\\n'),\n },\n ],\n isError: false,\n })\n )\n\n // Register each remote tool locally — passthrough proxy pattern\n for (const tool of tools ?? []) {\n if (HIDDEN_REMOTE_TOOL_NAMES.has(tool.name)) continue\n if (LOCAL_TOOL_NAMES.has(tool.name)) continue\n // A tool only a view of the graph endpoint may call has no view here.\n if (isAppOnlyTool(tool)) continue\n const inputSchema = knownPublicToolInputSchema(tool.name) ?? z.object({}).passthrough()\n const handler = async (args: unknown) => {\n try {\n if (!remoteConnected) {\n return {\n content: [\n {\n type: 'text' as const,\n text: `${remoteUnavailableMessage ?? `Chain Insights Graph is not connected at ${graphMcpEndpoint}`}. Restart the Chain Insights MCP proxy after the endpoint is reachable.`,\n },\n ],\n isError: true,\n }\n }\n const normalizedArgs = normalizeRemoteToolArguments(tool.name, args)\n const validationError = validateKnownPublicToolArguments(tool.name, normalizedArgs)\n if (validationError) {\n return {\n content: [{ type: 'text' as const, text: validationError }],\n isError: true,\n }\n }\n const request = {\n name: tool.name,\n arguments: normalizedArgs,\n }\n const requestOptions = remoteToolRequestOptions(tool.name)\n const result = await withRateLimitRetry(() =>\n requestOptions\n ? remoteClient.callTool(request, undefined, requestOptions)\n : remoteClient.callTool(request)\n )\n return normalizeRemoteToolResult(tool.name, result as RemoteToolResult)\n } catch (err) {\n const limited = rateLimitedResult(tool.name, err)\n if (limited) return limited\n return {\n content: [{ type: 'text' as const, text: remoteCallFailureText(tool.name, err) }],\n isError: true,\n }\n }\n }\n // The graph endpoint's own MCP Apps metadata (_meta.ui, ui/resourceUri)\n // is never forwarded: the proxy is the only source of views. graph_query\n // gets the view the proxy serves.\n const annotations = knownPublicToolAnnotations(tool.name)\n const drawsView = tool.name === 'graph_query'\n const toolConfig = {\n title: tool.title ?? KNOWN_PUBLIC_TOOL_TITLES[tool.name],\n description: claudeFacingToolDescription(tool),\n inputSchema,\n ...(annotations ? { annotations } : {}),\n ...(drawsView ? { _meta: VIEW_TOOL_META } : {}),\n }\n\n server.registerTool(tool.name, toolConfig, handler)\n }\n\n // The local top-up view (wallet address and QR code). It stays in the local\n // proxy: the wallet it funds is a local file, and the hosted connector never\n // offers it (ruling 2026-10-05).\n const { registerTopupView, TOPUP_VIEW_URI } = await import('../wallet/mcp-proxy/topup-server.js')\n await registerTopupView(server, { walletAddress: localTopupWalletAddress })\n\n // ui://chain-insights/view, read from the package. Nothing is fetched.\n server.registerResource(\n 'Chain Insights view',\n CLAUDE_VIEW_URI,\n {\n title: 'Chain Insights view',\n description:\n 'The Chain Insights view for hosts that draw MCP apps: money flows, query results and balance, in light and dark.',\n mimeType: MCP_APP_MIME_TYPE,\n _meta: VIEW_RESOURCE_META,\n },\n async () => ({\n contents: [\n {\n uri: CLAUDE_VIEW_URI,\n mimeType: MCP_APP_MIME_TYPE,\n text: readClaudeViewHtml(),\n _meta: VIEW_RESOURCE_META,\n },\n ],\n })\n )\n\n // Connect to stdio transport — after this line, stdout belongs to MCP\n const transport = new StdioServerTransport()\n await server.connect(transport)\n await logger.info('proxy.ready', {\n tools: [\n ...[...LOCAL_TOOL_NAMES].filter((name) => !APP_ONLY_LOCAL_TOOL_NAMES.has(name)),\n ...(tools ?? [])\n .filter((tool) => !isAppOnlyTool(tool))\n .map((tool) => tool.name)\n .filter((name) => !HIDDEN_REMOTE_TOOL_NAMES.has(name) && !LOCAL_TOOL_NAMES.has(name)),\n ].length,\n ui_resources: [CLAUDE_VIEW_URI, TOPUP_VIEW_URI],\n })\n\n // Signal handling — clean shutdown\n const shutdown = async () => {\n await logger.info('proxy.shutdown')\n transport.close()\n process.exit(0)\n }\n process.on('SIGINT', () => {\n void shutdown()\n })\n process.on('SIGTERM', () => {\n void shutdown()\n })\n}\n\n// Entry point — only execute when run as the main module (not when imported by tests)\n// Using process.argv check to detect direct execution vs import\nif (process.argv[1] && import.meta.url.includes(process.argv[1].replace(/\\\\/g, '/'))) {\n createProxy().catch((err) => {\n process.stderr.write(`Chain Insights MCP proxy startup failed: ${(err as Error).message}\\n`)\n process.exit(1)\n })\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"mcp-proxy.mjs","names":[],"sources":["../src/mcp/rate-limit.ts","../src/mcp/layer-routing.ts","../src/mcp/proxy.ts"],"sourcesContent":["/**\n * The graph endpoint limits requests per address. Its refusal arrives as a\n * thrown HTTP error, \"too many requests from this address; retry after 3\n * seconds\", not as a typed tool reply. Every proxied tool reads only, and a\n * refused request is not billed, so one wait and one resend are safe.\n *\n * When the resend is refused too, or the endpoint asks for a longer wait than\n * the proxy holds a call for, the caller gets the refusal envelope the graph\n * server uses for every other limit: class `capacity`. The skills tell an\n * agent what to do with that class.\n */\n\n/** The longest wait the proxy holds a call for before it sends it once more. */\nexport const MAX_RETRY_WAIT_MS = 10_000\n/** The wait when the endpoint names none. The skills say at least 5 seconds. */\nexport const DEFAULT_RETRY_WAIT_MS = 5_000\nconst MIN_RETRY_WAIT_MS = 1_000\n\nconst RATE_LIMITED = /\\b429\\b|too many requests|rate.?limit/i\nconst RETRY_AFTER = /retry after (\\d+) seconds?/i\n\nfunction messageOf(err: unknown): string {\n return err instanceof Error ? err.message : String(err)\n}\n\n/**\n * The wait the endpoint asks for, in milliseconds, when err is its rate\n * limit, and null for any other failure.\n */\nexport function rateLimitWaitMs(err: unknown): number | null {\n const code = err && typeof err === 'object' ? (err as { code?: unknown }).code : undefined\n const message = messageOf(err)\n if (code !== 429 && !RATE_LIMITED.test(message)) return null\n const seconds = RETRY_AFTER.exec(message)?.[1]\n return seconds ? Math.max(Number(seconds) * 1000, MIN_RETRY_WAIT_MS) : DEFAULT_RETRY_WAIT_MS\n}\n\nexport type Sleep = (ms: number) => Promise<void>\n\nconst defaultSleep: Sleep = (ms) => new Promise((done) => setTimeout(done, ms))\n\n/**\n * Runs call. When the endpoint refuses it for the rate limit and asks for a\n * wait of at most MAX_RETRY_WAIT_MS, waits that long and runs it once more.\n * Any other failure, and the second refusal, is thrown to the caller.\n */\nexport async function withRateLimitRetry<T>(\n call: () => Promise<T>,\n sleep: Sleep = defaultSleep\n): Promise<T> {\n try {\n return await call()\n } catch (err) {\n const wait = rateLimitWaitMs(err)\n if (wait === null || wait > MAX_RETRY_WAIT_MS) throw err\n await sleep(wait)\n return await call()\n }\n}\n\n/**\n * The reply for a call the endpoint refused for the rate limit: the text the\n * agent reads and the `capacity` envelope the skills act on. Null when err is\n * not the rate limit.\n */\nexport function rateLimitedResult(toolName: string, err: unknown) {\n const wait = rateLimitWaitMs(err)\n if (wait === null) return null\n const seconds = Math.ceil(wait / 1000)\n const fix = `Wait ${seconds} seconds, then send the same request once.`\n return {\n content: [\n {\n type: 'text' as const,\n text: `${toolName} was not run: Chain Insights Graph is limiting requests from this address. ${fix}`,\n },\n ],\n structuredContent: {\n schema: 'chain-insights.result.v1',\n tool: toolName,\n hint: null,\n error_detail: {\n code: 'rate_limited',\n rule: 'requests_per_address',\n class: 'capacity',\n fix,\n example: '',\n },\n },\n isError: true,\n }\n}\n","// The one source of the routing rule of the three read layers, and of the move\n// that each class of refusal asks of an agent.\n//\n// Three places teach them: the graph hints that the MCP proxy serves\n// (GRAPH_SCHEMA_HINTS in proxy.ts builds its two routing hints from this file),\n// the `chain-insights-cypher` skill and docs/graph-tools.md. The skill and the\n// guide are Markdown and hold the lines word for word. tests/layer-routing.test.ts\n// fails when one of the three differs from this file by one word.\n//\n// The rule follows the graph server's own: topology searches outward from one\n// known address, while facts and chain look up one known thing. The wording asks\n// the agent to say first what the question names (an address, a pair with a day,\n// a transaction, a block), because a question that names none of them is served\n// on no layer today. Nothing here is a limit. A limit that the server publishes\n// is read from `meta_network_capabilities`, and the text names the field, never\n// the number.\n//\n// `USE chain` serves the four labels of the server's chain catalogue and no\n// other: a transaction by its `tx_id`, a block by its `block_height` or its\n// `block_hash`, an address at one block (`Address`, with an optional `at_block`),\n// and the head, which takes no key. A kind (`:Account`, `:Contract`) is never a\n// lookup label: the `Address` lookup reads it as `is_contract` and `nonce`.\n// tests/fixtures/chain-catalogue.json pins the labels, and\n// tests/chain-catalogue-pin.test.ts holds every chain recipe and the chain\n// routing line to them. The depth below which a past block is served is a limit\n// that the server publishes, so the text names its field and never its number.\n\n/** The routing rule, in one sentence. The server states it in the same words. */\nexport const ROUTING_RULE =\n 'Route by what the question names: an address, a pair with a day, a transaction or a block. Topology searches outward from an address, while facts and chain look up one known thing.'\n\n/** The rule in three lines, one for each layer. */\nexport const ROUTING_LINES = [\n 'I know an address and want its links, senders, receivers, hops or a route: `USE topology`, anchored on that address.',\n 'I know both addresses of a pair and one day, and want the rows of that day: `USE facts`.',\n \"I want the chain's own record of a transaction by its `tx_id`, a block by its `block_height` or `block_hash`, an address at one block (`Address`), or the head: `USE chain`.\",\n] as const\n\n/** The chain layer reads one address at one block. A search for counterparties stays on topology. */\nexport const ROUTING_ADDRESS_ON_CHAIN =\n '`USE chain` with `Address` reads the balance, the nonce or the kind of one address, now or at a past block with `at_block`. A past block must be at least `chain_admission.at_block_min_depth` blocks below the tip. To find the counterparties of an address, search `USE topology`, then read the pair and one day on `USE facts`.'\n\n/** What to do when two layers fit. */\nexport const ROUTING_TWO_FIT =\n 'A transaction is a chain question: read it on `USE chain` by its `tx_id`. Chain gives its `block_date`; the transfers between a pair on that day are a `USE facts` read.'\n\n/** The key that each layer hands to the next. Each key keeps its name. */\nexport const ROUTING_HANDOFF =\n 'The layers hand each other keys, and each key keeps its name. Topology gives the pair and the first and last seen time. Facts gives the `tx_id` and the `block_height`. Chain takes an `address`, a `tx_id`, a `block_height` or a `block_hash`, and gives the `block_date` of a transaction or a block.'\n\n/** A whole-chain question is a time-bounded topology search; a server without search mode refuses it once. */\nexport const ROUTING_NOT_SERVED =\n 'A question about the whole chain (recent activity, the biggest senders, a top list) is a `USE topology` search: bound it by time with `f.last_seen_timestamp >= <epoch ms>` on the `FLOWS_TO` link, then sort and `LIMIT`. A search has 10 s. If the server refuses it with `anchor_missing`, it does not serve searches yet: say so in one line and ask for an address.'\n\n/** What a refusal carries, and which part of it decides the move. */\nexport const MOVE_INTRO =\n 'A refused, killed, busy or failed query comes back with `error_detail`: `code`, `rule`, `class`, `fix` and `example`. The `class` decides your next move.'\n\n/** The move for each class of the refusal envelope. */\nexport const MOVE_BY_CLASS = {\n refused: 'Class `refused`: read `fix`, rewrite the query from `example`, and send it once.',\n killed: 'Class `killed`: narrow the query (fewer properties, rows or hops) and send it once.',\n capacity: 'Class `capacity`: wait at least 5 seconds, then send the same query once.',\n failed: 'Class `failed`: tell the user what is down. Do not retry.',\n} as const\n\n/** A `fix` that names another layer. */\nexport const MOVE_OTHER_LAYER = 'A `fix` that names another layer means move to that layer.'\n\n/** One try, then the user. Never a loop. */\nexport const MOVE_ONCE =\n 'One rewrite or one retry for each query. When it is refused or busy again, stop and tell the user. Quote the `fix` text and send nothing more for that question.'\n\n/** After a refusal or a kill the server has already said no. */\nexport const MOVE_NEVER_SAME = 'Never send the same text again after `refused` or `killed`.'\n\n/** A batch has good members that already ran. */\nexport const MOVE_BATCH = 'In a batch, send again only the members that came back `capacity`.'\n\n/** The routing sentences, in the order a reader meets them. */\nexport function routingLines(): string[] {\n return [\n ROUTING_RULE,\n ...ROUTING_LINES,\n ROUTING_ADDRESS_ON_CHAIN,\n ROUTING_TWO_FIT,\n ROUTING_HANDOFF,\n ROUTING_NOT_SERVED,\n ]\n}\n\n/** The move-by-class sentences, in the order a reader meets them. */\nexport function moveLines(): string[] {\n return [\n MOVE_INTRO,\n MOVE_BY_CLASS.refused,\n MOVE_BY_CLASS.killed,\n MOVE_BY_CLASS.capacity,\n MOVE_BY_CLASS.failed,\n MOVE_OTHER_LAYER,\n MOVE_ONCE,\n MOVE_NEVER_SAME,\n MOVE_BATCH,\n ]\n}\n\n/**\n * The routing rule and its three lines, with the question that no layer serves,\n * as one paragraph. The proxy serves it first among its instructions: a host\n * that keeps only the start of the instructions (Claude Code keeps 2 KB) still\n * shows the agent where each kind of question goes, and which kind goes nowhere.\n */\nexport function routingHead(): string {\n return `Pick the layer first. ${ROUTING_RULE} 1. ${ROUTING_LINES[0]} 2. ${ROUTING_LINES[1]} 3. ${ROUTING_LINES[2]} ${ROUTING_NOT_SERVED}`\n}\n\n/**\n * The two hints that the MCP proxy serves for routing and for refusals. Each is\n * one line of the served instructions.\n */\nexport function routingHintLines(): string[] {\n return [\n `- Pick the layer first. ${ROUTING_RULE} 1. ${ROUTING_LINES[0]} 2. ${ROUTING_LINES[1]} 3. ${ROUTING_LINES[2]} ${ROUTING_NOT_SERVED} ${ROUTING_ADDRESS_ON_CHAIN} ${ROUTING_TWO_FIT} ${ROUTING_HANDOFF} The lookups that USE chain serves are listed in chain_admission.lookups of meta_network_capabilities. Read every limit that the server publishes from meta_network_capabilities (chain_admission and, when the server sends them, topology_admission and facts_admission). Never write a limit down.`,\n `- Act on a refusal by its class. ${moveLines().join(' ')}`,\n ]\n}\n","import { readFileSync } from 'node:fs'\nimport { appendFile, mkdir } from 'node:fs/promises'\nimport path from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'\nimport { Client } from '@modelcontextprotocol/sdk/client/index.js'\nimport { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'\nimport type { ContentBlock, GetPromptResult } from '@modelcontextprotocol/sdk/types.js'\nimport * as z from 'zod'\nimport type { InvestigatorConfig } from '../config/schema.js'\nimport { PACKAGE_VERSION } from '../version.js'\nimport type { McpTool } from './schema-cache.js'\nimport { rateLimitedResult, withRateLimitRetry } from './rate-limit.js'\nimport {\n HIDDEN_REMOTE_TOOL_NAMES,\n PUBLIC_MCP_TOOL_ALLOWED_ARGS,\n PUBLIC_MCP_TOOL_REQUIRED_ARGS,\n isAppOnlyTool,\n} from './tool-visibility.js'\nimport { PaymentRequiredError } from './client.js'\nimport { primitiveBackendUsageStatus } from './usage-status.js'\nimport { unavailableSubscriptionStatus } from './subscription-status.js'\nimport { mirrorGraphNetworkCapabilities } from './capabilities.js'\nimport { ROUTING_NOT_SERVED, routingHead, routingHintLines } from './layer-routing.js'\nimport {\n GraphSchemaError,\n META_SCHEMA_DESCRIPTION,\n META_SCHEMA_TITLE,\n handleMetaSchema,\n type GraphSchemaDependencies,\n} from './graph-schema.js'\nimport { actionLogSignalsFromResult, appendActionLog } from './action-log.js'\nimport {\n FLOWS_MAX_OFFSET,\n GRAPH_EXPAND_DESCRIPTION,\n GRAPH_EXPAND_TITLE,\n GRAPH_EXPAND_TOOL,\n handleGraphExpand,\n type FlowsDependencies,\n type GraphQueryAnswer,\n} from './flows.js'\n\nconst LOCAL_TOOL_NAMES = new Set([\n 'meta_network_capabilities',\n 'meta_schema',\n 'meta_usage_status',\n 'meta_subscription_status',\n 'meta_help',\n 'meta_rules',\n 'wallet_balance',\n 'wallet_topup',\n GRAPH_EXPAND_TOOL,\n])\n// Local tools only the view calls (visibility [\"app\"]): never named to the model.\nconst APP_ONLY_LOCAL_TOOL_NAMES = new Set([GRAPH_EXPAND_TOOL])\nconst GRAPH_ARRAY_KEYS = ['nodes', 'edges', 'flows', 'edge_anchors'] as const\n\n// The Chain Insights view: one HTML file, served by this proxy and by nothing\n// else. The graph endpoint serves no view; a view, a ui:// resource or a\n// _meta.ui it advertises is never forwarded. The tool result says which view\n// the file draws: a graph, a chart or a table from the columns of a graph_query\n// answer, the balance (meta_usage_status, meta_subscription_status), or the\n// answer of a click (graph_expand). The metadata below is the wire contract of\n// the Claude views, character for character.\nexport const CLAUDE_VIEW_URI = 'ui://chain-insights/view'\nexport const MCP_APP_MIME_TYPE = 'text/html;profile=mcp-app'\n// A tool whose answer the view draws: the resource under the current key and\n// the legacy flat key hosts still read.\nconst VIEW_TOOL_META = {\n ui: { resourceUri: CLAUDE_VIEW_URI },\n 'ui/resourceUri': CLAUDE_VIEW_URI,\n}\n// A tool only the view calls: the host keeps it away from the model.\nconst APP_ONLY_TOOL_META = { ui: { resourceUri: CLAUDE_VIEW_URI, visibility: ['app'] } }\nconst VIEW_RESOURCE_META = {\n ui: { prefersBorder: false, csp: { resourceDomains: ['https://assets.claude.ai'] } },\n}\nconst VIEW_TOOL_ANNOTATIONS = { readOnlyHint: true }\nconst CLAUDE_VIEW_FILE = 'claude-view.html'\n\nlet claudeViewHtml: string | undefined\n\n/**\n * The view file shipped in the package: dist/apps/claude-view.html next to the\n * built proxy, or src/mcp/apps/claude-view.html when run from source. Read once.\n */\nexport function readClaudeViewHtml(): string {\n if (claudeViewHtml !== undefined) return claudeViewHtml\n const here = path.dirname(fileURLToPath(import.meta.url))\n const candidates = [\n path.join(here, 'apps', CLAUDE_VIEW_FILE),\n path.join(here, '..', 'src', 'mcp', 'apps', CLAUDE_VIEW_FILE),\n ]\n for (const candidate of candidates) {\n try {\n claudeViewHtml = readFileSync(candidate, 'utf8')\n return claudeViewHtml\n } catch {\n /* try the next place */\n }\n }\n throw new Error(`The Chain Insights view file ${CLAUDE_VIEW_FILE} is missing from the package`)\n}\n\nexport type McpProxyMode = 'workspace' | 'stateless'\n\nexport function resolveMcpProxyMode(env: NodeJS.ProcessEnv = process.env): McpProxyMode {\n const raw = env['CHAIN_INSIGHTS_MCP_PROXY_MODE']?.trim().toLowerCase()\n if (!raw || raw === 'stateless') return 'stateless'\n if (raw === 'workspace') return 'workspace'\n if (raw === 'no-workspace' || raw === 'workspace-less') return 'stateless'\n throw new Error(`CHAIN_INSIGHTS_MCP_PROXY_MODE must be workspace or stateless; got \"${raw}\"`)\n}\n\nconst GRAPH_LAYERS_TEXT =\n \"Use USE topology for topology (address/FLOWS_TO/OPERATED_BY/LINKED graph with SWAPPED, ADDED_LIQUIDITY, REMOVED_LIQUIDITY, BRIDGED and the Pool label, unified recent+historical) and USE facts for bounded TRANSFER, SWAP, LIQUIDITY_ADD, LIQUIDITY_REMOVE and BRIDGE_CROSSING rows and enrichment, and USE chain for the chain node's own record of one known transaction, block, address or the head.\"\n\nconst KNOWN_PUBLIC_TOOL_DESCRIPTIONS: Record<string, string> = {\n meta_network_capabilities:\n 'Return the current Chain Insights network and tool support matrix. Takes no arguments: send {}.',\n meta_schema: META_SCHEMA_DESCRIPTION,\n meta_usage_status: \"Return the caller's public free graph_query quota for the current UTC day.\",\n meta_subscription_status:\n \"Return the caller's CIA subscription window end, daily allowance, consumption, and tier.\",\n meta_help: 'Show a short guide to Chain Insights tools and workflow.',\n meta_rules:\n 'Return the Chain Insights rules: how to write graph_query and graph_query_batch reads that are served, how to name columns so the answer is drawn as a graph, a time series or a table, and how to write the answer. Call it once per chat, before the first graph query, when the chain-insights-cypher skill is not loaded. Takes no arguments: send {}.',\n wallet_balance:\n 'Show the local Chain Insights payment wallet address, payment network, token, and amount.',\n graph_query: `Run a read-only GQL/Cypher query through the Chain Insights graph endpoint. ${GRAPH_LAYERS_TEXT} ${ROUTING_NOT_SERVED} Preserve full addresses exactly, and write every address in full, all 42 characters, in your answer: never shorten one with ... or ….`,\n graph_query_batch:\n 'Run multiple read-only GQL/Cypher queries through the Chain Insights graph endpoint in one paid batch. Prefer this for related topology/facts reads. For a time series across days, send one USE facts total per day with the query id set to the day, \"YYYY-MM-DD\": the view joins them into one series. Write every address in full in your answer, all 42 characters.',\n}\n// Titles for proxied tools whose graph endpoint definition carries none.\nconst KNOWN_PUBLIC_TOOL_TITLES: Record<string, string> = {\n graph_query: 'Graph Query',\n graph_query_batch: 'Graph Query Batch',\n}\nconst FALLBACK_GRAPH_PRIMITIVE_TOOL_NAMES = ['graph_query', 'graph_query_batch'] as const\n\ntype ToolInputShape = Record<string, z.ZodTypeAny>\ntype ToolHandler = (args: unknown, extra?: unknown) => Promise<unknown> | unknown\ntype ToolRegistrationConfig = Parameters<McpServer['registerTool']>[1]\ntype ToolCallInput = { name: string; arguments?: Record<string, unknown> }\ntype RemoteToolCaller = {\n callTool: Client['callTool']\n}\nconst NETWORK_DESCRIPTION =\n 'Network to query: robinhood, the one public network. CIA does not pick a default network, so always pass it. meta_network_capabilities (no arguments) lists the live networks.'\nconst NETWORK_SCHEMA = z.string().min(1).describe(NETWORK_DESCRIPTION)\n\nconst EMPTY_INPUT_SCHEMA = z.strictObject({})\nconst REMOTE_GRAPH_TOOL_REQUEST_TIMEOUT_MS = 15 * 60 * 1000\n\n// What the Chain Insights view draws from a graph_query answer, told by column\n// name. The same rules are in skills/chain-insights-cypher/SKILL.md.\nconst PICTURE_RULES = [\n 'Pictures: hosts that draw views, such as Claude Desktop and the Codex app, draw a graph_query answer from its column names. No other tool is needed. Choose the picture by the question: who sent to whom, flows and routes are a graph; how something changed over time is a time series; a list, a lookup or the details of one thing is a table.',\n '- Rows with from_address and to_address columns draw a graph of at most 5,000 addresses. Optional columns: amount_usd_sum, tx_count, first_seen_timestamp, last_seen_timestamp, link_kind, from_labels, to_labels. Example: USE topology MATCH (a:Address {address: $addr})-[f:FLOWS_TO]->(b:Address) WHERE NOT a:Pool RETURN a.address AS from_address, b.address AS to_address, f.amount_usd_sum AS amount_usd_sum, f.tx_count AS tx_count, f.last_seen_timestamp AS last_seen_timestamp ORDER BY f.last_seen_timestamp DESC LIMIT 25',\n '- Rows with a day, date, hour, week, month or *_timestamp column, number columns, no address in any value and at least 3 distinct times draw a time series. Within one day, the transfers of a known pair: USE facts MATCH (a:Address {address: $from})-[t:TRANSFER]->(b:Address {address: $to}) WHERE t.block_date = \"YYYY-MM-DD\" RETURN t.block_timestamp AS block_timestamp, t.amount_usd AS amount_usd LIMIT 200',\n '- Across days: facts reads one day at a time, so send graph_query_batch with one query per day (3 to 20 days), each query id the day as \"YYYY-MM-DD\", each returning one total row: USE facts MATCH (a:Address {address: $from})-[t:TRANSFER]->(b:Address {address: $to}) WHERE t.block_date = \"YYYY-MM-DD\" RETURN count(t) AS tx_count, sum(t.amount_usd) AS amount_usd_sum. The view joins the days into one time series. Do not group by day or name several days in one read: both are refused.',\n '- Any other rows draw a table.',\n '- The host draws the picture from the answer itself. Do not draw it again with another tool, such as a visualize or widget tool: the user would see the same answer drawn twice.',\n].join('\\n')\n\n// The same rule in one sentence, for meta_help.\nconst PICTURE_HELP_LINE =\n 'In hosts that draw views, a graph_query answer with from_address and to_address columns draws a graph, a day or time column with numbers draws a time series (across days: a graph_query_batch of one-day totals, each query id the day), and any other rows draw a table.'\n\n// Hosts that read skills from local files (Claude Code, Codex) load the\n// chain-insights-cypher skill that setup copies and the proxy keeps current.\n// Claude Desktop chat reads skills only from the user's claude.ai account, so\n// the proxy serves the same text through meta_rules: no upload, and always the\n// version of the installed package.\nconst RULES_FIRST =\n 'Rules first: if the chain-insights-cypher skill is not loaded in this chat, call meta_rules once (send {}) before the first graph_query or graph_query_batch, and follow what it returns. When the skill is loaded, do not call meta_rules.'\n\n// What every reply to the user follows. First in the instructions, so a host\n// that keeps only their start still has them; the skill says the same.\nconst ANSWER_RULES = [\n 'Answer rules, for every reply to the user:',\n '1. Write every address in full: 0x plus 40 hex characters, 42 in all, in sentences, lists, tables and summaries. Never shorten one with ... or …. The drawn picture may shorten its labels; your text never does.',\n '2. Do not print the query, its USE line, its LIMIT, the epoch numbers you computed, or a Ref: line, unless the user asks how a number was found.',\n '3. Tokens are not dollars: a transfer moves tokens worth an amount at the day price. Say \"tokens worth about 183 USD on 2026-10-06\", never \"sent 183 USD\".',\n '4. Never draw a graph or a chart in text, ASCII or Mermaid: the host draws the picture from the columns. Write a short answer beside it.',\n].join('\\n')\n\nconst CHAIN_INSIGHTS_WORKFLOW = [\n 'Workflow:',\n '1. Do not call investigation tools until required arguments are known. Network is required; use meta_network_capabilities to check supported networks and available tools, or ask the user if missing.',\n '2. Use graph_query(_batch) for every question about addresses, money flow and transactions.',\n '3. Preserve tool summaries and structured facts as returned. Keep full blockchain addresses intact.',\n].join('\\n')\n\nconst GRAPH_SCHEMA_HINTS = [\n 'Graph query hints:',\n '- Pass network=robinhood, the one public network, on every graph tool call. CIA does not pick a default network. meta_network_capabilities takes no arguments (send {}); call it only to check which tools and layers are live.',\n ...routingHintLines(),\n '- Coverage is per kind of data. coverage.complete_through_block is the floor of USE facts: below it every raw relationship is indexed. graph_progress.complete_through_block is the floor of USE topology, and graph_progress.layers gives each link kind its own height. An empty answer above the floor may mean not indexed yet, not absent on chain. An absent chain_tip_block means the indexers are catching up: read freshness.max_data_age_seconds for the lag.',\n \"- The graph is address-grain. The only topology money node label is Address, keyed by the raw chain-native H160 address on EVM networks, for example 0x1874a43d7c6d888f9eda3d22a3a49704e3cadb24. There is no separate identity key. network is the query's network on every node and relationship, never stored, except on :Chain, where it names the remote chain.\",\n '- An Address carries a kind from chain facts. topology carries :Account or :Contract as a second label on an Address (:Contract:SmartAccount for a contract wallet), with is_contract kept beside :Contract, and an address that only received has none. facts serves no kind, and a facts read that names :Account or :Contract is refused: ask USE topology or USE chain. chain Address returns is_contract, nonce and delegated_to at the newest block, or at the block named by at_block. block_timestamp is epoch milliseconds on every layer.',\n '- Address nodes carry address, network, labels, and the role flags is_exchange, is_scam, is_victim and is_sanctioned. (:Address)-[:LINKED]-(:Address) is an undirected ownership-overlay edge (basis derived/associated, plus confidence, source_event, declared_owner, owner_state) asserting the two addresses are controlled by the same actor. LINKED is served on the topology graph only. Enumerate LINKED neighbors with MATCH (a:Address {address: $addr})-[l:LINKED]-(b:Address) RETURN b.address, b.network, l.basis, l.confidence LIMIT 25.',\n '- Labels hold role words, never detector names: Exchange (plus the exchange name), Scam, Victim and Sanctioned (plus the entity name). Each role is also a node label and a flag on the Address: :Exchange and is_exchange follow the exchange label, :Scam and is_scam the risk label, :Victim and is_victim the protection label, :Sanctioned and is_sanctioned the sanctioned label. Each flag is present only when true and absent otherwise, never false: test IS NOT NULL or IS NULL, not = false. A withdrawn label removes its node label and its flag.',\n '- Address nodes carry base activity rollups: degree_in/degree_out/degree_total (distinct counterparty addresses), tx_in_count/tx_out_count/tx_total_count, total_in_usd/total_out_usd/total_volume_usd, net_flow_usd (in minus out; positive = net receiver) — all computed from external flows only — and first_activity_timestamp/last_activity_timestamp/activity_span_days, which include all flows (self-loops included). FLOWS_TO edges carry exactly tx_count, amount_usd_sum (total money flow, token and native value merged), first_seen_timestamp, last_seen_timestamp. Lifetime aggregates are the only serving window. Averages are computed inline (amount_usd_sum / toFloat(tx_count)). tx_count counts token and native transfers plus internal native transfers (a contract sending ETH during a call), and amount_usd_sum prices them all. USE facts TRANSFER lists all three, so the rows of a pair are the transfers its link counts, up to the height the link was built to. A link covers all time and a USE facts read covers one day: read the transfers of a pair one day at a time. A transaction anchor of a pair resolves through USE facts, on the UTC day of the first_seen_timestamp or last_seen_timestamp of the link: MATCH (a:Address {address: $from})-[t:TRANSFER]->(b:Address {address: $to}) WHERE t.block_date = \"YYYY-MM-DD\" RETURN t.tx_id, t.block_timestamp LIMIT 1.',\n '- For actor-level exposure (AC11), UNION FLOWS_TO and SWAPPED reachability over one visible LINKED hop instead of expanding through the LINKED edge itself: MATCH (a:Address {address: $addr})-[:LINKED]-(owned:Address)-[r:FLOWS_TO|SWAPPED]-(b:Address) WHERE NOT a:Pool AND NOT owned:Pool AND owned.address <> b.address AND a.address <> b.address RETURN owned.address, b.address, type(r), coalesce(r.amount_usd_sum, r.bought_usd) LIMIT 50.',\n '- Labels and per-label risk live on the address node: the labels array plus three parallel lists, label_risk_labels, label_risk_levels and label_risk_updated_timestamps, where entry i of each list is one label row. No overall risk score is served. USE facts serves bounded single-event rows (TRANSFER, SWAP, LIQUIDITY_ADD, LIQUIDITY_REMOVE and BRIDGE_CROSSING edges) only; lifetime address metrics (degrees, totals, activity window) are node properties on USE topology.',\n '- (from:Address)-[t:TRANSFER]->(to:Address) on USE facts returns individual transfer rows, not aggregates, with properties amount, amount_usd, asset_symbol, asset_contract, tx_id, block_height, block_timestamp, event_index, edge_index, kind, price_usd, and price_missing. Every TRANSFER row has a kind: token, native or internal. An internal row is ETH a contract sends while it runs a call, such as the ETH leg of a wrap. Add t.kind = \"internal\" to the pair and the day to read only the internal rows. A facts read names an address pair with one day, whether it is a TRANSFER row-select or a count()/sum() aggregate: both endpoint addresses as {address: \"...\"} on from and to, with WHERE t.block_date = \"YYYY-MM-DD\". A transaction is read on USE chain by its tx_id, not on facts. t.block_timestamp bounds in epoch milliseconds may narrow the day to a time window. One address, a day alone, a window of days, a block range or a bare LIMIT is not enough. A facts read holds at most 200 rows in a reply, has one relationship and no hop, and takes no ORDER BY: sort the page yourself.',\n '- Facts graph labels include Address; the TRANSFER, SWAP, LIQUIDITY_ADD, LIQUIDITY_REMOVE and BRIDGE_CROSSING relationships each connect two Address nodes. Facts address keys match topology address values exactly.',\n '- Topology relationships include FLOWS_TO, SWAPPED, OPERATED_BY, LINKED, and RISK_PROXIMITY between Address nodes, ADDED_LIQUIDITY and REMOVED_LIQUIDITY to and from Pool nodes, and BRIDGED to and from Chain nodes.',\n \"- (:Address)-[:OPERATED_BY]->(:Address) is the directed owner-to-operator edge: the transaction sender that moved the owner's tokens (ERC-20/721), or the event operator (ERC-1155); not the approved spender. Aggregate properties: tx_count, amount_usd_sum, first_seen_timestamp, last_seen_timestamp, and optional token_standard (ERC20/ERC721/ERC1155; absent when the pair is mixed-standard). Valuation fields: valuation_tracked_count, valued_count and the reason counters missing_valuation_price_count, unknown_quantity_count, unrepresentable_quantity_count and usd_range_count. valuation_complete is true when valued_count equals valuation_tracked_count, valuation_coverage_ratio is valued_count divided by valuation_tracked_count, and amount_usd_sum counts a transfer with no USD value as 0, so it is a floor unless valuation_complete is true. One edge per owner/operator pair; direct transfers with no operator create none; an owner never operates for itself. Topology-only, and a topology fact — not a risk label. Probe: MATCH (owner:Address)-[operation:OPERATED_BY]->(operator:Address {address: $addr}) RETURN owner.address AS owner, operation.tx_count AS tx_count ORDER BY operation.tx_count DESC LIMIT 10.\",\n '- FLOWS_TO properties are tx_count, amount_usd_sum, first_seen_timestamp, last_seen_timestamp. pair_key and synced_through_height are internal sync bookkeeping, not to be queried. synced_through_height is also on the other summed links (SWAPPED, OPERATED_BY, ADDED_LIQUIDITY, REMOVED_LIQUIDITY, BRIDGED, APPROVED, SPONSORED, BUNDLED, SIGNED_FOR) and pair_key on SWAPPED: bookkeeping there too. Tx ids of every transfer, internal ones included, come from USE facts TRANSFER. Averages come from inline arithmetic. FLOWS_TO into and out of pools is kept; the pool trace rule below governs a trace that reaches one.',\n '- Swaps, liquidity and bridges on USE topology are lifetime totals per pair; single events are USE facts rows. (:Address)-[:SWAPPED]->(:Address) joins a swap payer to a different recipient, one edge per payer, recipient, sold_asset and bought_asset, with swap_count, sold_amount_raw, bought_amount_raw, sold_usd, bought_usd, pools, families, strength, first_seen_height and last_seen_height. sold_usd and bought_usd sum the routes on the edge at the day price: a route side with no price adds 0, so 0 can mean no price. Do not read 0 as worth nothing. The USE facts SWAP row says which side had no price (sold_price_missing, bought_price_missing). strength is swap (the whole route is proven: payer, recipient, assets, exact raw amounts and conservation) or swap_like (the shape is a swap but the pool code is not proven). Today no route is swap: the swap reader reads transaction receipts only, with no execution trace, so every served route is swap_like, with reason unknown_pool_code and families unknown. A Uniswap V2 or V3 swap reads this way, and unknown does not mean the protocol is unsupported. A filter on strength = \"swap\" or on a known family matches nothing. A route that could not be paired (swap_unsplit) is never served: it has no payer, recipient or pool, makes no SWAPPED edge and no SWAP row, and exists in the warehouse only. Every Uniswap v4 swap is swap_unsplit today, by design, so v4 swaps are hidden. A self swap makes no edge, and a missing SWAPPED edge is not proof that no swap happened. Swap attribution is read from SWAPPED, the aggregate (strength, pools, families), or from the USE facts SWAP row, one route. FLOWS_TO carries value only.',\n \"- Pool is a second label on an Address: the pool of a swap route or liquidity event, with liquidity_added_usd, liquidity_removed_usd, liquidity_net_usd, provider_count and receiver_count. (:Address)-[:ADDED_LIQUIDITY]->(:Pool) and (:Pool)-[:REMOVED_LIQUIDITY]->(:Address) join named providers and receivers to the pool, with event_count, totals_raw, usd, unpriced_count, first_seen_height and last_seen_height; REMOVED_LIQUIDITY adds fees_raw, receiver_added_usd (USD the receiver itself added to the same pool) and receiver_provided. A receiver's profit from a pool is usd minus receiver_added_usd: receiver_provided false is a drain by a stranger, true with a large profit is the creator's rug pull.\",\n '- (:Address)-[:BRIDGED]->(:Chain) is outbound bridge use and (:Chain)-[:BRIDGED]->(:Address) inbound, with kinds, events, totals_raw, first_height, last_height and last_bridge_event_id. A Chain node (network, address) is the remote bridge endpoint, never an Address, so no FLOWS_TO walk passes through it.',\n '- Pool trace rule, for every trace: 1. Enter a `:Pool` on any edge. 2. Leave a `:Pool` only on `REMOVED_LIQUIDITY`, to the address the liquidity was paid to. 3. Never leave a `:Pool` on `FLOWS_TO`. 4. Across a swap, follow `SWAPPED` from payer to recipient, between two different addresses. Do not walk through the pool. Rug-pull probe: MATCH (victim:Address {address: $addr})-[paid:FLOWS_TO]->(pool:Pool)-[removal:REMOVED_LIQUIDITY]->(receiver:Address) WHERE NOT victim:Pool AND receiver.address <> victim.address RETURN pool.address AS pool_address, receiver.address AS receiver_address, removal.usd AS removed_usd, removal.receiver_added_usd AS receiver_added_usd, removal.receiver_provided AS receiver_provided LIMIT 25.',\n '- USE facts serves the single events. (payer:Address)-[s:SWAP]->(recipient:Address) is one row per swap route, self swaps included, with strength, reason, route_id and the sold_ and bought_ asset, amount and usd columns. (provider:Address)-[:LIQUIDITY_ADD]->(pool:Address) and (pool:Address)-[:LIQUIDITY_REMOVE]->(receiver:Address) are one row per liquidity event, with party_state and evidence_state. (sender:Address)-[c:BRIDGE_CROSSING]->(recipient:Address) is one row per bridge event. SWAP, LIQUIDITY_* and BRIDGE_CROSSING take the same rule as TRANSFER: an address pair with one day. USD comes from the daily price services, never from a swap: with no price, USD is empty and the matching price_missing column is true (price_missing on TRANSFER, sold_price_missing and bought_price_missing on SWAP, amount0_price_missing and amount1_price_missing on LIQUIDITY_*). block_timestamp on TRANSFER, SWAP and LIQUIDITY_* rows is epoch milliseconds, in filters and in results; BRIDGE_CROSSING has none.',\n '- A USE facts SWAP row carries no route: it has no pools and no families column. A read that names one of them, to return it, to filter on it or to order by it, is refused. The route of a swap stays a topology question. For the pools of the swaps of an address, read SWAPPED.pools and SWAPPED.families on USE topology, anchored on the payer or the recipient. SWAPPED has one link per payer, recipient, sold asset and bought asset, so its pools cover every route on the link, not one route.',\n '- Traversal rule: for BFS, fixed-hop fallback, shortest-path, or manual FLOWS_TO traversal, exchange hot wallets are terminal endpoints only. Do not expand from, through, or classify exchange nodes as deposit, suspect, or intermediate candidates; filter every non-terminal node with is_exchange IS NULL. is_exchange is absent unless true, so a labelled node with no is_exchange is walked through, and is_scam, is_victim and is_sanctioned do not end a walk. At a Pool, follow the pool trace rule above.',\n '- Pool guard: a trace walks FLOWS_TO and SWAPPED, so it crosses a swap from payer to recipient without passing through the pool. A walk may end at a Pool, but never starts at one or passes through one: its start and every address in its middle stay off a Pool. A fixed-hop walk adds WHERE NOT src:Pool AND NOT mid:Pool, each its own AND term, never inside an OR. A quantified or shortest-path walk puts the guards inside the path pattern, on the start and on up to 4 guarded hops before one last hop: MATCH p = SHORTEST 1 (a:Address {address: $from} WHERE NOT a:Pool) (()-[:FLOWS_TO|SWAPPED]-(via:Address) WHERE NOT via:Pool){0,4} ()-[:FLOWS_TO|SWAPPED]-(b:Address {address: $to}) RETURN [n IN nodes(p) | n.address] AS route LIMIT 5. ANY SHORTEST takes the same pattern. A route search asks for one path. An open target from one address: MATCH SHORTEST 1 (a:Address {address: $addr} WHERE NOT a:Pool) (()-[:FLOWS_TO|SWAPPED]-(via:Address) WHERE NOT via:Pool){0,4} ()-[:FLOWS_TO|SWAPPED]-(b:Address) RETURN b.address LIMIT 50. Use these shapes as written, changing only the addresses and the RETURN. A WHERE placed after a SHORTEST pattern runs after the shortest route is chosen, so it drops a route that crosses a pool instead of finding the route that avoids it.',\n '- Call meta_schema {network} first when you need field names; it is cached for 24 hours. The two discovery reads below are the fallback.',\n '- Start schema discovery with endpoint-safe property reads: MATCH (n:Address) RETURN n.address AS address, n.network AS network, n.labels AS labels, n.last_activity_timestamp AS last_activity_timestamp LIMIT 20',\n '- Relationship discovery: MATCH (:Address)-[r:FLOWS_TO]->(:Address) RETURN r.amount_usd_sum AS amount_usd_sum, r.tx_count AS tx_count LIMIT 20',\n \"- Anchor every topology read that filters on a link property: put an address in its pattern. Without one, a read starts from every link of the type it names, and you should not count on the filter to narrow that: WHERE x.strength = 'swap' on SWAPPED checks every SWAPPED link, LIMIT stops the read only after enough rows match, and a filter that matches few or none can run to the 60 s topology limit and fail with query_timeout. Example: MATCH (a:Address {address: $addr})-[x:SWAPPED]->(b:Address) WHERE x.swap_count >= 2 RETURN b.address, x.swap_count LIMIT 25. Pick an address with few links: degree_out and degree_in are a rough guide, because they count neighbours, not links, and an address with hundreds of thousands of neighbours can fail the same way. Discovery probes with LIMIT and no filter stay valid. The queries of one batch share a 100 s budget; USE facts queries stop at 30 s.\",\n '- graph_query uses the active Chain Insights graph endpoint. Select the graph with USE topology for topology (address/FLOWS_TO/OPERATED_BY/LINKED graph with SWAPPED, ADDED_LIQUIDITY, REMOVED_LIQUIDITY, BRIDGED and the Pool label, unified recent+historical) and USE facts for bounded TRANSFER, SWAP, LIQUIDITY_ADD, LIQUIDITY_REMOVE and BRIDGE_CROSSING rows and enrichment, and USE chain for one keyed lookup on the chain node: a transaction by tx_id, a block by block_height or block_hash, an address at one block, or the head. On topology, address is the node grain, not the topology name.',\n '- 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.',\n '- Use USE facts graph patterns for fact and enrichment reads. Do not query internal table namespaces directly.',\n].join('\\n')\n\n// The routing paragraph comes first: a host that keeps only the start of the\n// instructions must still show where each kind of question goes.\nconst ROUTING_HEAD = routingHead()\n\nconst SERVER_INSTRUCTIONS = [\n 'Chain Insights is an AML and graph-analysis MCP server for AI agents.',\n RULES_FIRST,\n ANSWER_RULES,\n CHAIN_INSIGHTS_WORKFLOW,\n ROUTING_HEAD,\n PICTURE_RULES,\n GRAPH_SCHEMA_HINTS,\n 'Presentation rules: preserve tool summaries as returned; never truncate blockchain addresses or identity_resolution audit mappings.',\n].join('\\n\\n')\n\nconst STATELESS_SERVER_INSTRUCTIONS = [\n 'Chain Insights is running as a stateless AML proxy for a host application.',\n RULES_FIRST,\n ANSWER_RULES,\n 'Call graph_query or graph_query_batch with network=robinhood. meta_network_capabilities takes no arguments (send {}); call it only to check which tools and layers are live.',\n ROUTING_HEAD,\n PICTURE_RULES,\n 'Use wallet_balance to inspect the local payment wallet when payment setup is needed.',\n GRAPH_SCHEMA_HINTS,\n 'Presentation rules: preserve tool summaries as returned; never truncate blockchain addresses or identity_resolution audit mappings.',\n].join('\\n\\n')\n\n// Exported so a test can prove, for EVERY public tool, that each declared\n// schema argument also appears in PUBLIC_MCP_TOOL_ALLOWED_ARGS. An argument\n// present here but missing there is silently stripped by\n// normalizeRemoteToolArguments and the caller never learns their override was\n// ignored — the failure mode that shipped with `time_scope`.\nexport function knownPublicToolInputSchema(toolName: string): ToolInputShape | null {\n switch (toolName) {\n case 'graph_query':\n return {\n query: z.string().min(1).describe(`Read-only GQL/Cypher query. ${GRAPH_LAYERS_TEXT}`),\n network: NETWORK_SCHEMA,\n }\n case 'graph_query_batch':\n return {\n network: NETWORK_SCHEMA,\n queries: z\n .array(\n z.object({\n id: z.string().optional(),\n query: z.string().min(1).describe('Read-only GQL/Cypher query'),\n })\n )\n .min(1)\n .max(20),\n per_query_timeout_seconds: z.number().int().min(1).max(600).optional(),\n }\n default:\n return null\n }\n}\n\nfunction fallbackGraphPrimitiveTools(): McpTool[] {\n return FALLBACK_GRAPH_PRIMITIVE_TOOL_NAMES.map((name) => ({\n name,\n description: KNOWN_PUBLIC_TOOL_DESCRIPTIONS[name],\n }))\n}\n\n/**\n * Local payment wallet address for tools that address the caller on the\n * server (subscription_status). Returns null — never throws — when no wallet\n * is configured; callers degrade to an unavailable-shape result.\n */\n/**\n * Local payment wallet address for the top-up view: null when no wallet file\n * exists, and an error when the file exists but cannot be read. Reads the\n * address only; nothing is signed or sent.\n */\nasync function localTopupWalletAddress(): Promise<string | null> {\n const { isWalletConfigured } = await import('../wallet/index.js')\n if (!(await isWalletConfigured())) return null\n const { getWalletAccount } = await import('../wallet/tools.js')\n return (await getWalletAccount()).address\n}\n\nasync function localSubscriptionWalletAddress(): Promise<string | null> {\n try {\n const { getWalletAccount } = await import('../wallet/tools.js')\n const account = await getWalletAccount()\n return account.address\n } catch {\n return null\n }\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return !!value && typeof value === 'object' && !Array.isArray(value)\n}\n\nfunction redactLogValue(value: unknown): unknown {\n if (Array.isArray(value)) return value.map(redactLogValue)\n if (!isRecord(value)) return value\n return Object.fromEntries(\n Object.entries(value).map(([key, entry]) => {\n if (/token|secret|password|private.?key|authorization/i.test(key)) return [key, '[redacted]']\n return [key, redactLogValue(entry)]\n })\n )\n}\n\nfunction errorForLog(err: unknown): Record<string, unknown> {\n const error = err as Error\n return {\n name: error.name ?? 'Error',\n message: error.message ?? String(err),\n }\n}\n\nfunction sanitizeCypher(query: string): string {\n return query.replace(/\\s+/g, ' ').trim()\n}\n\nfunction cypherLogPayload(tool: string, args: unknown): Record<string, unknown> | null {\n if (!isRecord(args)) return null\n if (tool === 'graph_query') {\n return {\n network: args.network,\n queries: [\n {\n id: tool,\n query: typeof args.query === 'string' ? sanitizeCypher(args.query) : args.query,\n },\n ],\n }\n }\n if (tool === 'graph_query_batch') {\n const queries = Array.isArray(args.queries) ? args.queries : []\n return {\n network: args.network,\n per_query_timeout_seconds: args.per_query_timeout_seconds,\n query_count: queries.length,\n queries: queries.map((entry, index) =>\n isRecord(entry)\n ? {\n id: typeof entry.id === 'string' ? entry.id : `q${index + 1}`,\n query: typeof entry.query === 'string' ? sanitizeCypher(entry.query) : entry.query,\n }\n : { id: `q${index + 1}`, query: entry }\n ),\n }\n }\n return null\n}\n\nfunction createMcpLogger(config: Pick<InvestigatorConfig, 'dataDir'>) {\n const disabled = process.env.CHAIN_INSIGHTS_MCP_LOG === '0'\n const filePath =\n process.env.CHAIN_INSIGHTS_MCP_LOG_PATH?.trim() ||\n path.join(config.dataDir, '.chain-insights', 'runtime', 'logs', 'mcp-proxy.jsonl')\n\n async function write(\n level: 'info' | 'error',\n event: string,\n fields: Record<string, unknown> = {}\n ): Promise<void> {\n if (disabled) return\n try {\n await mkdir(path.dirname(filePath), { recursive: true })\n await appendFile(\n filePath,\n JSON.stringify({\n ts: new Date().toISOString(),\n level,\n event,\n pid: process.pid,\n ...fields,\n }) + '\\n',\n { mode: 0o600 }\n )\n } catch {\n // Logging must never break the stdio MCP server.\n }\n }\n\n return {\n filePath,\n info: (event: string, fields?: Record<string, unknown>) => write('info', event, fields),\n error: (event: string, fields?: Record<string, unknown>) => write('error', event, fields),\n }\n}\n\nfunction installToolLogging(server: McpServer, logger: ReturnType<typeof createMcpLogger>): void {\n const existingRegisterTool = server.registerTool\n const originalRegisterTool = existingRegisterTool.bind(server)\n const wrappedRegisterTool = ((\n name: string,\n config: ToolRegistrationConfig,\n handler: ToolHandler\n ) => {\n const wrapped: ToolHandler = async (args, extra) => {\n const startedAt = Date.now()\n await logger.info('tool.start', {\n tool: name,\n args: redactLogValue(args),\n })\n try {\n const result = await handler(args, extra)\n const isError = isRecord(result) && result.isError === true\n await logger.info('tool.end', {\n tool: name,\n duration_ms: Date.now() - startedAt,\n is_error: isError,\n })\n return result\n } catch (err) {\n await logger.error('tool.throw', {\n tool: name,\n duration_ms: Date.now() - startedAt,\n error: errorForLog(err),\n })\n throw err\n }\n }\n return originalRegisterTool(name, config, wrapped as never)\n }) as typeof server.registerTool\n Object.assign(wrappedRegisterTool, existingRegisterTool)\n server.registerTool = wrappedRegisterTool\n}\n\nfunction installRemoteCypherLogging(\n remoteClient: RemoteToolCaller,\n logger: ReturnType<typeof createMcpLogger>\n): void {\n const existingCallTool = remoteClient.callTool\n const originalCallTool = existingCallTool.bind(remoteClient)\n const wrappedCallTool = (async (...args: Parameters<Client['callTool']>) => {\n const input = args[0] as ToolCallInput\n const queryPayload = cypherLogPayload(input.name, input.arguments)\n const toolArgs = input.arguments ?? {}\n const startedAt = Date.now()\n if (queryPayload) {\n await logger.info('topology.start', {\n tool: input.name,\n ...queryPayload,\n })\n }\n try {\n const result = await originalCallTool(...args)\n if (queryPayload) {\n await logger.info('topology.end', {\n tool: input.name,\n duration_ms: Date.now() - startedAt,\n is_error: isRecord(result) && result.isError === true,\n })\n }\n const { warnings, search_limits } = actionLogSignalsFromResult(result)\n await appendActionLog({\n timestamp: startedAt,\n tool: input.name,\n args: toolArgs,\n outcome: 'ok',\n duration_ms: Date.now() - startedAt,\n warnings,\n search_limits,\n })\n return result\n } catch (err) {\n if (queryPayload) {\n await logger.error('cypher.throw', {\n tool: input.name,\n duration_ms: Date.now() - startedAt,\n error: errorForLog(err),\n })\n }\n await appendActionLog({\n timestamp: startedAt,\n tool: input.name,\n args: toolArgs,\n outcome: 'error',\n duration_ms: Date.now() - startedAt,\n error: (err as Error).message,\n })\n throw err\n }\n }) as typeof remoteClient.callTool\n Object.assign(wrappedCallTool, existingCallTool)\n remoteClient.callTool = wrappedCallTool\n}\n\nfunction remoteToolRequestOptions(toolName: string): Parameters<Client['callTool']>[2] | undefined {\n if (toolName === 'graph_query' || toolName === 'graph_query_batch') {\n return {\n timeout: REMOTE_GRAPH_TOOL_REQUEST_TIMEOUT_MS,\n maxTotalTimeout: REMOTE_GRAPH_TOOL_REQUEST_TIMEOUT_MS,\n }\n }\n return undefined\n}\n\nfunction isBlankArgument(value: unknown): boolean {\n if (value === undefined || value === null) return true\n if (typeof value === 'string') return value.trim() === ''\n if (Array.isArray(value)) return value.length === 0 || value.every(isBlankArgument)\n return false\n}\n\nfunction normalizeRemoteToolArguments(toolName: string, args: unknown): Record<string, unknown> {\n const normalized = isRecord(args) ? { ...args } : {}\n if (!(toolName in PUBLIC_MCP_TOOL_REQUIRED_ARGS)) return normalized\n\n const allowedArgs = PUBLIC_MCP_TOOL_ALLOWED_ARGS[toolName]\n if (!allowedArgs) return normalized\n return Object.fromEntries(Object.entries(normalized).filter(([key]) => allowedArgs.includes(key)))\n}\n\nfunction validateKnownPublicToolArguments(\n toolName: string,\n args: Record<string, unknown>\n): string | null {\n const requiredArgs = PUBLIC_MCP_TOOL_REQUIRED_ARGS[toolName]\n if (!requiredArgs) return null\n\n for (const argName of requiredArgs) {\n if (isBlankArgument(args[argName])) {\n return `Missing required argument: ${argName}`\n }\n }\n\n return null\n}\n\nfunction claudeFacingToolDescription(tool: McpTool): string {\n const baseDescription = KNOWN_PUBLIC_TOOL_DESCRIPTIONS[tool.name] ?? tool.description ?? tool.name\n const requiredArgs = PUBLIC_MCP_TOOL_REQUIRED_ARGS[tool.name]\n if (!requiredArgs) return baseDescription\n return [\n baseDescription,\n '',\n `Required arguments: ${requiredArgs.join(', ')}.`,\n 'If the user did not provide the network, ask for it before calling this tool. Do not guess a default network.',\n ].join('\\n')\n}\n\nfunction knownPublicToolAnnotations(toolName: string): Record<string, boolean> | undefined {\n if (\n toolName === 'graph_query' ||\n toolName === 'graph_query_batch' ||\n toolName.startsWith('aml_')\n ) {\n return {\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: true,\n }\n }\n return undefined\n}\n\ntype RemoteToolResult = {\n content?: ContentBlock[]\n structuredContent?: Record<string, unknown>\n _meta?: Record<string, unknown>\n isError?: boolean\n}\n\n/**\n * The text a call to the graph endpoint answers with when it throws: payment\n * guidance for a payment failure, the error otherwise. Shared by every proxied\n * tool and by the money-flow reads.\n */\nfunction remoteCallFailureText(toolName: string, err: unknown): string {\n if (err instanceof PaymentRequiredError) return err.message\n const msg = (err as Error).message ?? String(err)\n if (/\\b402\\b/.test(msg) || msg.toLowerCase().includes('payment')) {\n return (\n `Payment required for ${toolName}. This tool costs USDC on Base via x402 micropayments. ` +\n 'Next steps: run `cia wallet ready` to check funding and finish one-time payment setup, ' +\n 'run `cia wallet topup` if it says the wallet needs USDC, ' +\n 'or `cia access-key set <key>` if you have been given test access.'\n )\n }\n return `MCP call failed: ${msg}`\n}\n\nfunction promptResult(text: string, description?: string): GetPromptResult {\n return {\n description,\n messages: [\n {\n role: 'user',\n content: {\n type: 'text',\n text,\n },\n },\n ],\n }\n}\n\nfunction registerLocalPrompts(server: McpServer): void {\n server.registerPrompt(\n 'meta-network-capabilities',\n {\n title: 'Network Capabilities',\n description: 'Inspect supported networks and available tools before selecting a network.',\n argsSchema: {},\n },\n async () =>\n promptResult(\n 'Use Chain Insights meta_network_capabilities. Report only the supported networks and available tools exactly as returned; do not infer unsupported networks.',\n 'Network capabilities'\n )\n )\n\n server.registerPrompt(\n 'meta-schema',\n {\n title: 'Graph Schema',\n description: 'Read the live graph schema of one network before writing a query.',\n argsSchema: { network: NETWORK_SCHEMA },\n },\n async ({ network }) =>\n promptResult(\n `Use Chain Insights meta_schema with network ${network}. Report the labels, link types and fields exactly as returned; do not invent fields.`,\n 'Graph schema'\n )\n )\n\n server.registerPrompt(\n 'meta-usage-status',\n {\n title: 'Usage Status',\n description: \"Check the caller's public free graph_query quota.\",\n argsSchema: {},\n },\n async () =>\n promptResult(\n 'Use Chain Insights meta_usage_status. Report the quota fields exactly as returned.',\n 'Usage status'\n )\n )\n\n server.registerPrompt(\n 'meta-subscription-status',\n {\n title: 'Subscription Status',\n description: \"Check the caller's CIA subscription window, daily allowance, and tier.\",\n argsSchema: {},\n },\n async () =>\n promptResult(\n 'Use Chain Insights meta_subscription_status. Report the subscription facts exactly as returned.',\n 'Subscription status'\n )\n )\n\n server.registerPrompt(\n 'graph-query',\n {\n title: 'Graph Query',\n description: 'Run a read-only GQL/Cypher query through the Chain Insights graph endpoint.',\n argsSchema: {\n network: NETWORK_SCHEMA,\n query: z.string().describe('Read-only GQL/Cypher query'),\n },\n },\n async ({ network, query }) =>\n promptResult(\n [\n `Use Chain Insights graph_query on ${network} with this read-only GQL/Cypher query:`,\n '',\n '```gql',\n query,\n '```',\n '',\n `${GRAPH_LAYERS_TEXT} If you need schema context, first run small discovery queries such as MATCH (a:Address) RETURN a.address AS address, keys(a) AS address_properties LIMIT 5 and MATCH (:Address)-[r:FLOWS_TO]->(:Address) RETURN keys(r) AS flow_properties LIMIT 5. Return the full address when available; never shorten addresses with ellipses.`,\n ].join('\\n'),\n 'Graph query'\n )\n )\n\n server.registerPrompt(\n 'graph-query-batch',\n {\n title: 'Graph Query Batch',\n description:\n 'Run related read-only GQL/Cypher queries through the Chain Insights graph endpoint in one paid batch.',\n argsSchema: {\n network: NETWORK_SCHEMA,\n queries: z\n .string()\n .describe('JSON array of query objects with optional id and required query fields'),\n per_query_timeout_seconds: z\n .string()\n .optional()\n .describe('Optional integer timeout per query, 1-600 seconds'),\n },\n },\n async ({ network, queries, per_query_timeout_seconds }) =>\n promptResult(\n [\n `Use Chain Insights graph_query_batch on ${network} with these read-only GQL/Cypher queries:`,\n '',\n '```json',\n queries,\n '```',\n per_query_timeout_seconds\n ? `per_query_timeout_seconds: ${per_query_timeout_seconds}`\n : '',\n '',\n `${GRAPH_LAYERS_TEXT} If you need schema context, first run small discovery queries such as MATCH (a:Address) RETURN a.address AS address, keys(a) AS address_properties LIMIT 5 and MATCH (:Address)-[r:FLOWS_TO]->(:Address) RETURN keys(r) AS flow_properties LIMIT 5. Return the full address when available; never shorten addresses with ellipses.`,\n ]\n .filter(Boolean)\n .join('\\n'),\n 'Graph query batch'\n )\n )\n\n server.registerPrompt(\n 'wallet-balance',\n {\n title: 'Wallet Balance',\n description:\n 'Show the local Chain Insights payment wallet address, payment network, token, and amount.',\n argsSchema: {},\n },\n async () =>\n promptResult(\n 'Use Chain Insights wallet_balance. Show the wallet address, payment network, token, and amount exactly as returned.',\n 'Wallet balance'\n )\n )\n\n server.registerPrompt(\n 'meta-help',\n {\n title: 'Chain Insights Help',\n description: 'Show available Chain Insights tools and workflow guidance.',\n argsSchema: {},\n },\n async () =>\n promptResult(\n 'Use Chain Insights meta_help. Summarize the available tools and workflow guidance without inventing capabilities.',\n 'Chain Insights help'\n )\n )\n}\n\nfunction sanitizeStructuredContentForGraphPayload(\n structuredContent: Record<string, unknown> | undefined\n): Record<string, unknown> | undefined {\n if (!structuredContent) return undefined\n return sanitizeStructuredValue(structuredContent) as Record<string, unknown>\n}\n\nfunction sanitizeStructuredValue(value: unknown): unknown {\n if (!value || typeof value !== 'object' || Array.isArray(value)) return value\n\n const sanitized: Record<string, unknown> = {}\n for (const [key, childValue] of Object.entries(value)) {\n if (key === 'app_data') continue\n if (\n GRAPH_ARRAY_KEYS.includes(key as (typeof GRAPH_ARRAY_KEYS)[number]) &&\n Array.isArray(childValue)\n ) {\n continue\n }\n sanitized[key] = sanitizeStructuredValue(childValue)\n }\n\n return sanitized\n}\n\n// The remote result _meta passes through, MCP Apps keys (ui, ui/resourceUri)\n// included. Only the old graph-report envelope (chainInsights.graph) is dropped.\nfunction sanitizeRemoteMeta(metaValue: RemoteToolResult['_meta']): RemoteToolResult['_meta'] {\n if (!metaValue || typeof metaValue !== 'object' || Array.isArray(metaValue)) return undefined\n\n const meta = { ...metaValue } as Record<string, unknown>\n\n const chainInsights = meta.chainInsights\n if (chainInsights && typeof chainInsights === 'object' && !Array.isArray(chainInsights)) {\n const { graph: _graph, ...withoutGraph } = chainInsights as Record<string, unknown>\n if (Object.keys(withoutGraph).length > 0) meta.chainInsights = withoutGraph\n else delete meta.chainInsights\n }\n\n return Object.keys(meta).length > 0 ? (meta as RemoteToolResult['_meta']) : undefined\n}\n\n// Raw graph arrays and app_data are stripped from the aml_* answers only, the\n// graph-report tools of the old viewer. Every other answer (graph_query,\n// graph_query_batch) keeps its structuredContent exactly as the hosted server\n// returned it: the views read it.\nfunction stripsGraphPayload(toolName: string): boolean {\n return toolName.startsWith('aml_')\n}\n\nfunction normalizeRemoteToolResult(toolName: string, result: RemoteToolResult) {\n return {\n content: result.content ?? [],\n structuredContent: stripsGraphPayload(toolName)\n ? sanitizeStructuredContentForGraphPayload(result.structuredContent)\n : result.structuredContent,\n _meta: sanitizeRemoteMeta(result._meta),\n isError: result.isError,\n }\n}\n\nfunction cleanNetworkCapabilities(value: unknown) {\n const structuredContent = isRecord(value) ? value.structuredContent : undefined\n const facts = isRecord(structuredContent) ? structuredContent.facts : undefined\n const capabilities = isRecord(facts) ? facts.capabilities : undefined\n const networks =\n isRecord(capabilities) && Array.isArray(capabilities.networks) ? capabilities.networks : []\n\n return {\n schema: 'chain-insights.result.v1' as const,\n tool: 'meta_network_capabilities',\n hint: null,\n facts: {\n capabilities: mirrorGraphNetworkCapabilities({ networks }),\n },\n }\n}\n\nfunction jsonTextResult(structuredContent: Record<string, unknown>) {\n return {\n content: [{ type: 'text' as const, text: JSON.stringify(structuredContent, null, 2) }],\n structuredContent,\n isError: false,\n }\n}\n\n/**\n * Core proxy logic — exported so tests can inject dependencies directly.\n * The IIFE at the bottom calls this with real dependencies.\n *\n * stdout purity: NEVER write to stdout in this file. Use console.error() or process.stderr.write() only.\n * All diagnostic output goes to console.error() or process.stderr.write().\n */\nexport async function createProxy(): Promise<void> {\n // Lazy imports to avoid module-load side effects (critical for stdio proxy)\n const { loadConfig } = await import('../config/index.js')\n const { activeDataDir, findActiveWorkspace } = await import('../workspace/active.js')\n const { createConfiguredGraphMcpFetch, resolveGraphMcpEndpoint } = await import('./client.js')\n const { loadSchema, saveSchema } = await import('./schema-cache.js')\n\n const proxyMode = resolveMcpProxyMode()\n const workspaceArtifactsEnabled = proxyMode === 'workspace'\n const loadedConfig = await loadConfig()\n const activeWorkspace = workspaceArtifactsEnabled ? findActiveWorkspace() : null\n const config = {\n ...loadedConfig,\n dataDir: workspaceArtifactsEnabled ? activeDataDir(loadedConfig.dataDir) : loadedConfig.dataDir,\n }\n const logger = createMcpLogger(config)\n await logger.info('proxy.start', {\n data_dir: config.dataDir,\n workspace_root: activeWorkspace?.root,\n proxy_mode: proxyMode,\n graph_mcp_mode: config.graphMcpMode,\n graph_mcp_endpoint: resolveGraphMcpEndpoint(config),\n log_path: logger.filePath,\n })\n const graphMcpEndpoint = resolveGraphMcpEndpoint(config)\n\n // Inside Claude Desktop the Claude Code copy cannot draw the views. When\n // Claude Desktop's own drawing copy is configured, this copy lists no tools\n // so that each tool appears once (src/mcp/claude-desktop.ts).\n const { defersToClaudeDesktop, DEFERRED_SERVER_INSTRUCTIONS } =\n await import('./claude-desktop.js')\n if (defersToClaudeDesktop()) {\n await logger.info('proxy.deferred_to_claude_desktop', { version: PACKAGE_VERSION })\n const deferred = new McpServer(\n { name: 'chain-insights', version: PACKAGE_VERSION },\n { instructions: DEFERRED_SERVER_INSTRUCTIONS }\n )\n await deferred.connect(new StdioServerTransport())\n return\n }\n\n // npm runs no setup step on upgrade: bring the skill copies setup installed\n // up to this release. A failure here never stops the proxy.\n // CHAIN_INSIGHTS_SKILL_REFRESH=0 turns it off for people who manage the skill themselves.\n if (process.env['CHAIN_INSIGHTS_SKILL_REFRESH'] !== '0') {\n try {\n const { refreshInstalledSkills } = await import('../skill-refresh.js')\n const refresh = refreshInstalledSkills(PACKAGE_VERSION)\n if (refresh.refreshed.length > 0 || refresh.failed.length > 0) {\n await logger.info('skills.refresh', { version: PACKAGE_VERSION, ...refresh })\n }\n } catch (error) {\n await logger.info('skills.refresh', {\n version: PACKAGE_VERSION,\n error: error instanceof Error ? error.message : String(error),\n })\n }\n }\n\n // Build remote MCP client. The local Chain Insights MCP surface must still\n // start when the graph endpoint is temporarily unavailable so agents can use\n // help and wallet tools.\n const remoteClient = new Client({ name: 'chain-insights-proxy-client', version: PACKAGE_VERSION })\n let remoteConnected = false\n let remoteUnavailableMessage: string | undefined\n let mcpFetch: typeof fetch | undefined\n\n try {\n mcpFetch = await createConfiguredGraphMcpFetch(config)\n } catch (err) {\n await logger.error('remote.fetch_setup_failed', {\n endpoint: graphMcpEndpoint,\n error: errorForLog(err),\n })\n remoteUnavailableMessage = `Chain Insights Graph setup unavailable at ${graphMcpEndpoint}: ${(err as Error).message}`\n process.stderr.write(\n `Chain Insights MCP graph tools unavailable: ${remoteUnavailableMessage}. Local Chain Insights tools are still available.\\n`\n )\n }\n\n if (mcpFetch) {\n try {\n await remoteClient.connect(\n new StreamableHTTPClientTransport(new URL(graphMcpEndpoint), { fetch: mcpFetch })\n )\n remoteConnected = true\n await logger.info('remote.connect', {\n transport: 'streamable_http',\n endpoint: graphMcpEndpoint,\n })\n } catch {\n await logger.error('remote.connect_failed', {\n transport: 'streamable_http',\n endpoint: graphMcpEndpoint,\n })\n // StreamableHTTP failed — try SSE fallback (assumption A1 from RESEARCH.md)\n try {\n const { SSEClientTransport } = await import('@modelcontextprotocol/sdk/client/sse.js')\n await remoteClient.connect(\n new SSEClientTransport(new URL(graphMcpEndpoint), { fetch: mcpFetch })\n )\n remoteConnected = true\n await logger.info('remote.connect', {\n transport: 'sse',\n endpoint: graphMcpEndpoint,\n })\n } catch (err2) {\n await logger.error('remote.connect_failed', {\n transport: 'sse',\n endpoint: graphMcpEndpoint,\n error: errorForLog(err2),\n })\n remoteUnavailableMessage = `Chain Insights Graph unreachable at ${graphMcpEndpoint}: ${(err2 as Error).message}`\n process.stderr.write(\n `Chain Insights MCP graph tools unavailable: ${remoteUnavailableMessage}. Local Chain Insights tools are still available.\\n`\n )\n }\n }\n }\n if (remoteConnected)\n installRemoteCypherLogging(remoteClient as unknown as RemoteToolCaller, logger)\n\n // Schema cache check — skip remote listTools call on cache hit\n let tools: McpTool[] | null = await loadSchema(graphMcpEndpoint)\n\n if (!tools && remoteConnected) {\n // Cache miss — fetch tools from remote (client is already connected above)\n const result = await remoteClient.listTools()\n tools = result.tools as McpTool[]\n await saveSchema(tools, graphMcpEndpoint)\n await logger.info('schema.tools_loaded', {\n source: 'remote',\n count: tools.length,\n })\n } else if (tools) {\n await logger.info('schema.tools_loaded', {\n source: 'cache',\n count: tools.length,\n })\n } else {\n tools = fallbackGraphPrimitiveTools()\n await logger.info('schema.tools_loaded', {\n source: 'unavailable',\n count: tools.length,\n })\n }\n const remoteToolNames = new Set((tools ?? []).map((tool) => tool.name))\n\n // Build local stdio proxy server\n const server = new McpServer(\n { name: 'chain-insights', version: PACKAGE_VERSION },\n {\n instructions: workspaceArtifactsEnabled ? SERVER_INSTRUCTIONS : STATELESS_SERVER_INSTRUCTIONS,\n }\n )\n installToolLogging(server, logger)\n\n if (remoteConnected) {\n try {\n await remoteClient.listPrompts()\n } catch (err) {\n await logger.error('remote.prompts_failed', {\n endpoint: graphMcpEndpoint,\n error: errorForLog(err),\n })\n process.stderr.write(\n `Chain Insights MCP remote prompt metadata unavailable at ${graphMcpEndpoint}: ${(err as Error).message}\\n`\n )\n }\n }\n\n registerLocalPrompts(server)\n\n server.registerTool(\n 'meta_network_capabilities',\n {\n title: 'Network Capabilities',\n description: KNOWN_PUBLIC_TOOL_DESCRIPTIONS.meta_network_capabilities,\n inputSchema: EMPTY_INPUT_SCHEMA,\n annotations: {\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false,\n },\n },\n async () => {\n if (remoteConnected && remoteToolNames.has('network_capabilities')) {\n try {\n const result = await remoteClient.callTool({\n name: 'network_capabilities',\n arguments: {},\n })\n return jsonTextResult(cleanNetworkCapabilities(result))\n } catch (err) {\n return {\n content: [\n {\n type: 'text' as const,\n text: `Network capabilities failed: ${(err as Error).message}`,\n },\n ],\n isError: true,\n }\n }\n }\n return jsonTextResult(cleanNetworkCapabilities(undefined))\n }\n )\n\n // meta_schema: the live graph schema of one network, built from the catalog\n // statements of the graph endpoint and kept on disk for 24 hours. A build reads\n // through the proxy's one remote session; a cache hit reads nothing.\n const schemaDependencies: GraphSchemaDependencies = {\n endpoint: graphMcpEndpoint,\n withClient: async (fn) => {\n if (!remoteConnected) {\n throw new GraphSchemaError(\n `${remoteUnavailableMessage ?? `Chain Insights Graph is not connected at ${graphMcpEndpoint}`}. Restart the Chain Insights MCP proxy after the endpoint is reachable.`\n )\n }\n return fn(remoteClient)\n },\n describeFailure: (err) => remoteCallFailureText('meta_schema', err),\n }\n server.registerTool(\n 'meta_schema',\n {\n title: META_SCHEMA_TITLE,\n description: KNOWN_PUBLIC_TOOL_DESCRIPTIONS.meta_schema,\n inputSchema: {\n network: NETWORK_SCHEMA,\n refresh: z\n .boolean()\n .optional()\n .describe('Rebuild the schema now instead of reading the 24 hour cache.'),\n },\n annotations: {\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: true,\n },\n },\n async (args) => handleMetaSchema(args, schemaDependencies)\n )\n\n server.registerTool(\n 'meta_usage_status',\n {\n title: 'Usage Status',\n description: KNOWN_PUBLIC_TOOL_DESCRIPTIONS.meta_usage_status,\n inputSchema: EMPTY_INPUT_SCHEMA,\n _meta: VIEW_TOOL_META,\n annotations: {\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: true,\n },\n },\n async () => {\n try {\n if (!remoteConnected) {\n return {\n content: [\n {\n type: 'text' as const,\n text: `${remoteUnavailableMessage ?? `Chain Insights Graph is not connected at ${graphMcpEndpoint}`}. Restart the Chain Insights MCP proxy after the endpoint is reachable.`,\n },\n ],\n isError: true,\n }\n }\n if (!remoteToolNames.has('usage_status')) {\n return jsonTextResult(primitiveBackendUsageStatus(graphMcpEndpoint))\n }\n const result = (await remoteClient.callTool({\n name: 'usage_status',\n arguments: {},\n })) as RemoteToolResult\n const structuredContent = isRecord(result.structuredContent)\n ? { ...result.structuredContent, tool: 'meta_usage_status' }\n : undefined\n return {\n content: structuredContent\n ? [{ type: 'text' as const, text: JSON.stringify(structuredContent, null, 2) }]\n : (result.content ?? []),\n structuredContent,\n _meta: result._meta,\n isError: result.isError,\n }\n } catch (err) {\n return {\n content: [\n { type: 'text' as const, text: `Usage status failed: ${(err as Error).message}` },\n ],\n isError: true,\n }\n }\n }\n )\n\n server.registerTool(\n 'meta_subscription_status',\n {\n title: 'Subscription Status',\n description: KNOWN_PUBLIC_TOOL_DESCRIPTIONS.meta_subscription_status,\n inputSchema: EMPTY_INPUT_SCHEMA,\n _meta: VIEW_TOOL_META,\n annotations: {\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: true,\n },\n },\n async () => {\n // Local proxy shape discipline: the tool is always present and never\n // throws. When the server tool is absent or errors, the facts explain\n // the unavailability.\n try {\n if (!remoteConnected) {\n return jsonTextResult(\n unavailableSubscriptionStatus(\n graphMcpEndpoint,\n remoteUnavailableMessage ??\n `Chain Insights Graph is not connected at ${graphMcpEndpoint}`\n )\n )\n }\n if (!remoteToolNames.has('subscription_status')) {\n return jsonTextResult(\n unavailableSubscriptionStatus(\n graphMcpEndpoint,\n 'The graph backend exposes primitive graph tools but no subscription_status tool.'\n )\n )\n }\n const walletAddress = await localSubscriptionWalletAddress()\n if (!walletAddress) {\n return jsonTextResult(\n unavailableSubscriptionStatus(\n graphMcpEndpoint,\n 'No local payment wallet is configured; run `cia wallet create` or `cia wallet import` first.'\n )\n )\n }\n const result = (await remoteClient.callTool({\n name: 'subscription_status',\n arguments: { wallet: walletAddress },\n })) as RemoteToolResult\n if (result.isError === true) {\n const firstText = Array.isArray(result.content)\n ? result.content.find(\n (block): block is Extract<ContentBlock, { type: 'text' }> =>\n block.type === 'text' && typeof block.text === 'string'\n )\n : undefined\n const reason = firstText?.text\n ? `subscription_status failed: ${firstText.text}`\n : 'subscription_status failed: the server returned an error'\n return jsonTextResult(unavailableSubscriptionStatus(graphMcpEndpoint, reason))\n }\n const structuredContent = isRecord(result.structuredContent)\n ? { ...result.structuredContent, tool: 'meta_subscription_status' }\n : undefined\n return {\n content: structuredContent\n ? [{ type: 'text' as const, text: JSON.stringify(structuredContent, null, 2) }]\n : (result.content ?? []),\n structuredContent,\n _meta: result._meta,\n isError: result.isError,\n }\n } catch (err) {\n return jsonTextResult(\n unavailableSubscriptionStatus(\n graphMcpEndpoint,\n `subscription_status failed: ${(err as Error).message}`\n )\n )\n }\n }\n )\n\n server.registerTool(\n 'wallet_balance',\n {\n title: 'Wallet Balance',\n description: KNOWN_PUBLIC_TOOL_DESCRIPTIONS.wallet_balance,\n inputSchema: EMPTY_INPUT_SCHEMA,\n annotations: {\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: true,\n },\n },\n async () => {\n try {\n const { formatWalletBalanceResult, getWalletAccount, getWalletBalanceResult } =\n await import('../wallet/tools.js')\n const account = await getWalletAccount()\n const structuredContent = await getWalletBalanceResult(account)\n return {\n content: [{ type: 'text' as const, text: formatWalletBalanceResult(structuredContent) }],\n structuredContent: structuredContent as unknown as Record<string, unknown>,\n isError: false,\n }\n } catch (err) {\n return {\n content: [{ type: 'text' as const, text: `Balance failed: ${(err as Error).message}` }],\n isError: true,\n }\n }\n }\n )\n // graph_expand, the one dedicated tool, for the view only (visibility\n // [\"app\"]): a node click is at most five anchored topology graph_query reads\n // (the node, then two chunks a side), a link click one facts graph_query\n // read, all sent through the remote client, the\n // path and payment wrapping a graph_query call takes, so each read is billed\n // as a graph query.\n const flowsDependencies: FlowsDependencies = {\n graphQuery: async (args) => {\n const options = remoteToolRequestOptions('graph_query')\n return (await withRateLimitRetry(() =>\n remoteClient.callTool({ name: 'graph_query', arguments: args }, undefined, options)\n )) as GraphQueryAnswer\n },\n describeFailure: (err) => remoteCallFailureText('graph_query', err),\n unavailable: () =>\n remoteConnected\n ? undefined\n : `${remoteUnavailableMessage ?? `Chain Insights Graph is not connected at ${graphMcpEndpoint}`}. Restart the Chain Insights MCP proxy after the endpoint is reachable.`,\n }\n server.registerTool(\n GRAPH_EXPAND_TOOL,\n {\n title: GRAPH_EXPAND_TITLE,\n description: GRAPH_EXPAND_DESCRIPTION,\n inputSchema: {\n network: z.string(),\n address: z.string().optional(),\n in_offset: z.number().int().min(0).max(FLOWS_MAX_OFFSET).optional(),\n out_offset: z.number().int().min(0).max(FLOWS_MAX_OFFSET).optional(),\n from: z.string().optional(),\n to: z.string().optional(),\n day: z.string().optional(),\n },\n annotations: VIEW_TOOL_ANNOTATIONS,\n _meta: APP_ONLY_TOOL_META,\n },\n async (args) => handleGraphExpand(args, flowsDependencies)\n )\n\n server.registerTool(\n 'meta_rules',\n {\n title: 'Chain Insights Rules',\n description: KNOWN_PUBLIC_TOOL_DESCRIPTIONS.meta_rules,\n inputSchema: EMPTY_INPUT_SCHEMA,\n annotations: {\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false,\n },\n },\n async () => {\n const { readPackagedSkillRules } = await import('../skill-refresh.js')\n return {\n content: [{ type: 'text' as const, text: readPackagedSkillRules() }],\n isError: false,\n }\n }\n )\n\n server.registerTool(\n 'meta_help',\n {\n title: 'Chain Insights Help',\n description: KNOWN_PUBLIC_TOOL_DESCRIPTIONS.meta_help,\n inputSchema: EMPTY_INPUT_SCHEMA,\n annotations: {\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false,\n },\n },\n async () => ({\n content: [\n {\n type: 'text' as const,\n text: workspaceArtifactsEnabled\n ? [\n 'Chain Insights helps AI agents run AML investigation workflows.',\n '',\n CHAIN_INSIGHTS_WORKFLOW,\n '',\n 'Investigation tools:',\n '- meta_network_capabilities: supported networks and available tools. Takes no arguments.',\n '- meta_schema: read the live graph schema of one network: labels, link types and field names. Cached for 24 hours.',\n '- meta_usage_status: check the caller public free graph_query quota.',\n '- meta_subscription_status: check the caller CIA subscription window end, daily allowance, consumption, and tier.',\n '- graph_query: run read-only GQL/Cypher through the universal graph endpoint. Use USE topology or USE facts.',\n '- graph_query_batch: run related read-only graph-language queries through one paid graph call.',\n '',\n PICTURE_HELP_LINE,\n '',\n 'Wallet tools:',\n '- wallet_balance: show the local payment wallet address, payment network, token, and amount.',\n '- meta_rules: the full query, picture and answer rules. Call once per chat when the chain-insights-cypher skill is not loaded.',\n '- meta_help: show this overview.',\n ].join('\\n')\n : [\n 'Chain Insights stateless AML proxy for host applications.',\n '',\n 'Available graph-backed tools:',\n '- meta_network_capabilities: supported networks and available tools. Takes no arguments.',\n '- meta_schema: read the live graph schema of one network: labels, link types and field names. Cached for 24 hours.',\n '- meta_usage_status: check the caller public free graph_query quota.',\n '- meta_subscription_status: check the caller CIA subscription window end, daily allowance, consumption, and tier.',\n '- graph_query: run read-only GQL/Cypher through the universal graph endpoint. Use USE topology or USE facts.',\n '- graph_query_batch: run related read-only graph-language queries through one paid graph call.',\n '- meta_rules: the full query, picture and answer rules. Call once per chat when the chain-insights-cypher skill is not loaded.',\n '',\n PICTURE_HELP_LINE,\n ].join('\\n'),\n },\n ],\n isError: false,\n })\n )\n\n // Register each remote tool locally — passthrough proxy pattern\n for (const tool of tools ?? []) {\n if (HIDDEN_REMOTE_TOOL_NAMES.has(tool.name)) continue\n if (LOCAL_TOOL_NAMES.has(tool.name)) continue\n // A tool only a view of the graph endpoint may call has no view here.\n if (isAppOnlyTool(tool)) continue\n const inputSchema = knownPublicToolInputSchema(tool.name) ?? z.object({}).passthrough()\n const handler = async (args: unknown) => {\n try {\n if (!remoteConnected) {\n return {\n content: [\n {\n type: 'text' as const,\n text: `${remoteUnavailableMessage ?? `Chain Insights Graph is not connected at ${graphMcpEndpoint}`}. Restart the Chain Insights MCP proxy after the endpoint is reachable.`,\n },\n ],\n isError: true,\n }\n }\n const normalizedArgs = normalizeRemoteToolArguments(tool.name, args)\n const validationError = validateKnownPublicToolArguments(tool.name, normalizedArgs)\n if (validationError) {\n return {\n content: [{ type: 'text' as const, text: validationError }],\n isError: true,\n }\n }\n const request = {\n name: tool.name,\n arguments: normalizedArgs,\n }\n const requestOptions = remoteToolRequestOptions(tool.name)\n const result = await withRateLimitRetry(() =>\n requestOptions\n ? remoteClient.callTool(request, undefined, requestOptions)\n : remoteClient.callTool(request)\n )\n return normalizeRemoteToolResult(tool.name, result as RemoteToolResult)\n } catch (err) {\n const limited = rateLimitedResult(tool.name, err)\n if (limited) return limited\n return {\n content: [{ type: 'text' as const, text: remoteCallFailureText(tool.name, err) }],\n isError: true,\n }\n }\n }\n // The graph endpoint's own MCP Apps metadata (_meta.ui, ui/resourceUri)\n // is never forwarded: the proxy is the only source of views. graph_query\n // gets the view the proxy serves.\n const annotations = knownPublicToolAnnotations(tool.name)\n const drawsView = tool.name === 'graph_query'\n const toolConfig = {\n title: tool.title ?? KNOWN_PUBLIC_TOOL_TITLES[tool.name],\n description: claudeFacingToolDescription(tool),\n inputSchema,\n ...(annotations ? { annotations } : {}),\n ...(drawsView ? { _meta: VIEW_TOOL_META } : {}),\n }\n\n server.registerTool(tool.name, toolConfig, handler)\n }\n\n // The local top-up view (wallet address and QR code). It stays in the local\n // proxy: the wallet it funds is a local file, and the hosted connector never\n // offers it (ruling 2026-10-05).\n const { registerTopupView, TOPUP_VIEW_URI } = await import('../wallet/mcp-proxy/topup-server.js')\n await registerTopupView(server, { walletAddress: localTopupWalletAddress })\n\n // ui://chain-insights/view, read from the package. Nothing is fetched.\n server.registerResource(\n 'Chain Insights view',\n CLAUDE_VIEW_URI,\n {\n title: 'Chain Insights view',\n description:\n 'The Chain Insights view for hosts that draw MCP apps: money flows, query results and balance, in light and dark.',\n mimeType: MCP_APP_MIME_TYPE,\n _meta: VIEW_RESOURCE_META,\n },\n async () => ({\n contents: [\n {\n uri: CLAUDE_VIEW_URI,\n mimeType: MCP_APP_MIME_TYPE,\n text: readClaudeViewHtml(),\n _meta: VIEW_RESOURCE_META,\n },\n ],\n })\n )\n\n // Connect to stdio transport — after this line, stdout belongs to MCP\n const transport = new StdioServerTransport()\n await server.connect(transport)\n await logger.info('proxy.ready', {\n tools: [\n ...[...LOCAL_TOOL_NAMES].filter((name) => !APP_ONLY_LOCAL_TOOL_NAMES.has(name)),\n ...(tools ?? [])\n .filter((tool) => !isAppOnlyTool(tool))\n .map((tool) => tool.name)\n .filter((name) => !HIDDEN_REMOTE_TOOL_NAMES.has(name) && !LOCAL_TOOL_NAMES.has(name)),\n ].length,\n ui_resources: [CLAUDE_VIEW_URI, TOPUP_VIEW_URI],\n })\n\n // Signal handling — clean shutdown\n const shutdown = async () => {\n await logger.info('proxy.shutdown')\n transport.close()\n process.exit(0)\n }\n process.on('SIGINT', () => {\n void shutdown()\n })\n process.on('SIGTERM', () => {\n void shutdown()\n })\n}\n\n// Entry point — only execute when run as the main module (not when imported by tests)\n// Using process.argv check to detect direct execution vs import\nif (process.argv[1] && import.meta.url.includes(process.argv[1].replace(/\\\\/g, '/'))) {\n createProxy().catch((err) => {\n process.stderr.write(`Chain Insights MCP proxy startup failed: ${(err as Error).message}\\n`)\n process.exit(1)\n })\n}\n"],"mappings":"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"}
|
|
@@ -34,6 +34,13 @@ function skillTargetDirs(home = (0, node_os.homedir)()) {
|
|
|
34
34
|
function packagedSkillsDir() {
|
|
35
35
|
return (0, node_url.fileURLToPath)(new URL("../skills", require("url").pathToFileURL(__filename).href));
|
|
36
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* The rules of the packaged skill, for hosts that cannot load a local skill:
|
|
39
|
+
* SKILL.md without its front matter, which only says when to load it.
|
|
40
|
+
*/
|
|
41
|
+
function readPackagedSkillRules(sourceDir = packagedSkillsDir()) {
|
|
42
|
+
return (0, node_fs.readFileSync)(node_path.default.join(sourceDir, PUBLIC_SKILL_NAMES[0], "SKILL.md"), "utf8").replace(/^---\n[\s\S]*?\n---\n+/, "").trimEnd() + "\n";
|
|
43
|
+
}
|
|
37
44
|
/** Compares two x.y.z versions; a pre-release suffix is ignored. */
|
|
38
45
|
function compareVersions(a, b) {
|
|
39
46
|
const parts = (v) => v.split("-")[0].split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
@@ -139,4 +146,5 @@ function refreshInstalledSkills(version, options = {}) {
|
|
|
139
146
|
return result;
|
|
140
147
|
}
|
|
141
148
|
//#endregion
|
|
149
|
+
exports.readPackagedSkillRules = readPackagedSkillRules;
|
|
142
150
|
exports.refreshInstalledSkills = refreshInstalledSkills;
|
|
@@ -32,6 +32,13 @@ function skillTargetDirs(home = homedir()) {
|
|
|
32
32
|
function packagedSkillsDir() {
|
|
33
33
|
return fileURLToPath(new URL("../skills", import.meta.url));
|
|
34
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* The rules of the packaged skill, for hosts that cannot load a local skill:
|
|
37
|
+
* SKILL.md without its front matter, which only says when to load it.
|
|
38
|
+
*/
|
|
39
|
+
function readPackagedSkillRules(sourceDir = packagedSkillsDir()) {
|
|
40
|
+
return readFileSync(path.join(sourceDir, PUBLIC_SKILL_NAMES[0], "SKILL.md"), "utf8").replace(/^---\n[\s\S]*?\n---\n+/, "").trimEnd() + "\n";
|
|
41
|
+
}
|
|
35
42
|
/** Compares two x.y.z versions; a pre-release suffix is ignored. */
|
|
36
43
|
function compareVersions(a, b) {
|
|
37
44
|
const parts = (v) => v.split("-")[0].split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
@@ -137,6 +144,6 @@ function refreshInstalledSkills(version, options = {}) {
|
|
|
137
144
|
return result;
|
|
138
145
|
}
|
|
139
146
|
//#endregion
|
|
140
|
-
export { refreshInstalledSkills };
|
|
147
|
+
export { readPackagedSkillRules, refreshInstalledSkills };
|
|
141
148
|
|
|
142
|
-
//# sourceMappingURL=skill-refresh-
|
|
149
|
+
//# sourceMappingURL=skill-refresh-Dya9LQKN.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skill-refresh-Dya9LQKN.mjs","names":[],"sources":["../src/skill-refresh.ts"],"sourcesContent":["import {\n cpSync,\n existsSync,\n lstatSync,\n readFileSync,\n renameSync,\n rmSync,\n writeFileSync,\n} from 'node:fs'\nimport { homedir } from 'node:os'\nimport path from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\n/**\n * Keeps the skill copies that `cia setup` installed at the version of the\n * package that runs. npm runs no setup step on `npm i -g chain-insights@X`, so\n * without this an upgrade brings new server rules while every host keeps\n * reading the skill of the release it was set up with.\n *\n * Rules, in order:\n * - Only a place setup already installed is refreshed. Nothing is installed.\n * - A symlinked skill belongs to its owner (a checkout) and is never touched.\n * - A copy stamped by a newer release is left alone: two installs of\n * different versions (a global install and a bundled extension) must not\n * take turns downgrading each other.\n */\n\n/** The reviewed skill set; the same list as PUBLIC_SKILL_NAMES in bin/install.cjs. */\nexport const PUBLIC_SKILL_NAMES = Object.freeze(['chain-insights-cypher'])\n\n/** Written inside each installed skill by setup and by the refresh. */\nexport const SKILL_VERSION_FILE = '.chain-insights-version'\n\nexport interface SkillRefreshResult {\n /** Skill directories replaced with the packaged copy. */\n refreshed: string[]\n /** Skill directories left alone, with the reason. */\n skipped: { dir: string; reason: 'symlink' | 'newer' | 'current' }[]\n /** Skill directories that could not be replaced, with the error text. */\n failed: { dir: string; error: string }[]\n}\n\n/** The places `cia setup` installs skills: Claude Code, Codex, Hermes. */\nexport function skillTargetDirs(home: string = homedir()): string[] {\n return [\n path.join(home, '.claude', 'skills'),\n path.join(home, '.codex', 'skills'),\n path.join(home, '.hermes', 'skills', 'chain-insights'),\n ]\n}\n\n/** The skills/ directory shipped in the package, next to dist/ and src/. */\nexport function packagedSkillsDir(): string {\n return fileURLToPath(new URL('../skills', import.meta.url))\n}\n\n/**\n * The rules of the packaged skill, for hosts that cannot load a local skill:\n * SKILL.md without its front matter, which only says when to load it.\n */\nexport function readPackagedSkillRules(sourceDir: string = packagedSkillsDir()): string {\n const text = readFileSync(path.join(sourceDir, PUBLIC_SKILL_NAMES[0]!, 'SKILL.md'), 'utf8')\n const frontMatter = /^---\\n[\\s\\S]*?\\n---\\n+/\n return text.replace(frontMatter, '').trimEnd() + '\\n'\n}\n\n/** Compares two x.y.z versions; a pre-release suffix is ignored. */\nexport function compareVersions(a: string, b: string): number {\n const parts = (v: string) =>\n v\n .split('-')[0]!\n .split('.')\n .map((n) => Number.parseInt(n, 10) || 0)\n const left = parts(a)\n const right = parts(b)\n for (let i = 0; i < Math.max(left.length, right.length); i++) {\n const diff = (left[i] ?? 0) - (right[i] ?? 0)\n if (diff !== 0) return diff\n }\n return 0\n}\n\nfunction readStamp(skillDir: string): string | undefined {\n try {\n return readFileSync(path.join(skillDir, SKILL_VERSION_FILE), 'utf8').trim() || undefined\n } catch {\n return undefined\n }\n}\n\n/** Swaps the packaged copy in through hidden siblings, so a reader never sees half a skill. */\nfunction replaceSkill(source: string, dest: string, version: string): void {\n const parent = path.dirname(dest)\n const name = path.basename(dest)\n const staged = path.join(parent, `.${name}.refresh-${process.pid}`)\n const retired = path.join(parent, `.${name}.old-${process.pid}`)\n rmSync(staged, { recursive: true, force: true })\n try {\n cpSync(source, staged, { recursive: true })\n writeFileSync(path.join(staged, SKILL_VERSION_FILE), `${version}\\n`, 'utf8')\n renameSync(dest, retired)\n try {\n renameSync(staged, dest)\n } catch (error) {\n renameSync(retired, dest)\n throw error\n }\n rmSync(retired, { recursive: true, force: true })\n } finally {\n rmSync(staged, { recursive: true, force: true })\n }\n}\n\nexport function refreshInstalledSkills(\n version: string,\n options: { home?: string; sourceDir?: string } = {}\n): SkillRefreshResult {\n const sourceDir = options.sourceDir ?? packagedSkillsDir()\n const result: SkillRefreshResult = { refreshed: [], skipped: [], failed: [] }\n for (const target of skillTargetDirs(options.home)) {\n for (const name of PUBLIC_SKILL_NAMES) {\n const source = path.join(sourceDir, name)\n const dest = path.join(target, name)\n if (!existsSync(source)) continue\n let stat\n try {\n stat = lstatSync(dest)\n } catch {\n continue // not installed here\n }\n if (stat.isSymbolicLink()) {\n result.skipped.push({ dir: dest, reason: 'symlink' })\n continue\n }\n if (!stat.isDirectory()) continue\n const stamp = readStamp(dest)\n if (stamp !== undefined) {\n const order = compareVersions(stamp, version)\n if (order > 0) {\n result.skipped.push({ dir: dest, reason: 'newer' })\n continue\n }\n if (order === 0) {\n result.skipped.push({ dir: dest, reason: 'current' })\n continue\n }\n }\n try {\n replaceSkill(source, dest, version)\n result.refreshed.push(dest)\n } catch (error) {\n result.failed.push({\n dir: dest,\n error: error instanceof Error ? error.message : String(error),\n })\n }\n }\n }\n return result\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"skill-refresh-jGwQWlOH.mjs","names":[],"sources":["../src/skill-refresh.ts"],"sourcesContent":["import {\n cpSync,\n existsSync,\n lstatSync,\n readFileSync,\n renameSync,\n rmSync,\n writeFileSync,\n} from 'node:fs'\nimport { homedir } from 'node:os'\nimport path from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\n/**\n * Keeps the skill copies that `cia setup` installed at the version of the\n * package that runs. npm runs no setup step on `npm i -g chain-insights@X`, so\n * without this an upgrade brings new server rules while every host keeps\n * reading the skill of the release it was set up with.\n *\n * Rules, in order:\n * - Only a place setup already installed is refreshed. Nothing is installed.\n * - A symlinked skill belongs to its owner (a checkout) and is never touched.\n * - A copy stamped by a newer release is left alone: two installs of\n * different versions (a global install and a bundled extension) must not\n * take turns downgrading each other.\n */\n\n/** The reviewed skill set; the same list as PUBLIC_SKILL_NAMES in bin/install.cjs. */\nexport const PUBLIC_SKILL_NAMES = Object.freeze(['chain-insights-cypher'])\n\n/** Written inside each installed skill by setup and by the refresh. */\nexport const SKILL_VERSION_FILE = '.chain-insights-version'\n\nexport interface SkillRefreshResult {\n /** Skill directories replaced with the packaged copy. */\n refreshed: string[]\n /** Skill directories left alone, with the reason. */\n skipped: { dir: string; reason: 'symlink' | 'newer' | 'current' }[]\n /** Skill directories that could not be replaced, with the error text. */\n failed: { dir: string; error: string }[]\n}\n\n/** The places `cia setup` installs skills: Claude Code, Codex, Hermes. */\nexport function skillTargetDirs(home: string = homedir()): string[] {\n return [\n path.join(home, '.claude', 'skills'),\n path.join(home, '.codex', 'skills'),\n path.join(home, '.hermes', 'skills', 'chain-insights'),\n ]\n}\n\n/** The skills/ directory shipped in the package, next to dist/ and src/. */\nexport function packagedSkillsDir(): string {\n return fileURLToPath(new URL('../skills', import.meta.url))\n}\n\n/** Compares two x.y.z versions; a pre-release suffix is ignored. */\nexport function compareVersions(a: string, b: string): number {\n const parts = (v: string) =>\n v\n .split('-')[0]!\n .split('.')\n .map((n) => Number.parseInt(n, 10) || 0)\n const left = parts(a)\n const right = parts(b)\n for (let i = 0; i < Math.max(left.length, right.length); i++) {\n const diff = (left[i] ?? 0) - (right[i] ?? 0)\n if (diff !== 0) return diff\n }\n return 0\n}\n\nfunction readStamp(skillDir: string): string | undefined {\n try {\n return readFileSync(path.join(skillDir, SKILL_VERSION_FILE), 'utf8').trim() || undefined\n } catch {\n return undefined\n }\n}\n\n/** Swaps the packaged copy in through hidden siblings, so a reader never sees half a skill. */\nfunction replaceSkill(source: string, dest: string, version: string): void {\n const parent = path.dirname(dest)\n const name = path.basename(dest)\n const staged = path.join(parent, `.${name}.refresh-${process.pid}`)\n const retired = path.join(parent, `.${name}.old-${process.pid}`)\n rmSync(staged, { recursive: true, force: true })\n try {\n cpSync(source, staged, { recursive: true })\n writeFileSync(path.join(staged, SKILL_VERSION_FILE), `${version}\\n`, 'utf8')\n renameSync(dest, retired)\n try {\n renameSync(staged, dest)\n } catch (error) {\n renameSync(retired, dest)\n throw error\n }\n rmSync(retired, { recursive: true, force: true })\n } finally {\n rmSync(staged, { recursive: true, force: true })\n }\n}\n\nexport function refreshInstalledSkills(\n version: string,\n options: { home?: string; sourceDir?: string } = {}\n): SkillRefreshResult {\n const sourceDir = options.sourceDir ?? packagedSkillsDir()\n const result: SkillRefreshResult = { refreshed: [], skipped: [], failed: [] }\n for (const target of skillTargetDirs(options.home)) {\n for (const name of PUBLIC_SKILL_NAMES) {\n const source = path.join(sourceDir, name)\n const dest = path.join(target, name)\n if (!existsSync(source)) continue\n let stat\n try {\n stat = lstatSync(dest)\n } catch {\n continue // not installed here\n }\n if (stat.isSymbolicLink()) {\n result.skipped.push({ dir: dest, reason: 'symlink' })\n continue\n }\n if (!stat.isDirectory()) continue\n const stamp = readStamp(dest)\n if (stamp !== undefined) {\n const order = compareVersions(stamp, version)\n if (order > 0) {\n result.skipped.push({ dir: dest, reason: 'newer' })\n continue\n }\n if (order === 0) {\n result.skipped.push({ dir: dest, reason: 'current' })\n continue\n }\n }\n try {\n replaceSkill(source, dest, version)\n result.refreshed.push(dest)\n } catch (error) {\n result.failed.push({\n dir: dest,\n error: error instanceof Error ? error.message : String(error),\n })\n }\n }\n }\n return result\n}\n"],"mappings":"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"}
|