chain-insights 0.46.4 → 0.46.6
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
CHANGED
|
@@ -19,6 +19,71 @@ let _modelcontextprotocol_sdk_server_mcp_js = require("@modelcontextprotocol/sdk
|
|
|
19
19
|
let _modelcontextprotocol_sdk_server_stdio_js = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
20
20
|
let _modelcontextprotocol_sdk_client_index_js = require("@modelcontextprotocol/sdk/client/index.js");
|
|
21
21
|
let _modelcontextprotocol_sdk_client_streamableHttp_js = require("@modelcontextprotocol/sdk/client/streamableHttp.js");
|
|
22
|
+
/** The wait when the endpoint names none. The skills say at least 5 seconds. */
|
|
23
|
+
const DEFAULT_RETRY_WAIT_MS = 5e3;
|
|
24
|
+
const MIN_RETRY_WAIT_MS = 1e3;
|
|
25
|
+
const RATE_LIMITED = /\b429\b|too many requests|rate.?limit/i;
|
|
26
|
+
const RETRY_AFTER = /retry after (\d+) seconds?/i;
|
|
27
|
+
function messageOf(err) {
|
|
28
|
+
return err instanceof Error ? err.message : String(err);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The wait the endpoint asks for, in milliseconds, when err is its rate
|
|
32
|
+
* limit, and null for any other failure.
|
|
33
|
+
*/
|
|
34
|
+
function rateLimitWaitMs(err) {
|
|
35
|
+
const code = err && typeof err === "object" ? err.code : void 0;
|
|
36
|
+
const message = messageOf(err);
|
|
37
|
+
if (code !== 429 && !RATE_LIMITED.test(message)) return null;
|
|
38
|
+
const seconds = RETRY_AFTER.exec(message)?.[1];
|
|
39
|
+
return seconds ? Math.max(Number(seconds) * 1e3, MIN_RETRY_WAIT_MS) : DEFAULT_RETRY_WAIT_MS;
|
|
40
|
+
}
|
|
41
|
+
const defaultSleep = (ms) => new Promise((done) => setTimeout(done, ms));
|
|
42
|
+
/**
|
|
43
|
+
* Runs call. When the endpoint refuses it for the rate limit and asks for a
|
|
44
|
+
* wait of at most MAX_RETRY_WAIT_MS, waits that long and runs it once more.
|
|
45
|
+
* Any other failure, and the second refusal, is thrown to the caller.
|
|
46
|
+
*/
|
|
47
|
+
async function withRateLimitRetry(call, sleep = defaultSleep) {
|
|
48
|
+
try {
|
|
49
|
+
return await call();
|
|
50
|
+
} catch (err) {
|
|
51
|
+
const wait = rateLimitWaitMs(err);
|
|
52
|
+
if (wait === null || wait > 1e4) throw err;
|
|
53
|
+
await sleep(wait);
|
|
54
|
+
return await call();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The reply for a call the endpoint refused for the rate limit: the text the
|
|
59
|
+
* agent reads and the `capacity` envelope the skills act on. Null when err is
|
|
60
|
+
* not the rate limit.
|
|
61
|
+
*/
|
|
62
|
+
function rateLimitedResult(toolName, err) {
|
|
63
|
+
const wait = rateLimitWaitMs(err);
|
|
64
|
+
if (wait === null) return null;
|
|
65
|
+
const fix = `Wait ${Math.ceil(wait / 1e3)} seconds, then send the same request once.`;
|
|
66
|
+
return {
|
|
67
|
+
content: [{
|
|
68
|
+
type: "text",
|
|
69
|
+
text: `${toolName} was not run: Chain Insights Graph is limiting requests from this address. ${fix}`
|
|
70
|
+
}],
|
|
71
|
+
structuredContent: {
|
|
72
|
+
schema: "chain-insights.result.v1",
|
|
73
|
+
tool: toolName,
|
|
74
|
+
hint: null,
|
|
75
|
+
error_detail: {
|
|
76
|
+
code: "rate_limited",
|
|
77
|
+
rule: "requests_per_address",
|
|
78
|
+
class: "capacity",
|
|
79
|
+
fix,
|
|
80
|
+
example: ""
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
isError: true
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
//#endregion
|
|
22
87
|
//#region src/mcp/layer-routing.ts
|
|
23
88
|
/** The routing rule, in one sentence. The server states it in the same words. */
|
|
24
89
|
const ROUTING_RULE = "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.";
|
|
@@ -901,10 +966,10 @@ async function createProxy() {
|
|
|
901
966
|
const flowsDependencies = {
|
|
902
967
|
graphQuery: async (args) => {
|
|
903
968
|
const options = remoteToolRequestOptions("graph_query");
|
|
904
|
-
return await remoteClient.callTool({
|
|
969
|
+
return await withRateLimitRetry(() => remoteClient.callTool({
|
|
905
970
|
name: "graph_query",
|
|
906
971
|
arguments: args
|
|
907
|
-
}, void 0, options);
|
|
972
|
+
}, void 0, options));
|
|
908
973
|
},
|
|
909
974
|
describeFailure: (err) => remoteCallFailureText("graph_query", err),
|
|
910
975
|
unavailable: () => remoteConnected ? void 0 : `${remoteUnavailableMessage ?? `Chain Insights Graph is not connected at ${graphMcpEndpoint}`}. Restart the Chain Insights MCP proxy after the endpoint is reachable.`
|
|
@@ -999,9 +1064,11 @@ async function createProxy() {
|
|
|
999
1064
|
arguments: normalizedArgs
|
|
1000
1065
|
};
|
|
1001
1066
|
const requestOptions = remoteToolRequestOptions(tool.name);
|
|
1002
|
-
const result = requestOptions ?
|
|
1067
|
+
const result = await withRateLimitRetry(() => requestOptions ? remoteClient.callTool(request, void 0, requestOptions) : remoteClient.callTool(request));
|
|
1003
1068
|
return normalizeRemoteToolResult(tool.name, result);
|
|
1004
1069
|
} catch (err) {
|
|
1070
|
+
const limited = rateLimitedResult(tool.name, err);
|
|
1071
|
+
if (limited) return limited;
|
|
1005
1072
|
return {
|
|
1006
1073
|
content: [{
|
|
1007
1074
|
type: "text",
|
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":";;qBAgEa;qBACA;;;;;wBAqBG;YAkBJ;wBAEI,oBAAoB,MAAK,OAAO,aAA2B;KA+BtE,iBAAiB,eAAe,EAAE;wBAgGvB,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":";;qBAgEa;qBACA;;;;;wBAqBG;YAkBJ;wBAEI,oBAAoB,MAAK,OAAO,aAA2B;KA+BtE,iBAAiB,eAAe,EAAE;wBAgGvB,2BAA2B,mBAAmB;;;;;;;;wBA+lBxC,eAAe"}
|
package/dist/mcp-proxy.mjs
CHANGED
|
@@ -15,6 +15,71 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
15
15
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
16
16
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
17
17
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
18
|
+
/** The wait when the endpoint names none. The skills say at least 5 seconds. */
|
|
19
|
+
const DEFAULT_RETRY_WAIT_MS = 5e3;
|
|
20
|
+
const MIN_RETRY_WAIT_MS = 1e3;
|
|
21
|
+
const RATE_LIMITED = /\b429\b|too many requests|rate.?limit/i;
|
|
22
|
+
const RETRY_AFTER = /retry after (\d+) seconds?/i;
|
|
23
|
+
function messageOf(err) {
|
|
24
|
+
return err instanceof Error ? err.message : String(err);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The wait the endpoint asks for, in milliseconds, when err is its rate
|
|
28
|
+
* limit, and null for any other failure.
|
|
29
|
+
*/
|
|
30
|
+
function rateLimitWaitMs(err) {
|
|
31
|
+
const code = err && typeof err === "object" ? err.code : void 0;
|
|
32
|
+
const message = messageOf(err);
|
|
33
|
+
if (code !== 429 && !RATE_LIMITED.test(message)) return null;
|
|
34
|
+
const seconds = RETRY_AFTER.exec(message)?.[1];
|
|
35
|
+
return seconds ? Math.max(Number(seconds) * 1e3, MIN_RETRY_WAIT_MS) : DEFAULT_RETRY_WAIT_MS;
|
|
36
|
+
}
|
|
37
|
+
const defaultSleep = (ms) => new Promise((done) => setTimeout(done, ms));
|
|
38
|
+
/**
|
|
39
|
+
* Runs call. When the endpoint refuses it for the rate limit and asks for a
|
|
40
|
+
* wait of at most MAX_RETRY_WAIT_MS, waits that long and runs it once more.
|
|
41
|
+
* Any other failure, and the second refusal, is thrown to the caller.
|
|
42
|
+
*/
|
|
43
|
+
async function withRateLimitRetry(call, sleep = defaultSleep) {
|
|
44
|
+
try {
|
|
45
|
+
return await call();
|
|
46
|
+
} catch (err) {
|
|
47
|
+
const wait = rateLimitWaitMs(err);
|
|
48
|
+
if (wait === null || wait > 1e4) throw err;
|
|
49
|
+
await sleep(wait);
|
|
50
|
+
return await call();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The reply for a call the endpoint refused for the rate limit: the text the
|
|
55
|
+
* agent reads and the `capacity` envelope the skills act on. Null when err is
|
|
56
|
+
* not the rate limit.
|
|
57
|
+
*/
|
|
58
|
+
function rateLimitedResult(toolName, err) {
|
|
59
|
+
const wait = rateLimitWaitMs(err);
|
|
60
|
+
if (wait === null) return null;
|
|
61
|
+
const fix = `Wait ${Math.ceil(wait / 1e3)} seconds, then send the same request once.`;
|
|
62
|
+
return {
|
|
63
|
+
content: [{
|
|
64
|
+
type: "text",
|
|
65
|
+
text: `${toolName} was not run: Chain Insights Graph is limiting requests from this address. ${fix}`
|
|
66
|
+
}],
|
|
67
|
+
structuredContent: {
|
|
68
|
+
schema: "chain-insights.result.v1",
|
|
69
|
+
tool: toolName,
|
|
70
|
+
hint: null,
|
|
71
|
+
error_detail: {
|
|
72
|
+
code: "rate_limited",
|
|
73
|
+
rule: "requests_per_address",
|
|
74
|
+
class: "capacity",
|
|
75
|
+
fix,
|
|
76
|
+
example: ""
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
isError: true
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
//#endregion
|
|
18
83
|
//#region src/mcp/layer-routing.ts
|
|
19
84
|
/** The routing rule, in one sentence. The server states it in the same words. */
|
|
20
85
|
const ROUTING_RULE = "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.";
|
|
@@ -897,10 +962,10 @@ async function createProxy() {
|
|
|
897
962
|
const flowsDependencies = {
|
|
898
963
|
graphQuery: async (args) => {
|
|
899
964
|
const options = remoteToolRequestOptions("graph_query");
|
|
900
|
-
return await remoteClient.callTool({
|
|
965
|
+
return await withRateLimitRetry(() => remoteClient.callTool({
|
|
901
966
|
name: "graph_query",
|
|
902
967
|
arguments: args
|
|
903
|
-
}, void 0, options);
|
|
968
|
+
}, void 0, options));
|
|
904
969
|
},
|
|
905
970
|
describeFailure: (err) => remoteCallFailureText("graph_query", err),
|
|
906
971
|
unavailable: () => remoteConnected ? void 0 : `${remoteUnavailableMessage ?? `Chain Insights Graph is not connected at ${graphMcpEndpoint}`}. Restart the Chain Insights MCP proxy after the endpoint is reachable.`
|
|
@@ -995,9 +1060,11 @@ async function createProxy() {
|
|
|
995
1060
|
arguments: normalizedArgs
|
|
996
1061
|
};
|
|
997
1062
|
const requestOptions = remoteToolRequestOptions(tool.name);
|
|
998
|
-
const result = requestOptions ?
|
|
1063
|
+
const result = await withRateLimitRetry(() => requestOptions ? remoteClient.callTool(request, void 0, requestOptions) : remoteClient.callTool(request));
|
|
999
1064
|
return normalizeRemoteToolResult(tool.name, result);
|
|
1000
1065
|
} catch (err) {
|
|
1066
|
+
const limited = rateLimitedResult(tool.name, err);
|
|
1067
|
+
if (limited) return limited;
|
|
1001
1068
|
return {
|
|
1002
1069
|
content: [{
|
|
1003
1070
|
type: "text",
|
package/dist/mcp-proxy.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-proxy.mjs","names":[],"sources":["../src/mcp/layer-routing.ts","../src/mcp/proxy.ts"],"sourcesContent":["// 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 {\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: 'Return the current Chain Insights network and tool support matrix.',\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.`,\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.',\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. Call meta_network_capabilities first and pass a name GraphRAG advertised. CIA does not pick a default network.'\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, draw a graph_query answer from its column names. No other tool is needed.',\n '- Rows with from_address and to_address columns draw a graph of at most 60 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 or *_timestamp column and number columns draw a chart. Example, the transfers of a known pair on one day: 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 '- Any other rows draw a table.',\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 chart, and any other rows draw a table.'\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 '- Call meta_network_capabilities first. Pass network= exactly as GraphRAG advertised it. CIA does not pick a default network.',\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 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 'Use meta_network_capabilities first when network support is unknown, then call graph_query or graph_query_batch as needed.',\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 // 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 three anchored topology graph_query reads, a link\n // click one facts graph_query 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 remoteClient.callTool(\n { name: 'graph_query', arguments: args },\n undefined,\n 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: inspect supported networks and available tools.',\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: inspect supported networks and available tools.',\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 = requestOptions\n ? await remoteClient.callTool(request, undefined, requestOptions)\n : await remoteClient.callTool(request)\n return normalizeRemoteToolResult(tool.name, result as RemoteToolResult)\n } catch (err) {\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 '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: 'Return the current Chain Insights network and tool support matrix.',\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.`,\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.',\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. Call meta_network_capabilities first and pass a name GraphRAG advertised. CIA does not pick a default network.'\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, draw a graph_query answer from its column names. No other tool is needed.',\n '- Rows with from_address and to_address columns draw a graph of at most 60 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 or *_timestamp column and number columns draw a chart. Example, the transfers of a known pair on one day: 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 '- Any other rows draw a table.',\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 chart, and any other rows draw a table.'\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 '- Call meta_network_capabilities first. Pass network= exactly as GraphRAG advertised it. CIA does not pick a default network.',\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 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 'Use meta_network_capabilities first when network support is unknown, then call graph_query or graph_query_batch as needed.',\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 // 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 three anchored topology graph_query reads, a link\n // click one facts graph_query 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: inspect supported networks and available tools.',\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: inspect supported networks and available tools.',\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":";;;;;;;;;;;;;;;;;;AAeA,MAAa,wBAAwB;AACrC,MAAM,oBAAoB;AAE1B,MAAM,eAAe;AACrB,MAAM,cAAc;AAEpB,SAAS,UAAU,KAAsB;CACvC,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AACxD;;;;;AAMA,SAAgB,gBAAgB,KAA6B;CAC3D,MAAM,OAAO,OAAO,OAAO,QAAQ,WAAY,IAA2B,OAAO,KAAA;CACjF,MAAM,UAAU,UAAU,GAAG;CAC7B,IAAI,SAAS,OAAO,CAAC,aAAa,KAAK,OAAO,GAAG,OAAO;CACxD,MAAM,UAAU,YAAY,KAAK,OAAO,CAAC,GAAG;CAC5C,OAAO,UAAU,KAAK,IAAI,OAAO,OAAO,IAAI,KAAM,iBAAiB,IAAI;AACzE;AAIA,MAAM,gBAAuB,OAAO,IAAI,SAAS,SAAS,WAAW,MAAM,EAAE,CAAC;;;;;;AAO9E,eAAsB,mBACpB,MACA,QAAe,cACH;CACZ,IAAI;EACF,OAAO,MAAM,KAAK;CACpB,SAAS,KAAK;EACZ,MAAM,OAAO,gBAAgB,GAAG;EAChC,IAAI,SAAS,QAAQ,OAAA,KAA0B,MAAM;EACrD,MAAM,MAAM,IAAI;EAChB,OAAO,MAAM,KAAK;CACpB;AACF;;;;;;AAOA,SAAgB,kBAAkB,UAAkB,KAAc;CAChE,MAAM,OAAO,gBAAgB,GAAG;CAChC,IAAI,SAAS,MAAM,OAAO;CAE1B,MAAM,MAAM,QADI,KAAK,KAAK,OAAO,GACP,EAAE;CAC5B,OAAO;EACL,SAAS,CACP;GACE,MAAM;GACN,MAAM,GAAG,SAAS,6EAA6E;EACjG,CACF;EACA,mBAAmB;GACjB,QAAQ;GACR,MAAM;GACN,MAAM;GACN,cAAc;IACZ,MAAM;IACN,MAAM;IACN,OAAO;IACP;IACA,SAAS;GACX;EACF;EACA,SAAS;CACX;AACF;;;;AC/DA,MAAa,eACX;;AAGF,MAAa,gBAAgB;CAC3B;CACA;CACA;AACF;;AAGA,MAAa,2BACX;;AAGF,MAAa,kBACX;;AAGF,MAAa,kBACX;;AAGF,MAAa,qBACX;;AAGF,MAAa,aACX;;AAGF,MAAa,gBAAgB;CAC3B,SAAS;CACT,QAAQ;CACR,UAAU;CACV,QAAQ;AACV;;AAGA,MAAa,mBAAmB;;AAGhC,MAAa,YACX;;AAGF,MAAa,kBAAkB;;AAG/B,MAAa,aAAa;;AAe1B,SAAgB,YAAsB;CACpC,OAAO;EACL;EACA,cAAc;EACd,cAAc;EACd,cAAc;EACd,cAAc;EACd;EACA;EACA;EACA;CACF;AACF;;;;;;;AAQA,SAAgB,cAAsB;CACpC,OAAO,yBAAyB,aAAa,MAAM,cAAc,GAAG,MAAM,cAAc,GAAG,MAAM,cAAc,GAAG,GAAG;AACvH;;;;;AAMA,SAAgB,mBAA6B;CAC3C,OAAO,CACL,2BAA2B,aAAa,MAAM,cAAc,GAAG,MAAM,cAAc,GAAG,MAAM,cAAc,GAAG,GAAG,mBAAmB,GAAG,yBAAyB,GAAG,gBAAgB,GAAG,gBAAgB,wSACrM,oCAAoC,UAAU,CAAC,CAAC,KAAK,GAAG,GAC1D;AACF;;;AClFA,MAAM,mCAAmB,IAAI,IAAI;CAC/B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;AAED,MAAM,4CAA4B,IAAI,IAAI,CAAC,iBAAiB,CAAC;AAC7D,MAAM,mBAAmB;CAAC;CAAS;CAAS;CAAS;AAAc;AASnE,MAAa,kBAAkB;AAC/B,MAAa,oBAAoB;AAGjC,MAAM,iBAAiB;CACrB,IAAI,EAAE,aAAa,gBAAgB;CACnC,kBAAkB;AACpB;AAEA,MAAM,qBAAqB,EAAE,IAAI;CAAE,aAAa;CAAiB,YAAY,CAAC,KAAK;AAAE,EAAE;AACvF,MAAM,qBAAqB,EACzB,IAAI;CAAE,eAAe;CAAO,KAAK,EAAE,iBAAiB,CAAC,0BAA0B,EAAE;AAAE,EACrF;AACA,MAAM,wBAAwB,EAAE,cAAc,KAAK;AACnD,MAAM,mBAAmB;AAEzB,IAAI;;;;;AAMJ,SAAgB,qBAA6B;CAC3C,IAAI,mBAAmB,KAAA,GAAW,OAAO;CACzC,MAAM,OAAO,KAAK,QAAQ,cAAc,YAAY,GAAG,CAAC;CACxD,MAAM,aAAa,CACjB,KAAK,KAAK,MAAM,QAAQ,gBAAgB,GACxC,KAAK,KAAK,MAAM,MAAM,OAAO,OAAO,QAAQ,gBAAgB,CAC9D;CACA,KAAK,MAAM,aAAa,YACtB,IAAI;EACF,iBAAiB,aAAa,WAAW,MAAM;EAC/C,OAAO;CACT,QAAQ,CAER;CAEF,MAAM,IAAI,MAAM,gCAAgC,iBAAiB,6BAA6B;AAChG;AAIA,SAAgB,oBAAoB,MAAyB,QAAQ,KAAmB;CACtF,MAAM,MAAM,IAAI,gCAAgC,EAAE,KAAK,CAAC,CAAC,YAAY;CACrE,IAAI,CAAC,OAAO,QAAQ,aAAa,OAAO;CACxC,IAAI,QAAQ,aAAa,OAAO;CAChC,IAAI,QAAQ,kBAAkB,QAAQ,kBAAkB,OAAO;CAC/D,MAAM,IAAI,MAAM,sEAAsE,IAAI,EAAE;AAC9F;AAEA,MAAM,oBACJ;AAEF,MAAM,iCAAyD;CAC7D,2BAA2B;CAC3B,aAAa;CACb,mBAAmB;CACnB,0BACE;CACF,WAAW;CACX,gBACE;CACF,aAAa,+EAA+E,kBAAkB,GAAG,mBAAmB;CACpI,mBACE;AACJ;AAEA,MAAM,2BAAmD;CACvD,aAAa;CACb,mBAAmB;AACrB;AACA,MAAM,sCAAsC,CAAC,eAAe,mBAAmB;AAW/E,MAAM,iBAAiB,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,kIAAmB;AAErE,MAAM,qBAAqB,EAAE,aAAa,CAAC,CAAC;AAC5C,MAAM,uCAAuC;AAI7C,MAAM,gBAAgB;CACpB;CACA;CACA;CACA;AACF,CAAC,CAAC,KAAK,IAAI;AAGX,MAAM,oBACJ;AAEF,MAAM,0BAA0B;CAC9B;CACA;CACA;CACA;AACF,CAAC,CAAC,KAAK,IAAI;AAEX,MAAM,qBAAqB;CACzB;CACA;CACA,GAAG,iBAAiB;CACpB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC,CAAC,KAAK,IAAI;AAIX,MAAM,eAAe,YAAY;AAEjC,MAAM,sBAAsB;CAC1B;CACA;CACA;CACA;CACA;CACA;AACF,CAAC,CAAC,KAAK,MAAM;AAEb,MAAM,gCAAgC;CACpC;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC,CAAC,KAAK,MAAM;AAOb,SAAgB,2BAA2B,UAAyC;CAClF,QAAQ,UAAR;EACE,KAAK,eACH,OAAO;GACL,OAAO,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,+BAA+B,mBAAmB;GACpF,SAAS;EACX;EACF,KAAK,qBACH,OAAO;GACL,SAAS;GACT,SAAS,EACN,MACC,EAAE,OAAO;IACP,IAAI,EAAE,OAAO,CAAC,CAAC,SAAS;IACxB,OAAO,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,4BAA4B;GAChE,CAAC,CACH,CAAC,CACA,IAAI,CAAC,CAAC,CACN,IAAI,EAAE;GACT,2BAA2B,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,SAAS;EACvE;EACF,SACE,OAAO;CACX;AACF;AAEA,SAAS,8BAAyC;CAChD,OAAO,oCAAoC,KAAK,UAAU;EACxD;EACA,aAAa,+BAA+B;CAC9C,EAAE;AACJ;;;;;;;;;;;AAYA,eAAe,0BAAkD;CAC/D,MAAM,EAAE,uBAAuB,MAAM,OAAO,wBAAqB,CAAA,MAAA,MAAA,EAAA,CAAA;CACjE,IAAI,CAAE,MAAM,mBAAmB,GAAI,OAAO;CAC1C,MAAM,EAAE,qBAAqB,MAAM,OAAO,uBAAqB,CAAA,MAAA,MAAA,EAAA,CAAA;CAC/D,QAAQ,MAAM,iBAAiB,EAAA,CAAG;AACpC;AAEA,eAAe,iCAAyD;CACtE,IAAI;EACF,MAAM,EAAE,qBAAqB,MAAM,OAAO,uBAAqB,CAAA,MAAA,MAAA,EAAA,CAAA;EAE/D,QAAO,MADe,iBAAiB,EAAA,CACxB;CACjB,QAAQ;EACN,OAAO;CACT;AACF;AAEA,SAAS,SAAS,OAAkD;CAClE,OAAO,CAAC,CAAC,SAAS,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK;AACrE;AAEA,SAAS,eAAe,OAAyB;CAC/C,IAAI,MAAM,QAAQ,KAAK,GAAG,OAAO,MAAM,IAAI,cAAc;CACzD,IAAI,CAAC,SAAS,KAAK,GAAG,OAAO;CAC7B,OAAO,OAAO,YACZ,OAAO,QAAQ,KAAK,CAAC,CAAC,KAAK,CAAC,KAAK,WAAW;EAC1C,IAAI,oDAAoD,KAAK,GAAG,GAAG,OAAO,CAAC,KAAK,YAAY;EAC5F,OAAO,CAAC,KAAK,eAAe,KAAK,CAAC;CACpC,CAAC,CACH;AACF;AAEA,SAAS,YAAY,KAAuC;CAC1D,MAAM,QAAQ;CACd,OAAO;EACL,MAAM,MAAM,QAAQ;EACpB,SAAS,MAAM,WAAW,OAAO,GAAG;CACtC;AACF;AAEA,SAAS,eAAe,OAAuB;CAC7C,OAAO,MAAM,QAAQ,QAAQ,GAAG,CAAC,CAAC,KAAK;AACzC;AAEA,SAAS,iBAAiB,MAAc,MAA+C;CACrF,IAAI,CAAC,SAAS,IAAI,GAAG,OAAO;CAC5B,IAAI,SAAS,eACX,OAAO;EACL,SAAS,KAAK;EACd,SAAS,CACP;GACE,IAAI;GACJ,OAAO,OAAO,KAAK,UAAU,WAAW,eAAe,KAAK,KAAK,IAAI,KAAK;EAC5E,CACF;CACF;CAEF,IAAI,SAAS,qBAAqB;EAChC,MAAM,UAAU,MAAM,QAAQ,KAAK,OAAO,IAAI,KAAK,UAAU,CAAC;EAC9D,OAAO;GACL,SAAS,KAAK;GACd,2BAA2B,KAAK;GAChC,aAAa,QAAQ;GACrB,SAAS,QAAQ,KAAK,OAAO,UAC3B,SAAS,KAAK,IACV;IACE,IAAI,OAAO,MAAM,OAAO,WAAW,MAAM,KAAK,IAAI,QAAQ;IAC1D,OAAO,OAAO,MAAM,UAAU,WAAW,eAAe,MAAM,KAAK,IAAI,MAAM;GAC/E,IACA;IAAE,IAAI,IAAI,QAAQ;IAAK,OAAO;GAAM,CAC1C;EACF;CACF;CACA,OAAO;AACT;AAEA,SAAS,gBAAgB,QAA6C;CACpE,MAAM,WAAW,QAAQ,IAAI,2BAA2B;CACxD,MAAM,WACJ,QAAQ,IAAI,6BAA6B,KAAK,KAC9C,KAAK,KAAK,OAAO,SAAS,mBAAmB,WAAW,QAAQ,iBAAiB;CAEnF,eAAe,MACb,OACA,OACA,SAAkC,CAAC,GACpB;EACf,IAAI,UAAU;EACd,IAAI;GACF,MAAM,MAAM,KAAK,QAAQ,QAAQ,GAAG,EAAE,WAAW,KAAK,CAAC;GACvD,MAAM,WACJ,UACA,KAAK,UAAU;IACb,qBAAI,IAAI,KAAK,EAAA,CAAE,YAAY;IAC3B;IACA;IACA,KAAK,QAAQ;IACb,GAAG;GACL,CAAC,IAAI,MACL,EAAE,MAAM,IAAM,CAChB;EACF,QAAQ,CAER;CACF;CAEA,OAAO;EACL;EACA,OAAO,OAAe,WAAqC,MAAM,QAAQ,OAAO,MAAM;EACtF,QAAQ,OAAe,WAAqC,MAAM,SAAS,OAAO,MAAM;CAC1F;AACF;AAEA,SAAS,mBAAmB,QAAmB,QAAkD;CAC/F,MAAM,uBAAuB,OAAO;CACpC,MAAM,uBAAuB,qBAAqB,KAAK,MAAM;CAC7D,MAAM,wBACJ,MACA,QACA,YACG;EACH,MAAM,UAAuB,OAAO,MAAM,UAAU;GAClD,MAAM,YAAY,KAAK,IAAI;GAC3B,MAAM,OAAO,KAAK,cAAc;IAC9B,MAAM;IACN,MAAM,eAAe,IAAI;GAC3B,CAAC;GACD,IAAI;IACF,MAAM,SAAS,MAAM,QAAQ,MAAM,KAAK;IACxC,MAAM,UAAU,SAAS,MAAM,KAAK,OAAO,YAAY;IACvD,MAAM,OAAO,KAAK,YAAY;KAC5B,MAAM;KACN,aAAa,KAAK,IAAI,IAAI;KAC1B,UAAU;IACZ,CAAC;IACD,OAAO;GACT,SAAS,KAAK;IACZ,MAAM,OAAO,MAAM,cAAc;KAC/B,MAAM;KACN,aAAa,KAAK,IAAI,IAAI;KAC1B,OAAO,YAAY,GAAG;IACxB,CAAC;IACD,MAAM;GACR;EACF;EACA,OAAO,qBAAqB,MAAM,QAAQ,OAAgB;CAC5D;CACA,OAAO,OAAO,qBAAqB,oBAAoB;CACvD,OAAO,eAAe;AACxB;AAEA,SAAS,2BACP,cACA,QACM;CACN,MAAM,mBAAmB,aAAa;CACtC,MAAM,mBAAmB,iBAAiB,KAAK,YAAY;CAC3D,MAAM,mBAAmB,OAAO,GAAG,SAAyC;EAC1E,MAAM,QAAQ,KAAK;EACnB,MAAM,eAAe,iBAAiB,MAAM,MAAM,MAAM,SAAS;EACjE,MAAM,WAAW,MAAM,aAAa,CAAC;EACrC,MAAM,YAAY,KAAK,IAAI;EAC3B,IAAI,cACF,MAAM,OAAO,KAAK,kBAAkB;GAClC,MAAM,MAAM;GACZ,GAAG;EACL,CAAC;EAEH,IAAI;GACF,MAAM,SAAS,MAAM,iBAAiB,GAAG,IAAI;GAC7C,IAAI,cACF,MAAM,OAAO,KAAK,gBAAgB;IAChC,MAAM,MAAM;IACZ,aAAa,KAAK,IAAI,IAAI;IAC1B,UAAU,SAAS,MAAM,KAAK,OAAO,YAAY;GACnD,CAAC;GAEH,MAAM,EAAE,UAAU,kBAAkB,2BAA2B,MAAM;GACrE,MAAM,gBAAgB;IACpB,WAAW;IACX,MAAM,MAAM;IACZ,MAAM;IACN,SAAS;IACT,aAAa,KAAK,IAAI,IAAI;IAC1B;IACA;GACF,CAAC;GACD,OAAO;EACT,SAAS,KAAK;GACZ,IAAI,cACF,MAAM,OAAO,MAAM,gBAAgB;IACjC,MAAM,MAAM;IACZ,aAAa,KAAK,IAAI,IAAI;IAC1B,OAAO,YAAY,GAAG;GACxB,CAAC;GAEH,MAAM,gBAAgB;IACpB,WAAW;IACX,MAAM,MAAM;IACZ,MAAM;IACN,SAAS;IACT,aAAa,KAAK,IAAI,IAAI;IAC1B,OAAQ,IAAc;GACxB,CAAC;GACD,MAAM;EACR;CACF;CACA,OAAO,OAAO,iBAAiB,gBAAgB;CAC/C,aAAa,WAAW;AAC1B;AAEA,SAAS,yBAAyB,UAAiE;CACjG,IAAI,aAAa,iBAAiB,aAAa,qBAC7C,OAAO;EACL,SAAS;EACT,iBAAiB;CACnB;AAGJ;AAEA,SAAS,gBAAgB,OAAyB;CAChD,IAAI,UAAU,KAAA,KAAa,UAAU,MAAM,OAAO;CAClD,IAAI,OAAO,UAAU,UAAU,OAAO,MAAM,KAAK,MAAM;CACvD,IAAI,MAAM,QAAQ,KAAK,GAAG,OAAO,MAAM,WAAW,KAAK,MAAM,MAAM,eAAe;CAClF,OAAO;AACT;AAEA,SAAS,6BAA6B,UAAkB,MAAwC;CAC9F,MAAM,aAAa,SAAS,IAAI,IAAI,EAAE,GAAG,KAAK,IAAI,CAAC;CACnD,IAAI,EAAE,YAAY,gCAAgC,OAAO;CAEzD,MAAM,cAAc,6BAA6B;CACjD,IAAI,CAAC,aAAa,OAAO;CACzB,OAAO,OAAO,YAAY,OAAO,QAAQ,UAAU,CAAC,CAAC,QAAQ,CAAC,SAAS,YAAY,SAAS,GAAG,CAAC,CAAC;AACnG;AAEA,SAAS,iCACP,UACA,MACe;CACf,MAAM,eAAe,8BAA8B;CACnD,IAAI,CAAC,cAAc,OAAO;CAE1B,KAAK,MAAM,WAAW,cACpB,IAAI,gBAAgB,KAAK,QAAQ,GAC/B,OAAO,8BAA8B;CAIzC,OAAO;AACT;AAEA,SAAS,4BAA4B,MAAuB;CAC1D,MAAM,kBAAkB,+BAA+B,KAAK,SAAS,KAAK,eAAe,KAAK;CAC9F,MAAM,eAAe,8BAA8B,KAAK;CACxD,IAAI,CAAC,cAAc,OAAO;CAC1B,OAAO;EACL;EACA;EACA,uBAAuB,aAAa,KAAK,IAAI,EAAE;EAC/C;CACF,CAAC,CAAC,KAAK,IAAI;AACb;AAEA,SAAS,2BAA2B,UAAuD;CACzF,IACE,aAAa,iBACb,aAAa,uBACb,SAAS,WAAW,MAAM,GAE1B,OAAO;EACL,cAAc;EACd,iBAAiB;EACjB,gBAAgB;EAChB,eAAe;CACjB;AAGJ;;;;;;AAcA,SAAS,sBAAsB,UAAkB,KAAsB;CACrE,IAAI,eAAe,sBAAsB,OAAO,IAAI;CACpD,MAAM,MAAO,IAAc,WAAW,OAAO,GAAG;CAChD,IAAI,UAAU,KAAK,GAAG,KAAK,IAAI,YAAY,CAAC,CAAC,SAAS,SAAS,GAC7D,OACE,wBAAwB,SAAS;CAMrC,OAAO,oBAAoB;AAC7B;AAEA,SAAS,aAAa,MAAc,aAAuC;CACzE,OAAO;EACL;EACA,UAAU,CACR;GACE,MAAM;GACN,SAAS;IACP,MAAM;IACN;GACF;EACF,CACF;CACF;AACF;AAEA,SAAS,qBAAqB,QAAyB;CACrD,OAAO,eACL,6BACA;EACE,OAAO;EACP,aAAa;EACb,YAAY,CAAC;CACf,GACA,YACE,aACE,gKACA,sBACF,CACJ;CAEA,OAAO,eACL,eACA;EACE,OAAO;EACP,aAAa;EACb,YAAY,EAAE,SAAS,eAAe;CACxC,GACA,OAAO,EAAE,cACP,aACE,+CAA+C,QAAQ,wFACvD,cACF,CACJ;CAEA,OAAO,eACL,qBACA;EACE,OAAO;EACP,aAAa;EACb,YAAY,CAAC;CACf,GACA,YACE,aACE,sFACA,cACF,CACJ;CAEA,OAAO,eACL,4BACA;EACE,OAAO;EACP,aAAa;EACb,YAAY,CAAC;CACf,GACA,YACE,aACE,mGACA,qBACF,CACJ;CAEA,OAAO,eACL,eACA;EACE,OAAO;EACP,aAAa;EACb,YAAY;GACV,SAAS;GACT,OAAO,EAAE,OAAO,CAAC,CAAC,SAAS,4BAA4B;EACzD;CACF,GACA,OAAO,EAAE,SAAS,YAChB,aACE;EACE,qCAAqC,QAAQ;EAC7C;EACA;EACA;EACA;EACA;EACA,GAAG,kBAAkB;CACvB,CAAC,CAAC,KAAK,IAAI,GACX,aACF,CACJ;CAEA,OAAO,eACL,qBACA;EACE,OAAO;EACP,aACE;EACF,YAAY;GACV,SAAS;GACT,SAAS,EACN,OAAO,CAAC,CACR,SAAS,wEAAwE;GACpF,2BAA2B,EACxB,OAAO,CAAC,CACR,SAAS,CAAC,CACV,SAAS,mDAAmD;EACjE;CACF,GACA,OAAO,EAAE,SAAS,SAAS,gCACzB,aACE;EACE,2CAA2C,QAAQ;EACnD;EACA;EACA;EACA;EACA,4BACI,8BAA8B,8BAC9B;EACJ;EACA,GAAG,kBAAkB;CACvB,CAAC,CACE,OAAO,OAAO,CAAC,CACf,KAAK,IAAI,GACZ,mBACF,CACJ;CAEA,OAAO,eACL,kBACA;EACE,OAAO;EACP,aACE;EACF,YAAY,CAAC;CACf,GACA,YACE,aACE,uHACA,gBACF,CACJ;CAEA,OAAO,eACL,aACA;EACE,OAAO;EACP,aAAa;EACb,YAAY,CAAC;CACf,GACA,YACE,aACE,qHACA,qBACF,CACJ;AACF;AAEA,SAAS,yCACP,mBACqC;CACrC,IAAI,CAAC,mBAAmB,OAAO,KAAA;CAC/B,OAAO,wBAAwB,iBAAiB;AAClD;AAEA,SAAS,wBAAwB,OAAyB;CACxD,IAAI,CAAC,SAAS,OAAO,UAAU,YAAY,MAAM,QAAQ,KAAK,GAAG,OAAO;CAExE,MAAM,YAAqC,CAAC;CAC5C,KAAK,MAAM,CAAC,KAAK,eAAe,OAAO,QAAQ,KAAK,GAAG;EACrD,IAAI,QAAQ,YAAY;EACxB,IACE,iBAAiB,SAAS,GAAwC,KAClE,MAAM,QAAQ,UAAU,GAExB;EAEF,UAAU,OAAO,wBAAwB,UAAU;CACrD;CAEA,OAAO;AACT;AAIA,SAAS,mBAAmB,WAAiE;CAC3F,IAAI,CAAC,aAAa,OAAO,cAAc,YAAY,MAAM,QAAQ,SAAS,GAAG,OAAO,KAAA;CAEpF,MAAM,OAAO,EAAE,GAAG,UAAU;CAE5B,MAAM,gBAAgB,KAAK;CAC3B,IAAI,iBAAiB,OAAO,kBAAkB,YAAY,CAAC,MAAM,QAAQ,aAAa,GAAG;EACvF,MAAM,EAAE,OAAO,QAAQ,GAAG,iBAAiB;EAC3C,IAAI,OAAO,KAAK,YAAY,CAAC,CAAC,SAAS,GAAG,KAAK,gBAAgB;OAC1D,OAAO,KAAK;CACnB;CAEA,OAAO,OAAO,KAAK,IAAI,CAAC,CAAC,SAAS,IAAK,OAAqC,KAAA;AAC9E;AAMA,SAAS,mBAAmB,UAA2B;CACrD,OAAO,SAAS,WAAW,MAAM;AACnC;AAEA,SAAS,0BAA0B,UAAkB,QAA0B;CAC7E,OAAO;EACL,SAAS,OAAO,WAAW,CAAC;EAC5B,mBAAmB,mBAAmB,QAAQ,IAC1C,yCAAyC,OAAO,iBAAiB,IACjE,OAAO;EACX,OAAO,mBAAmB,OAAO,KAAK;EACtC,SAAS,OAAO;CAClB;AACF;AAEA,SAAS,yBAAyB,OAAgB;CAChD,MAAM,oBAAoB,SAAS,KAAK,IAAI,MAAM,oBAAoB,KAAA;CACtE,MAAM,QAAQ,SAAS,iBAAiB,IAAI,kBAAkB,QAAQ,KAAA;CACtE,MAAM,eAAe,SAAS,KAAK,IAAI,MAAM,eAAe,KAAA;CAC5D,MAAM,WACJ,SAAS,YAAY,KAAK,MAAM,QAAQ,aAAa,QAAQ,IAAI,aAAa,WAAW,CAAC;CAE5F,OAAO;EACL,QAAQ;EACR,MAAM;EACN,MAAM;EACN,OAAO,EACL,cAAc,+BAA+B,EAAE,SAAS,CAAC,EAC3D;CACF;AACF;AAEA,SAAS,eAAe,mBAA4C;CAClE,OAAO;EACL,SAAS,CAAC;GAAE,MAAM;GAAiB,MAAM,KAAK,UAAU,mBAAmB,MAAM,CAAC;EAAE,CAAC;EACrF;EACA,SAAS;CACX;AACF;;;;;;;;AASA,eAAsB,cAA6B;CAEjD,MAAM,EAAE,eAAe,MAAM,OAAO,wBAAqB,CAAA,MAAA,MAAA,EAAA,CAAA;CACzD,MAAM,EAAE,eAAe,wBAAwB,MAAM,OAAO,wBAAyB,CAAA,MAAA,MAAA,EAAA,CAAA;CACrF,MAAM,EAAE,+BAA+B,4BAA4B,MAAM,OAAO,wBAAc,CAAA,MAAA,MAAA,EAAA,CAAA;CAC9F,MAAM,EAAE,YAAY,eAAe,MAAM,OAAO;CAEhD,MAAM,YAAY,oBAAoB;CACtC,MAAM,4BAA4B,cAAc;CAChD,MAAM,eAAe,MAAM,WAAW;CACtC,MAAM,kBAAkB,4BAA4B,oBAAoB,IAAI;CAC5E,MAAM,SAAS;EACb,GAAG;EACH,SAAS,4BAA4B,cAAc,aAAa,OAAO,IAAI,aAAa;CAC1F;CACA,MAAM,SAAS,gBAAgB,MAAM;CACrC,MAAM,OAAO,KAAK,eAAe;EAC/B,UAAU,OAAO;EACjB,gBAAgB,iBAAiB;EACjC,YAAY;EACZ,gBAAgB,OAAO;EACvB,oBAAoB,wBAAwB,MAAM;EAClD,UAAU,OAAO;CACnB,CAAC;CACD,MAAM,mBAAmB,wBAAwB,MAAM;CAKvD,MAAM,eAAe,IAAI,OAAO;EAAE,MAAM;EAA+B,SAAS;CAAgB,CAAC;CACjG,IAAI,kBAAkB;CACtB,IAAI;CACJ,IAAI;CAEJ,IAAI;EACF,WAAW,MAAM,8BAA8B,MAAM;CACvD,SAAS,KAAK;EACZ,MAAM,OAAO,MAAM,6BAA6B;GAC9C,UAAU;GACV,OAAO,YAAY,GAAG;EACxB,CAAC;EACD,2BAA2B,6CAA6C,iBAAiB,IAAK,IAAc;EAC5G,QAAQ,OAAO,MACb,+CAA+C,yBAAyB,oDAC1E;CACF;CAEA,IAAI,UACF,IAAI;EACF,MAAM,aAAa,QACjB,IAAI,8BAA8B,IAAI,IAAI,gBAAgB,GAAG,EAAE,OAAO,SAAS,CAAC,CAClF;EACA,kBAAkB;EAClB,MAAM,OAAO,KAAK,kBAAkB;GAClC,WAAW;GACX,UAAU;EACZ,CAAC;CACH,QAAQ;EACN,MAAM,OAAO,MAAM,yBAAyB;GAC1C,WAAW;GACX,UAAU;EACZ,CAAC;EAED,IAAI;GACF,MAAM,EAAE,uBAAuB,MAAM,OAAO;GAC5C,MAAM,aAAa,QACjB,IAAI,mBAAmB,IAAI,IAAI,gBAAgB,GAAG,EAAE,OAAO,SAAS,CAAC,CACvE;GACA,kBAAkB;GAClB,MAAM,OAAO,KAAK,kBAAkB;IAClC,WAAW;IACX,UAAU;GACZ,CAAC;EACH,SAAS,MAAM;GACb,MAAM,OAAO,MAAM,yBAAyB;IAC1C,WAAW;IACX,UAAU;IACV,OAAO,YAAY,IAAI;GACzB,CAAC;GACD,2BAA2B,uCAAuC,iBAAiB,IAAK,KAAe;GACvG,QAAQ,OAAO,MACb,+CAA+C,yBAAyB,oDAC1E;EACF;CACF;CAEF,IAAI,iBACF,2BAA2B,cAA6C,MAAM;CAGhF,IAAI,QAA0B,MAAM,WAAW,gBAAgB;CAE/D,IAAI,CAAC,SAAS,iBAAiB;EAG7B,SAAQ,MADa,aAAa,UAAU,EAAA,CAC7B;EACf,MAAM,WAAW,OAAO,gBAAgB;EACxC,MAAM,OAAO,KAAK,uBAAuB;GACvC,QAAQ;GACR,OAAO,MAAM;EACf,CAAC;CACH,OAAO,IAAI,OACT,MAAM,OAAO,KAAK,uBAAuB;EACvC,QAAQ;EACR,OAAO,MAAM;CACf,CAAC;MACI;EACL,QAAQ,4BAA4B;EACpC,MAAM,OAAO,KAAK,uBAAuB;GACvC,QAAQ;GACR,OAAO,MAAM;EACf,CAAC;CACH;CACA,MAAM,kBAAkB,IAAI,KAAK,SAAS,CAAC,EAAA,CAAG,KAAK,SAAS,KAAK,IAAI,CAAC;CAGtE,MAAM,SAAS,IAAI,UACjB;EAAE,MAAM;EAAkB,SAAS;CAAgB,GACnD,EACE,cAAc,4BAA4B,sBAAsB,8BAClE,CACF;CACA,mBAAmB,QAAQ,MAAM;CAEjC,IAAI,iBACF,IAAI;EACF,MAAM,aAAa,YAAY;CACjC,SAAS,KAAK;EACZ,MAAM,OAAO,MAAM,yBAAyB;GAC1C,UAAU;GACV,OAAO,YAAY,GAAG;EACxB,CAAC;EACD,QAAQ,OAAO,MACb,4DAA4D,iBAAiB,IAAK,IAAc,QAAQ,GAC1G;CACF;CAGF,qBAAqB,MAAM;CAE3B,OAAO,aACL,6BACA;EACE,OAAO;EACP,aAAa,+BAA+B;EAC5C,aAAa;EACb,aAAa;GACX,cAAc;GACd,iBAAiB;GACjB,gBAAgB;GAChB,eAAe;EACjB;CACF,GACA,YAAY;EACV,IAAI,mBAAmB,gBAAgB,IAAI,sBAAsB,GAC/D,IAAI;GAKF,OAAO,eAAe,yBAAyB,MAJ1B,aAAa,SAAS;IACzC,MAAM;IACN,WAAW,CAAC;GACd,CAAC,CACoD,CAAC;EACxD,SAAS,KAAK;GACZ,OAAO;IACL,SAAS,CACP;KACE,MAAM;KACN,MAAM,gCAAiC,IAAc;IACvD,CACF;IACA,SAAS;GACX;EACF;EAEF,OAAO,eAAe,yBAAyB,KAAA,CAAS,CAAC;CAC3D,CACF;CAKA,MAAM,qBAA8C;EAClD,UAAU;EACV,YAAY,OAAO,OAAO;GACxB,IAAI,CAAC,iBACH,MAAM,IAAI,iBACR,GAAG,4BAA4B,4CAA4C,mBAAmB,wEAChG;GAEF,OAAO,GAAG,YAAY;EACxB;EACA,kBAAkB,QAAQ,sBAAsB,eAAe,GAAG;CACpE;CACA,OAAO,aACL,eACA;EACE,OAAO;EACP,aAAa,+BAA+B;EAC5C,aAAa;GACX,SAAS;GACT,SAAS,EACN,QAAQ,CAAC,CACT,SAAS,CAAC,CACV,SAAS,8DAA8D;EAC5E;EACA,aAAa;GACX,cAAc;GACd,iBAAiB;GACjB,gBAAgB;GAChB,eAAe;EACjB;CACF,GACA,OAAO,SAAS,iBAAiB,MAAM,kBAAkB,CAC3D;CAEA,OAAO,aACL,qBACA;EACE,OAAO;EACP,aAAa,+BAA+B;EAC5C,aAAa;EACb,OAAO;EACP,aAAa;GACX,cAAc;GACd,iBAAiB;GACjB,gBAAgB;GAChB,eAAe;EACjB;CACF,GACA,YAAY;EACV,IAAI;GACF,IAAI,CAAC,iBACH,OAAO;IACL,SAAS,CACP;KACE,MAAM;KACN,MAAM,GAAG,4BAA4B,4CAA4C,mBAAmB;IACtG,CACF;IACA,SAAS;GACX;GAEF,IAAI,CAAC,gBAAgB,IAAI,cAAc,GACrC,OAAO,eAAe,4BAA4B,gBAAgB,CAAC;GAErE,MAAM,SAAU,MAAM,aAAa,SAAS;IAC1C,MAAM;IACN,WAAW,CAAC;GACd,CAAC;GACD,MAAM,oBAAoB,SAAS,OAAO,iBAAiB,IACvD;IAAE,GAAG,OAAO;IAAmB,MAAM;GAAoB,IACzD,KAAA;GACJ,OAAO;IACL,SAAS,oBACL,CAAC;KAAE,MAAM;KAAiB,MAAM,KAAK,UAAU,mBAAmB,MAAM,CAAC;IAAE,CAAC,IAC3E,OAAO,WAAW,CAAC;IACxB;IACA,OAAO,OAAO;IACd,SAAS,OAAO;GAClB;EACF,SAAS,KAAK;GACZ,OAAO;IACL,SAAS,CACP;KAAE,MAAM;KAAiB,MAAM,wBAAyB,IAAc;IAAU,CAClF;IACA,SAAS;GACX;EACF;CACF,CACF;CAEA,OAAO,aACL,4BACA;EACE,OAAO;EACP,aAAa,+BAA+B;EAC5C,aAAa;EACb,OAAO;EACP,aAAa;GACX,cAAc;GACd,iBAAiB;GACjB,gBAAgB;GAChB,eAAe;EACjB;CACF,GACA,YAAY;EAIV,IAAI;GACF,IAAI,CAAC,iBACH,OAAO,eACL,8BACE,kBACA,4BACE,4CAA4C,kBAChD,CACF;GAEF,IAAI,CAAC,gBAAgB,IAAI,qBAAqB,GAC5C,OAAO,eACL,8BACE,kBACA,kFACF,CACF;GAEF,MAAM,gBAAgB,MAAM,+BAA+B;GAC3D,IAAI,CAAC,eACH,OAAO,eACL,8BACE,kBACA,8FACF,CACF;GAEF,MAAM,SAAU,MAAM,aAAa,SAAS;IAC1C,MAAM;IACN,WAAW,EAAE,QAAQ,cAAc;GACrC,CAAC;GACD,IAAI,OAAO,YAAY,MAAM;IAC3B,MAAM,YAAY,MAAM,QAAQ,OAAO,OAAO,IAC1C,OAAO,QAAQ,MACZ,UACC,MAAM,SAAS,UAAU,OAAO,MAAM,SAAS,QACnD,IACA,KAAA;IACJ,MAAM,SAAS,WAAW,OACtB,+BAA+B,UAAU,SACzC;IACJ,OAAO,eAAe,8BAA8B,kBAAkB,MAAM,CAAC;GAC/E;GACA,MAAM,oBAAoB,SAAS,OAAO,iBAAiB,IACvD;IAAE,GAAG,OAAO;IAAmB,MAAM;GAA2B,IAChE,KAAA;GACJ,OAAO;IACL,SAAS,oBACL,CAAC;KAAE,MAAM;KAAiB,MAAM,KAAK,UAAU,mBAAmB,MAAM,CAAC;IAAE,CAAC,IAC3E,OAAO,WAAW,CAAC;IACxB;IACA,OAAO,OAAO;IACd,SAAS,OAAO;GAClB;EACF,SAAS,KAAK;GACZ,OAAO,eACL,8BACE,kBACA,+BAAgC,IAAc,SAChD,CACF;EACF;CACF,CACF;CAEA,OAAO,aACL,kBACA;EACE,OAAO;EACP,aAAa,+BAA+B;EAC5C,aAAa;EACb,aAAa;GACX,cAAc;GACd,iBAAiB;GACjB,gBAAgB;GAChB,eAAe;EACjB;CACF,GACA,YAAY;EACV,IAAI;GACF,MAAM,EAAE,2BAA2B,kBAAkB,2BACnD,MAAM,OAAO,uBAAqB,CAAA,MAAA,MAAA,EAAA,CAAA;GAEpC,MAAM,oBAAoB,MAAM,uBAAuB,MADjC,iBAAiB,CACuB;GAC9D,OAAO;IACL,SAAS,CAAC;KAAE,MAAM;KAAiB,MAAM,0BAA0B,iBAAiB;IAAE,CAAC;IACpE;IACnB,SAAS;GACX;EACF,SAAS,KAAK;GACZ,OAAO;IACL,SAAS,CAAC;KAAE,MAAM;KAAiB,MAAM,mBAAoB,IAAc;IAAU,CAAC;IACtF,SAAS;GACX;EACF;CACF,CACF;CAMA,MAAM,oBAAuC;EAC3C,YAAY,OAAO,SAAS;GAC1B,MAAM,UAAU,yBAAyB,aAAa;GACtD,OAAQ,MAAM,yBACZ,aAAa,SAAS;IAAE,MAAM;IAAe,WAAW;GAAK,GAAG,KAAA,GAAW,OAAO,CACpF;EACF;EACA,kBAAkB,QAAQ,sBAAsB,eAAe,GAAG;EAClE,mBACE,kBACI,KAAA,IACA,GAAG,4BAA4B,4CAA4C,mBAAmB;CACtG;CACA,OAAO,aACL,mBACA;EACE,OAAO;EACP,aAAa;EACb,aAAa;GACX,SAAS,EAAE,OAAO;GAClB,SAAS,EAAE,OAAO,CAAC,CAAC,SAAS;GAC7B,WAAW,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,gBAAgB,CAAC,CAAC,SAAS;GAClE,YAAY,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,gBAAgB,CAAC,CAAC,SAAS;GACnE,MAAM,EAAE,OAAO,CAAC,CAAC,SAAS;GAC1B,IAAI,EAAE,OAAO,CAAC,CAAC,SAAS;GACxB,KAAK,EAAE,OAAO,CAAC,CAAC,SAAS;EAC3B;EACA,aAAa;EACb,OAAO;CACT,GACA,OAAO,SAAS,kBAAkB,MAAM,iBAAiB,CAC3D;CAEA,OAAO,aACL,aACA;EACE,OAAO;EACP,aAAa,+BAA+B;EAC5C,aAAa;EACb,aAAa;GACX,cAAc;GACd,iBAAiB;GACjB,gBAAgB;GAChB,eAAe;EACjB;CACF,GACA,aAAa;EACX,SAAS,CACP;GACE,MAAM;GACN,MAAM,4BACF;IACE;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;GACF,CAAC,CAAC,KAAK,IAAI,IACX;IACE;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;GACF,CAAC,CAAC,KAAK,IAAI;EACjB,CACF;EACA,SAAS;CACX,EACF;CAGA,KAAK,MAAM,QAAQ,SAAS,CAAC,GAAG;EAC9B,IAAI,yBAAyB,IAAI,KAAK,IAAI,GAAG;EAC7C,IAAI,iBAAiB,IAAI,KAAK,IAAI,GAAG;EAErC,IAAI,cAAc,IAAI,GAAG;EACzB,MAAM,cAAc,2BAA2B,KAAK,IAAI,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,YAAY;EACtF,MAAM,UAAU,OAAO,SAAkB;GACvC,IAAI;IACF,IAAI,CAAC,iBACH,OAAO;KACL,SAAS,CACP;MACE,MAAM;MACN,MAAM,GAAG,4BAA4B,4CAA4C,mBAAmB;KACtG,CACF;KACA,SAAS;IACX;IAEF,MAAM,iBAAiB,6BAA6B,KAAK,MAAM,IAAI;IACnE,MAAM,kBAAkB,iCAAiC,KAAK,MAAM,cAAc;IAClF,IAAI,iBACF,OAAO;KACL,SAAS,CAAC;MAAE,MAAM;MAAiB,MAAM;KAAgB,CAAC;KAC1D,SAAS;IACX;IAEF,MAAM,UAAU;KACd,MAAM,KAAK;KACX,WAAW;IACb;IACA,MAAM,iBAAiB,yBAAyB,KAAK,IAAI;IACzD,MAAM,SAAS,MAAM,yBACnB,iBACI,aAAa,SAAS,SAAS,KAAA,GAAW,cAAc,IACxD,aAAa,SAAS,OAAO,CACnC;IACA,OAAO,0BAA0B,KAAK,MAAM,MAA0B;GACxE,SAAS,KAAK;IACZ,MAAM,UAAU,kBAAkB,KAAK,MAAM,GAAG;IAChD,IAAI,SAAS,OAAO;IACpB,OAAO;KACL,SAAS,CAAC;MAAE,MAAM;MAAiB,MAAM,sBAAsB,KAAK,MAAM,GAAG;KAAE,CAAC;KAChF,SAAS;IACX;GACF;EACF;EAIA,MAAM,cAAc,2BAA2B,KAAK,IAAI;EACxD,MAAM,YAAY,KAAK,SAAS;EAChC,MAAM,aAAa;GACjB,OAAO,KAAK,SAAS,yBAAyB,KAAK;GACnD,aAAa,4BAA4B,IAAI;GAC7C;GACA,GAAI,cAAc,EAAE,YAAY,IAAI,CAAC;GACrC,GAAI,YAAY,EAAE,OAAO,eAAe,IAAI,CAAC;EAC/C;EAEA,OAAO,aAAa,KAAK,MAAM,YAAY,OAAO;CACpD;CAKA,MAAM,EAAE,mBAAmB,mBAAmB,MAAM,OAAO,8BAAsC,CAAA,MAAA,MAAA,EAAA,CAAA;CACjG,MAAM,kBAAkB,QAAQ,EAAE,eAAe,wBAAwB,CAAC;CAG1E,OAAO,iBACL,uBACA,iBACA;EACE,OAAO;EACP,aACE;EACF,UAAU;EACV,OAAO;CACT,GACA,aAAa,EACX,UAAU,CACR;EACE,KAAK;EACL,UAAU;EACV,MAAM,mBAAmB;EACzB,OAAO;CACT,CACF,EACF,EACF;CAGA,MAAM,YAAY,IAAI,qBAAqB;CAC3C,MAAM,OAAO,QAAQ,SAAS;CAC9B,MAAM,OAAO,KAAK,eAAe;EAC/B,OAAO,CACL,GAAG,CAAC,GAAG,gBAAgB,CAAC,CAAC,QAAQ,SAAS,CAAC,0BAA0B,IAAI,IAAI,CAAC,GAC9E,IAAI,SAAS,CAAC,EAAA,CACX,QAAQ,SAAS,CAAC,cAAc,IAAI,CAAC,CAAC,CACtC,KAAK,SAAS,KAAK,IAAI,CAAC,CACxB,QAAQ,SAAS,CAAC,yBAAyB,IAAI,IAAI,KAAK,CAAC,iBAAiB,IAAI,IAAI,CAAC,CACxF,CAAC,CAAC;EACF,cAAc,CAAC,iBAAiB,cAAc;CAChD,CAAC;CAGD,MAAM,WAAW,YAAY;EAC3B,MAAM,OAAO,KAAK,gBAAgB;EAClC,UAAU,MAAM;EAChB,QAAQ,KAAK,CAAC;CAChB;CACA,QAAQ,GAAG,gBAAgB;EACzB,SAAc;CAChB,CAAC;CACD,QAAQ,GAAG,iBAAiB;EAC1B,SAAc;CAChB,CAAC;AACH;AAIA,IAAI,QAAQ,KAAK,MAAM,YAAY,IAAI,SAAS,QAAQ,KAAK,EAAE,CAAC,QAAQ,OAAO,GAAG,CAAC,GACjF,YAAY,CAAC,CAAC,OAAO,QAAQ;CAC3B,QAAQ,OAAO,MAAM,4CAA6C,IAAc,QAAQ,GAAG;CAC3F,QAAQ,KAAK,CAAC;AAChB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: chain-insights-cypher
|
|
3
|
-
description: Load this before the first graph_query or graph_query_batch call: without it, most queries are refused. Use when answering any question about addresses, money flows, transfers, transactions or blocks with Chain Insights, when tracing where money went or came from, when writing or reviewing graph_query or graph_query_batch ISO GQL, when choosing between USE topology, USE facts and USE chain, or when a graph query is refused. Address, unit, layer, graph search and
|
|
3
|
+
description: Load this before the first graph_query or graph_query_batch call: without it, most queries are refused. Use when answering any question about addresses, money flows, transfers, transactions or blocks with Chain Insights, when tracing where money went or came from, when writing or reviewing graph_query or graph_query_batch ISO GQL, when choosing between USE topology, USE facts and USE chain, or when a graph query is refused. Address, unit, layer, graph search, refusal and answer rules (full addresses, no query text in replies, tokens are not dollars), with one query for each kind of search.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Chain Insights Cypher
|
|
@@ -18,9 +18,10 @@ Always pass `network` and your own `LIMIT`. These two commands, and
|
|
|
18
18
|
## Rules that stop most failures
|
|
19
19
|
|
|
20
20
|
1. Write every address in full, in lowercase: `0x` plus 40 hex characters, 42
|
|
21
|
-
in all. Never shorten one with `...` or `…`, in a query
|
|
22
|
-
Copy addresses from results exactly
|
|
23
|
-
Robinhood Chain: use it without asking
|
|
21
|
+
in all. Never shorten one with `...` or `…`, in a query, a table, a sentence
|
|
22
|
+
or a summary. Copy addresses from results exactly, all 42 characters, every
|
|
23
|
+
time. The network is `robinhood`, the Robinhood Chain: use it without asking
|
|
24
|
+
the user which network.
|
|
24
25
|
2. Timestamps on every layer are integer milliseconds since the epoch, UTC,
|
|
25
26
|
and `USE chain` is no exception. Compute them from the current date: a day
|
|
26
27
|
is 86,400,000 ms. Write the number as a literal: the server has no `now()`,
|
|
@@ -36,6 +37,16 @@ Always pass `network` and your own `LIMIT`. These two commands, and
|
|
|
36
37
|
6. After a refusal, fix the query once from `fix` and `example`. A second
|
|
37
38
|
refusal ends it: tell the user plainly what cannot be asked and what can.
|
|
38
39
|
Never send variations in a loop.
|
|
40
|
+
7. Answer in plain text. Do not print the query, its `USE` line, its `LIMIT`,
|
|
41
|
+
the epoch numbers you computed, or a `Ref:` line. The user does not need
|
|
42
|
+
them. Show them only when the user asks how a number was found.
|
|
43
|
+
8. Never draw the graph yourself, in ASCII art or in Mermaid. Claude Desktop
|
|
44
|
+
draws the picture from the column names (see Name the columns for a
|
|
45
|
+
picture). Write a short text answer beside it.
|
|
46
|
+
9. Tokens are not dollars. A transfer moves tokens, and its `amount_usd` is
|
|
47
|
+
their value at the day's average price. Say "tokens worth about 183 USD on
|
|
48
|
+
2026-10-06", never "sent 183 USD". For the assets behind a flow, use the
|
|
49
|
+
facts read in Value of one day's flow.
|
|
39
50
|
|
|
40
51
|
## Pick the layer first
|
|
41
52
|
|
|
@@ -280,6 +291,16 @@ MATCH (a:Address {address: "0x31a817802ee183eb8b13167ffe24bd28dcc6f30c"})-[t:TRA
|
|
|
280
291
|
RETURN t.tx_id AS tx_id, t.block_timestamp AS block_timestamp, t.asset_symbol AS asset_symbol, t.amount AS amount, t.amount_usd AS amount_usd, t.kind AS kind LIMIT 50
|
|
281
292
|
```
|
|
282
293
|
|
|
294
|
+
### Value of one day's flow
|
|
295
|
+
|
|
296
|
+
A topology link gives the pair's lifetime total, not the day's. To say what a
|
|
297
|
+
pair moved on one day, read its `TRANSFER` rows for that day, then:
|
|
298
|
+
|
|
299
|
+
1. List each row's `asset_symbol` and `amount`. Show the assets, not only a total.
|
|
300
|
+
2. Add the `amount_usd` values for the USD figure, after you convert the text to numbers. `amount_usd` is each row's value at the day's average price.
|
|
301
|
+
3. If a row's `price_missing` is true, show its amount and say no price exists for that asset. Never guess a USD value.
|
|
302
|
+
4. Say "tokens worth about X USD on DAY", because the row moved tokens.
|
|
303
|
+
|
|
283
304
|
## Chain: one key, one node
|
|
284
305
|
|
|
285
306
|
`USE chain` asks the chain node for one known thing: `Transaction` by `tx_id`, `Block` by `block_height` or `block_hash` (exactly one), `Address` by `address` (with an optional `at_block`), or `Head`. One node, literal keys in braces, a `RETURN` of `var.property` items, no `WHERE`, no range. Read its limits in `chain_admission` (`cia network robinhood --json`).
|
|
@@ -345,3 +366,5 @@ Claude Desktop draws a `graph_query` answer from its column names. Alias the
|
|
|
345
366
|
A graph also reads these optional columns: `amount_usd_sum`, `tx_count`, `first_seen_timestamp`, `last_seen_timestamp`, `link_kind`, `from_labels` and `to_labels`. In `graph_query_batch`, each query draws its own tab. Claude Code draws nothing and shows the rows as text.
|
|
346
367
|
|
|
347
368
|
A graph is the newest-links read of one address, receivers or senders, in Graph searches. A chart is the facts read of one pair over one day above, with `block_timestamp` and `amount_usd` in the `RETURN`.
|
|
369
|
+
|
|
370
|
+
A row that names its ends `src`, `dst` or `address` draws no graph. Alias them `from_address` and `to_address`.
|