chain-insights 0.42.1 → 0.42.2
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/{call-args-ysbB_xdA.mjs → call-args-DPWeqIla.mjs} +3 -1
- package/dist/call-args-DPWeqIla.mjs.map +1 -0
- package/dist/{call-args-DAiQSDmD.cjs → call-args-Db8KXsQx.cjs} +2 -0
- package/dist/cli.cjs +6 -6
- package/dist/cli.mjs +6 -6
- package/dist/{flows-BfUlyWvJ.mjs → flows-9XJmeF_M.mjs} +27 -8
- package/dist/flows-9XJmeF_M.mjs.map +1 -0
- package/dist/{flows-CzsxUf35.cjs → flows-CJSNSXkH.cjs} +26 -7
- package/dist/mcp-proxy.cjs +5 -3
- package/dist/mcp-proxy.mjs +5 -3
- package/dist/mcp-proxy.mjs.map +1 -1
- package/dist/{tool-visibility-wW2vAaj9.mjs → tool-visibility-BdncNrVB.mjs} +7 -2
- package/dist/tool-visibility-BdncNrVB.mjs.map +1 -0
- package/dist/{tool-visibility-BdsE6vQB.cjs → tool-visibility-CfMxSzf8.cjs} +6 -1
- package/package.json +7 -7
- package/dist/call-args-ysbB_xdA.mjs.map +0 -1
- package/dist/flows-BfUlyWvJ.mjs.map +0 -1
- package/dist/tool-visibility-wW2vAaj9.mjs.map +0 -1
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
//#region src/mcp/call-args.ts
|
|
2
2
|
const NUMERIC_ARG_KEYS = /* @__PURE__ */ new Set([
|
|
3
|
+
"in_offset",
|
|
4
|
+
"out_offset",
|
|
3
5
|
"per_query_timeout_seconds",
|
|
4
6
|
"incident_timestamp",
|
|
5
7
|
"max_hops",
|
|
@@ -26,4 +28,4 @@ function parseMcpCallArgs(rawArgs) {
|
|
|
26
28
|
//#endregion
|
|
27
29
|
export { parseMcpCallArgs };
|
|
28
30
|
|
|
29
|
-
//# sourceMappingURL=call-args-
|
|
31
|
+
//# sourceMappingURL=call-args-DPWeqIla.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"call-args-DPWeqIla.mjs","names":[],"sources":["../src/mcp/call-args.ts"],"sourcesContent":["// `cia mcp call tool key=value` yields strings. A key listed here is coerced\n// to a number; anything else stays a string and is rejected by the tool's\n// numeric zod schema. Every tunable search bound must therefore appear here,\n// or passing it from the CLI fails with a confusing type error.\nconst NUMERIC_ARG_KEYS = new Set([\n 'in_offset',\n 'out_offset',\n 'per_query_timeout_seconds',\n 'incident_timestamp',\n 'max_hops',\n 'per_address_limit',\n 'row_limit',\n])\n\nfunction parseMcpArgValue(key: string, value: string): unknown {\n const trimmed = value.trim()\n if (!trimmed) return value\n\n if (trimmed.startsWith('{') || trimmed.startsWith('[')) {\n return JSON.parse(trimmed) as unknown\n }\n\n if (NUMERIC_ARG_KEYS.has(key) && /^-?\\d+(?:\\.\\d+)?$/.test(trimmed)) {\n return Number(trimmed)\n }\n\n return value\n}\n\nexport function parseMcpCallArgs(rawArgs: string[]): Record<string, unknown> {\n const args: Record<string, unknown> = {}\n for (const pair of rawArgs) {\n const eqIdx = pair.indexOf('=')\n if (eqIdx === -1) {\n throw new Error(`Invalid arg format: ${pair} (expected key=value)`)\n }\n const key = pair.slice(0, eqIdx)\n args[key] = parseMcpArgValue(key, pair.slice(eqIdx + 1))\n }\n return args\n}\n"],"mappings":";AAIA,MAAM,mCAAmB,IAAI,IAAI;CAC/B;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;AAED,SAAS,iBAAiB,KAAa,OAAwB;CAC7D,MAAM,UAAU,MAAM,KAAK;CAC3B,IAAI,CAAC,SAAS,OAAO;CAErB,IAAI,QAAQ,WAAW,GAAG,KAAK,QAAQ,WAAW,GAAG,GACnD,OAAO,KAAK,MAAM,OAAO;CAG3B,IAAI,iBAAiB,IAAI,GAAG,KAAK,oBAAoB,KAAK,OAAO,GAC/D,OAAO,OAAO,OAAO;CAGvB,OAAO;AACT;AAEA,SAAgB,iBAAiB,SAA4C;CAC3E,MAAM,OAAgC,CAAC;CACvC,KAAK,MAAM,QAAQ,SAAS;EAC1B,MAAM,QAAQ,KAAK,QAAQ,GAAG;EAC9B,IAAI,UAAU,IACZ,MAAM,IAAI,MAAM,uBAAuB,KAAK,sBAAsB;EAEpE,MAAM,MAAM,KAAK,MAAM,GAAG,KAAK;EAC/B,KAAK,OAAO,iBAAiB,KAAK,KAAK,MAAM,QAAQ,CAAC,CAAC;CACzD;CACA,OAAO;AACT"}
|
package/dist/cli.cjs
CHANGED
|
@@ -552,7 +552,7 @@ program.command("mcp").description("Low-level access to the Chain Insights MCP e
|
|
|
552
552
|
try {
|
|
553
553
|
const { loadSchema, saveSchema } = await Promise.resolve().then(() => require("./schema-cache-n7Gbhvsj.cjs"));
|
|
554
554
|
const { formatToolsTable } = await Promise.resolve().then(() => require("./format-BjZuYDr8.cjs"));
|
|
555
|
-
const { visibleRemoteTools } = await Promise.resolve().then(() => require("./tool-visibility-
|
|
555
|
+
const { visibleRemoteTools } = await Promise.resolve().then(() => require("./tool-visibility-CfMxSzf8.cjs")).then((n) => n.tool_visibility_exports);
|
|
556
556
|
const { loadConfig } = await Promise.resolve().then(() => require("./config-BxtMoo9c.cjs")).then((n) => n.config_exports);
|
|
557
557
|
const { createConfiguredGraphMcpFetch, resolveGraphMcpEndpoint } = await Promise.resolve().then(() => require("./client-Ypw9BNbn.cjs")).then((n) => n.client_exports);
|
|
558
558
|
const config = await loadConfig();
|
|
@@ -574,7 +574,7 @@ program.command("mcp").description("Low-level access to the Chain Insights MCP e
|
|
|
574
574
|
await client.close();
|
|
575
575
|
}
|
|
576
576
|
}
|
|
577
|
-
const { MONEY_FLOWS_TOOL, MONEY_FLOWS_DESCRIPTION } = await Promise.resolve().then(() => require("./flows-
|
|
577
|
+
const { MONEY_FLOWS_TOOL, MONEY_FLOWS_DESCRIPTION } = await Promise.resolve().then(() => require("./flows-CJSNSXkH.cjs")).then((n) => n.flows_exports);
|
|
578
578
|
const listed = visibleRemoteTools(tools).filter((t) => t.name !== MONEY_FLOWS_TOOL);
|
|
579
579
|
console.log(formatToolsTable([...listed, {
|
|
580
580
|
name: MONEY_FLOWS_TOOL,
|
|
@@ -586,8 +586,8 @@ program.command("mcp").description("Low-level access to the Chain Insights MCP e
|
|
|
586
586
|
}
|
|
587
587
|
})).addCommand(createCliCommand("call").description("Call a low-level MCP tool directly (advanced)").option("--json", "Print machine-readable JSON output").argument("<tool>", "Tool name to call").argument("[args...]", "Key=value arguments (e.g. address=0x1234... network=robinhood)").action(async (tool, rawArgs, opts) => {
|
|
588
588
|
try {
|
|
589
|
-
const { parseMcpCallArgs } = await Promise.resolve().then(() => require("./call-args-
|
|
590
|
-
const { assertPublicMcpToolName, validatePublicMcpToolArguments } = await Promise.resolve().then(() => require("./tool-visibility-
|
|
589
|
+
const { parseMcpCallArgs } = await Promise.resolve().then(() => require("./call-args-Db8KXsQx.cjs"));
|
|
590
|
+
const { assertPublicMcpToolName, validatePublicMcpToolArguments } = await Promise.resolve().then(() => require("./tool-visibility-CfMxSzf8.cjs")).then((n) => n.tool_visibility_exports);
|
|
591
591
|
const args = parseMcpCallArgs(rawArgs);
|
|
592
592
|
assertPublicMcpToolName(tool);
|
|
593
593
|
if (tool === "aml_address_risk") throw new Error("aml_address_risk is a CIA workflow. Run `cia workflow aml-address-risk` instead.");
|
|
@@ -604,7 +604,7 @@ program.command("mcp").description("Low-level access to the Chain Insights MCP e
|
|
|
604
604
|
if (tool === "graph_expand") throw new Error("graph_expand is used only by the money-flow view in Claude. Run `cia mcp call money_flows` instead.");
|
|
605
605
|
if (tool === "money_flows") {
|
|
606
606
|
await withGraphMcpClient("chain-insights-cli-call", async (client) => {
|
|
607
|
-
const { handleMoneyFlows } = await Promise.resolve().then(() => require("./flows-
|
|
607
|
+
const { handleMoneyFlows } = await Promise.resolve().then(() => require("./flows-CJSNSXkH.cjs")).then((n) => n.flows_exports);
|
|
608
608
|
const { graphToolRequestOptions } = await Promise.resolve().then(() => require("./request-timeout-CYmQF8N7.cjs"));
|
|
609
609
|
const result = await handleMoneyFlows(args, {
|
|
610
610
|
graphQuery: async (queryArgs) => await client.callTool({
|
|
@@ -678,7 +678,7 @@ program.command("mcp").description("Low-level access to the Chain Insights MCP e
|
|
|
678
678
|
});
|
|
679
679
|
});
|
|
680
680
|
} catch (err) {
|
|
681
|
-
const { formatMcpCallError } = await Promise.resolve().then(() => require("./tool-visibility-
|
|
681
|
+
const { formatMcpCallError } = await Promise.resolve().then(() => require("./tool-visibility-CfMxSzf8.cjs")).then((n) => n.tool_visibility_exports);
|
|
682
682
|
console.error(formatMcpCallError(tool, err));
|
|
683
683
|
process.exit(1);
|
|
684
684
|
}
|
package/dist/cli.mjs
CHANGED
|
@@ -550,7 +550,7 @@ program.command("mcp").description("Low-level access to the Chain Insights MCP e
|
|
|
550
550
|
try {
|
|
551
551
|
const { loadSchema, saveSchema } = await import("./schema-cache-DJdU4MIP.mjs");
|
|
552
552
|
const { formatToolsTable } = await import("./format-DB2IH2U5.mjs");
|
|
553
|
-
const { visibleRemoteTools } = await import("./tool-visibility-
|
|
553
|
+
const { visibleRemoteTools } = await import("./tool-visibility-BdncNrVB.mjs").then((n) => n.a);
|
|
554
554
|
const { loadConfig } = await import("./config-9MabJh5R.mjs").then((n) => n.t);
|
|
555
555
|
const { createConfiguredGraphMcpFetch, resolveGraphMcpEndpoint } = await import("./client-gsLi528K.mjs").then((n) => n.r);
|
|
556
556
|
const config = await loadConfig();
|
|
@@ -572,7 +572,7 @@ program.command("mcp").description("Low-level access to the Chain Insights MCP e
|
|
|
572
572
|
await client.close();
|
|
573
573
|
}
|
|
574
574
|
}
|
|
575
|
-
const { MONEY_FLOWS_TOOL, MONEY_FLOWS_DESCRIPTION } = await import("./flows-
|
|
575
|
+
const { MONEY_FLOWS_TOOL, MONEY_FLOWS_DESCRIPTION } = await import("./flows-9XJmeF_M.mjs").then((n) => n.s);
|
|
576
576
|
const listed = visibleRemoteTools(tools).filter((t) => t.name !== MONEY_FLOWS_TOOL);
|
|
577
577
|
console.log(formatToolsTable([...listed, {
|
|
578
578
|
name: MONEY_FLOWS_TOOL,
|
|
@@ -584,8 +584,8 @@ program.command("mcp").description("Low-level access to the Chain Insights MCP e
|
|
|
584
584
|
}
|
|
585
585
|
})).addCommand(createCliCommand("call").description("Call a low-level MCP tool directly (advanced)").option("--json", "Print machine-readable JSON output").argument("<tool>", "Tool name to call").argument("[args...]", "Key=value arguments (e.g. address=0x1234... network=robinhood)").action(async (tool, rawArgs, opts) => {
|
|
586
586
|
try {
|
|
587
|
-
const { parseMcpCallArgs } = await import("./call-args-
|
|
588
|
-
const { assertPublicMcpToolName, validatePublicMcpToolArguments } = await import("./tool-visibility-
|
|
587
|
+
const { parseMcpCallArgs } = await import("./call-args-DPWeqIla.mjs");
|
|
588
|
+
const { assertPublicMcpToolName, validatePublicMcpToolArguments } = await import("./tool-visibility-BdncNrVB.mjs").then((n) => n.a);
|
|
589
589
|
const args = parseMcpCallArgs(rawArgs);
|
|
590
590
|
assertPublicMcpToolName(tool);
|
|
591
591
|
if (tool === "aml_address_risk") throw new Error("aml_address_risk is a CIA workflow. Run `cia workflow aml-address-risk` instead.");
|
|
@@ -602,7 +602,7 @@ program.command("mcp").description("Low-level access to the Chain Insights MCP e
|
|
|
602
602
|
if (tool === "graph_expand") throw new Error("graph_expand is used only by the money-flow view in Claude. Run `cia mcp call money_flows` instead.");
|
|
603
603
|
if (tool === "money_flows") {
|
|
604
604
|
await withGraphMcpClient("chain-insights-cli-call", async (client) => {
|
|
605
|
-
const { handleMoneyFlows } = await import("./flows-
|
|
605
|
+
const { handleMoneyFlows } = await import("./flows-9XJmeF_M.mjs").then((n) => n.s);
|
|
606
606
|
const { graphToolRequestOptions } = await import("./request-timeout-lBBe0mGf.mjs");
|
|
607
607
|
const result = await handleMoneyFlows(args, {
|
|
608
608
|
graphQuery: async (queryArgs) => await client.callTool({
|
|
@@ -676,7 +676,7 @@ program.command("mcp").description("Low-level access to the Chain Insights MCP e
|
|
|
676
676
|
});
|
|
677
677
|
});
|
|
678
678
|
} catch (err) {
|
|
679
|
-
const { formatMcpCallError } = await import("./tool-visibility-
|
|
679
|
+
const { formatMcpCallError } = await import("./tool-visibility-BdncNrVB.mjs").then((n) => n.a);
|
|
680
680
|
console.error(formatMcpCallError(tool, err));
|
|
681
681
|
process.exit(1);
|
|
682
682
|
}
|
|
@@ -215,13 +215,27 @@ function flowsSummary(view, now) {
|
|
|
215
215
|
const receivers = [];
|
|
216
216
|
for (const edge of view.edges) if (edge.to === view.center) senders.push(party(edge.from, edge));
|
|
217
217
|
else receivers.push(party(edge.to, edge));
|
|
218
|
-
const
|
|
218
|
+
const body = [
|
|
219
219
|
`Money flows of ${view.center} on ${view.network}: ${roleText(center)}; lifetime ${formatFlowsUsd(center?.total_in_usd ?? 0)} in, ${formatFlowsUsd(center?.total_out_usd ?? 0)} out; ${center?.degree_in ?? 0} senders and ${center?.degree_out ?? 0} receivers in the topology graph.`,
|
|
220
220
|
...sideLines("Recent senders", senders),
|
|
221
221
|
...sideLines("Recent receivers", receivers)
|
|
222
|
-
];
|
|
223
|
-
|
|
224
|
-
return
|
|
222
|
+
].slice(0, 18);
|
|
223
|
+
const footer = pagingLines(view, senders.length, receivers.length, center);
|
|
224
|
+
return [...body, ...footer].join("\n");
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Where this page sits and how to read the next one. Written for every reader:
|
|
228
|
+
* the model, a terminal (cia mcp call), and a person looking at the picture in
|
|
229
|
+
* Claude Desktop, who clicks an address instead.
|
|
230
|
+
*/
|
|
231
|
+
function pagingLines(view, shownSenders, shownReceivers, center) {
|
|
232
|
+
const totalIn = center?.degree_in ?? 0;
|
|
233
|
+
const totalOut = center?.degree_out ?? 0;
|
|
234
|
+
const { in_offset: nextIn, out_offset: nextOut } = view.cursor;
|
|
235
|
+
const range = (shown, next, total) => shown === 0 ? `none of ${total}` : `${next - shown + 1} to ${next} of ${total}`;
|
|
236
|
+
const where = `Showing senders ${range(shownSenders, nextIn, totalIn)} and receivers ${range(shownReceivers, nextOut, totalOut)}, newest first.`;
|
|
237
|
+
if (!view.truncated && nextIn >= totalIn && nextOut >= totalOut) return [where];
|
|
238
|
+
return [where, `Next page: money_flows with in_offset=${nextIn} out_offset=${nextOut} (terminal: cia mcp call money_flows network=${view.network} address=${view.center} in_offset=${nextIn} out_offset=${nextOut}); in Claude Desktop, click an address in the picture instead.`];
|
|
225
239
|
}
|
|
226
240
|
function errorResult(text) {
|
|
227
241
|
return {
|
|
@@ -317,11 +331,16 @@ async function runFlows(deps, network, address, inOffset, outOffset) {
|
|
|
317
331
|
isError: false
|
|
318
332
|
};
|
|
319
333
|
}
|
|
320
|
-
/** money_flows {address, network}:
|
|
334
|
+
/** money_flows {address, network, in_offset?, out_offset?}: one page of one address, the first by default. */
|
|
321
335
|
async function handleMoneyFlows(args, deps) {
|
|
322
|
-
const
|
|
336
|
+
const record = isRecord(args) ? args : {};
|
|
337
|
+
const checked = checkNetworkAndAddress(record);
|
|
323
338
|
if ("error" in checked) return errorResult(checked.error);
|
|
324
|
-
|
|
339
|
+
const inOffset = checkOffset(record.in_offset);
|
|
340
|
+
if (typeof inOffset !== "number") return errorResult(inOffset.error);
|
|
341
|
+
const outOffset = checkOffset(record.out_offset);
|
|
342
|
+
if (typeof outOffset !== "number") return errorResult(outOffset.error);
|
|
343
|
+
return runFlows(deps, checked.network, checked.address, inOffset, outOffset);
|
|
325
344
|
}
|
|
326
345
|
/** graph_expand {network, address, in_offset, out_offset}: the next page of one address. */
|
|
327
346
|
async function handleGraphExpand(args, deps) {
|
|
@@ -341,4 +360,4 @@ const MONEY_FLOWS_INSTRUCTION = "Use money_flows to see who sent money to one ro
|
|
|
341
360
|
//#endregion
|
|
342
361
|
export { MONEY_FLOWS_INSTRUCTION as a, handleGraphExpand as c, MONEY_FLOWS_DESCRIPTION as i, handleMoneyFlows as l, GRAPH_EXPAND_DESCRIPTION as n, MONEY_FLOWS_TOOL as o, GRAPH_EXPAND_TOOL as r, flows_exports as s, FLOWS_MAX_OFFSET as t };
|
|
343
362
|
|
|
344
|
-
//# sourceMappingURL=flows-
|
|
363
|
+
//# sourceMappingURL=flows-9XJmeF_M.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flows-9XJmeF_M.mjs","names":[],"sources":["../src/mcp/flows.ts"],"sourcesContent":["/**\n * The money-flow view tools of the local proxy: money_flows and graph_expand.\n *\n * money_flows answers one robinhood address with its most recent senders and\n * receivers: a short text the model reads, and chain-insights.flows.v1 in\n * structuredContent, which the Chain Insights view (ui://chain-insights/view)\n * draws. graph_expand is the same answer for the next page of one address. The\n * view calls it when the investigator clicks an address; its _meta says\n * visibility [\"app\"], so a host keeps it away from the model.\n *\n * Both tools are composed on the client PC from three anchored graph_query\n * reads (the node, its outgoing FLOWS_TO and its incoming FLOWS_TO, newest\n * first), sent through the proxy's remote client, the same path and payment\n * wrapping a graph_query call takes. Each read is billed as a graph_query.\n * Every argument is checked before the first read, so a refused call reaches\n * no graph endpoint and costs nothing.\n */\n\nexport const FLOWS_SCHEMA = 'chain-insights.flows.v1'\nexport const MONEY_FLOWS_TOOL = 'money_flows'\nexport const GRAPH_EXPAND_TOOL = 'graph_expand'\nexport const FLOWS_NETWORK = 'robinhood'\n\n/** How many senders and how many receivers one page shows. */\nexport const FLOWS_PER_SIDE = 12\n/** The view data of one answer: at most this many addresses... */\nexport const FLOWS_MAX_NODES = 60\n/** ...and under this many characters of JSON. */\nexport const FLOWS_MAX_CHARS = 40_000\n/** The text the model reads holds at most this many lines. */\nexport const FLOWS_SUMMARY_MAX_LINES = 20\n/** Where a graph_expand page may start on each side, at most. */\nexport const FLOWS_MAX_OFFSET = 10_000\n\nexport type FlowNode = {\n address: string\n role: string\n labels: string[]\n total_in_usd: number\n total_out_usd: number\n degree_in: number\n degree_out: number\n}\n\nexport type FlowEdge = {\n from: string\n to: string\n usd: number\n tx_count: number\n first_seen_ms: number\n last_seen_ms: number\n}\n\nexport type FlowCursor = { in_offset: number; out_offset: number }\n\nexport type FlowView = {\n schema: typeof FLOWS_SCHEMA\n network: string\n center: string\n nodes: FlowNode[]\n edges: FlowEdge[]\n cursor: FlowCursor\n truncated: boolean\n}\n\ntype Row = Record<string, unknown>\n\nexport type FlowsPage = {\n center: Row\n senders: Row[]\n receivers: Row[]\n inOffset: number\n outOffset: number\n}\n\n/** One graph_query answer as the remote client returns it. */\nexport type GraphQueryAnswer = {\n content?: Array<{ type: string; text?: string }>\n structuredContent?: Record<string, unknown>\n isError?: boolean\n}\n\nexport type FlowsToolResult = {\n content: Array<{ type: 'text'; text: string }>\n structuredContent?: Record<string, unknown>\n isError: boolean\n}\n\nexport type FlowsDependencies = {\n /** One graph_query call through the proxy's remote client. */\n graphQuery: (args: { network: string; query: string }) => Promise<GraphQueryAnswer>\n /** The text of a call that threw (payment required, transport failure). */\n describeFailure: (err: unknown) => string\n /** Why no read can run (the graph endpoint is not connected), checked after the arguments. */\n unavailable?: () => string | undefined\n now?: () => Date\n}\n\nexport const INVALID_ADDRESS_TEXT =\n 'invalid_address: a full 0x address of 40 hexadecimal characters is needed, for example 0x04911a118f11c75667e4d0dfb8e640af5a353550'\nexport const OFFSET_TOO_LARGE_TEXT = `invalid_offset: in_offset and out_offset must each be at most ${FLOWS_MAX_OFFSET}`\nexport const OFFSET_NEGATIVE_TEXT =\n 'invalid_offset: in_offset and out_offset must each be a whole number, 0 or more'\n\nconst ADDRESS_PATTERN = /^0x[0-9a-f]{40}$/\nconst CODE_PREFIX = /^([a-z][a-z0-9_]*):\\s*/\n\n/**\n * The address lower-cased, or null when it is not 0x and 40 hexadecimal\n * characters. Only an address that passes is ever written into a read, so the\n * reads can carry it as a literal.\n */\nexport function normalizeFlowsAddress(raw: unknown): string | null {\n if (typeof raw !== 'string') return null\n const address = raw.trim().toLowerCase()\n return ADDRESS_PATTERN.test(address) ? address : null\n}\n\nconst NODE_FIELDS =\n 'b.address AS address, b.labels AS labels, b.is_exchange AS is_exchange, ' +\n 'b.is_sanctioned AS is_sanctioned, b.is_scam AS is_scam, b.is_victim AS is_victim, b:Pool AS pool, ' +\n 'b.total_in_usd AS total_in_usd, b.total_out_usd AS total_out_usd, b.degree_in AS degree_in, b.degree_out AS degree_out'\nconst PARTY_FIELDS =\n NODE_FIELDS +\n ', f.amount_usd_sum AS usd, f.tx_count AS tx_count, ' +\n 'f.first_seen_timestamp AS first_seen, f.last_seen_timestamp AS last_seen'\n// Newest link first, the address breaking a tie, so a page and the next one\n// never share or skip a link.\nconst PAGE_ORDER = ' ORDER BY f.last_seen_timestamp DESC, b.address'\n\nexport type FlowsRead = { id: 'node' | 'out' | 'in'; query: string }\n\n/**\n * The three anchored reads of one page. Each side asks for one row more than\n * it shows: the extra row says another page exists.\n */\nexport function flowsReadsFor(address: string, inOffset: number, outOffset: number): FlowsRead[] {\n const page = (offset: number) =>\n `${PAGE_ORDER}${offset > 0 ? ` SKIP ${offset}` : ''} LIMIT ${FLOWS_PER_SIDE + 1}`\n return [\n {\n id: 'node',\n query: `USE topology MATCH (b:Address {address: '${address}'}) RETURN ${NODE_FIELDS} LIMIT 1`,\n },\n {\n id: 'out',\n query: `USE topology MATCH (a:Address {address: '${address}'})-[f:FLOWS_TO]->(b:Address) RETURN ${PARTY_FIELDS}${page(outOffset)}`,\n },\n {\n id: 'in',\n query: `USE topology MATCH (b:Address)-[f:FLOWS_TO]->(a:Address {address: '${address}'}) RETURN ${PARTY_FIELDS}${page(inOffset)}`,\n },\n ]\n}\n\n/** The role word of an address, first match wins. */\nexport function flowsRole(row: Row): string {\n if (row.is_exchange === true) return 'exchange'\n if (row.is_sanctioned === true) return 'sanctioned'\n if (row.is_scam === true) return 'scam'\n if (row.is_victim === true) return 'victim'\n if (row.pool === true) return 'pool'\n return 'other'\n}\n\nfunction flowsFloat(value: unknown): number {\n return typeof value === 'number' && Number.isFinite(value) ? value : 0\n}\n\nfunction flowsInt(value: unknown): number {\n return typeof value === 'number' && Number.isFinite(value) ? Math.trunc(value) : 0\n}\n\nfunction flowsNodeOf(row: Row): FlowNode {\n const labels = Array.isArray(row.labels)\n ? row.labels.filter((label): label is string => typeof label === 'string')\n : []\n return {\n address: typeof row.address === 'string' ? row.address : '',\n role: flowsRole(row),\n labels,\n total_in_usd: flowsFloat(row.total_in_usd),\n total_out_usd: flowsFloat(row.total_out_usd),\n degree_in: flowsInt(row.degree_in),\n degree_out: flowsInt(row.degree_out),\n }\n}\n\nfunction assembleFlowView(\n network: string,\n page: FlowsPage,\n senders: Row[],\n receivers: Row[],\n truncated: boolean\n): FlowView {\n const center = flowsNodeOf(page.center)\n const view: FlowView = {\n schema: FLOWS_SCHEMA,\n network,\n center: center.address,\n nodes: [center],\n edges: [],\n cursor: {\n in_offset: page.inOffset + senders.length,\n out_offset: page.outOffset + receivers.length,\n },\n truncated,\n }\n const seen = new Set([center.address])\n const add = (row: Row, incoming: boolean) => {\n const node = flowsNodeOf(row)\n if (!seen.has(node.address)) {\n seen.add(node.address)\n view.nodes.push(node)\n }\n view.edges.push({\n from: incoming ? node.address : center.address,\n to: incoming ? center.address : node.address,\n usd: flowsFloat(row.usd),\n tx_count: flowsInt(row.tx_count),\n first_seen_ms: flowsInt(row.first_seen),\n last_seen_ms: flowsInt(row.last_seen),\n })\n }\n for (const row of senders) add(row, true)\n for (const row of receivers) add(row, false)\n return view\n}\n\nfunction fitsFlowBudget(view: FlowView): boolean {\n return view.nodes.length <= FLOWS_MAX_NODES && JSON.stringify(view).length < FLOWS_MAX_CHARS\n}\n\n/**\n * flows.v1 from one page: the centre first, then the senders and the\n * receivers, each newest first, at most FLOWS_PER_SIDE a side. When the view\n * data would hold more than FLOWS_MAX_NODES addresses or reach FLOWS_MAX_CHARS\n * characters, the oldest links go first, the view is marked truncated, and the\n * cursor still says where the next page starts.\n */\nexport function buildFlowView(network: string, page: FlowsPage): FlowView {\n let truncated = page.senders.length > FLOWS_PER_SIDE || page.receivers.length > FLOWS_PER_SIDE\n let senders = page.senders.slice(0, FLOWS_PER_SIDE)\n let receivers = page.receivers.slice(0, FLOWS_PER_SIDE)\n for (;;) {\n const view = assembleFlowView(network, page, senders, receivers, truncated)\n if (fitsFlowBudget(view) || senders.length + receivers.length === 0) return view\n truncated = true\n if (senders.length >= receivers.length) senders = senders.slice(0, -1)\n else receivers = receivers.slice(0, -1)\n }\n}\n\nexport function formatFlowsUsd(value: number): string {\n if (!(value > 0)) return '$0'\n if (value < 0.01) return 'under $0.01'\n const whole = Math.floor(value)\n const cents = Math.round((value - whole) * 100)\n if (cents === 100) return formatFlowsUsd(whole + 1)\n const grouped = whole.toFixed(0).replace(/\\B(?=(\\d{3})+(?!\\d))/g, ',')\n return `$${grouped}.${String(cents).padStart(2, '0')}`\n}\n\nfunction formatFlowsAge(lastSeenMs: number, now: Date): string {\n if (lastSeenMs <= 0) return 'at an unknown time'\n const date = new Date(lastSeenMs).toISOString().slice(0, 10)\n const days = Math.trunc((now.getTime() - lastSeenMs) / 86_400_000)\n if (days < 1) return `${date} (less than a day ago)`\n if (days === 1) return `${date} (1 day ago)`\n return `${date} (${days} days ago)`\n}\n\n/** The role word, with up to three address labels beside it. */\nfunction roleText(node: FlowNode | undefined): string {\n if (!node) return 'other'\n if (node.labels.length === 0) return node.role\n return `${node.role} (${node.labels.slice(0, 3).join(', ')})`\n}\n\nfunction sideLines(title: string, parties: string[]): string[] {\n if (parties.length === 0) return [`${title}, newest first: none.`]\n const lines = [`${title}, newest first (${parties.length}):`]\n // A side with more than six counterparties lists two on a line, so a full\n // page stays within the line budget.\n const perLine = parties.length > 6 ? 2 : 1\n for (let start = 0; start < parties.length; start += perLine) {\n lines.push(`- ${parties.slice(start, start + perLine).join('; ')}`)\n }\n return lines\n}\n\n/**\n * The text the model reads: the centre, its role and lifetime totals, then\n * every counterparty of the view with its USD, transaction count and last-seen\n * date and age. At most FLOWS_SUMMARY_MAX_LINES lines.\n */\nexport function flowsSummary(view: FlowView, now: Date): string {\n const nodes = new Map(view.nodes.map((node) => [node.address, node]))\n const center = nodes.get(view.center)\n const party = (address: string, edge: FlowEdge) =>\n `${address} ${roleText(nodes.get(address))}, ${formatFlowsUsd(edge.usd)} in ${edge.tx_count} tx, last seen ${formatFlowsAge(edge.last_seen_ms, now)}`\n const senders: string[] = []\n const receivers: string[] = []\n for (const edge of view.edges) {\n if (edge.to === view.center) senders.push(party(edge.from, edge))\n else receivers.push(party(edge.to, edge))\n }\n const lines = [\n `Money flows of ${view.center} on ${view.network}: ${roleText(center)}; lifetime ${formatFlowsUsd(center?.total_in_usd ?? 0)} in, ${formatFlowsUsd(center?.total_out_usd ?? 0)} out; ${center?.degree_in ?? 0} senders and ${center?.degree_out ?? 0} receivers in the topology graph.`,\n ...sideLines('Recent senders', senders),\n ...sideLines('Recent receivers', receivers),\n ]\n const body = lines.slice(0, FLOWS_SUMMARY_MAX_LINES - 2)\n const footer = pagingLines(view, senders.length, receivers.length, center)\n return [...body, ...footer].join('\\n')\n}\n\n/**\n * Where this page sits and how to read the next one. Written for every reader:\n * the model, a terminal (cia mcp call), and a person looking at the picture in\n * Claude Desktop, who clicks an address instead.\n */\nfunction pagingLines(\n view: FlowView,\n shownSenders: number,\n shownReceivers: number,\n center: FlowNode | undefined\n): string[] {\n const totalIn = center?.degree_in ?? 0\n const totalOut = center?.degree_out ?? 0\n const { in_offset: nextIn, out_offset: nextOut } = view.cursor\n const range = (shown: number, next: number, total: number) =>\n shown === 0 ? `none of ${total}` : `${next - shown + 1} to ${next} of ${total}`\n const where = `Showing senders ${range(shownSenders, nextIn, totalIn)} and receivers ${range(shownReceivers, nextOut, totalOut)}, newest first.`\n if (!view.truncated && nextIn >= totalIn && nextOut >= totalOut) return [where]\n return [\n where,\n `Next page: money_flows with in_offset=${nextIn} out_offset=${nextOut} (terminal: cia mcp call money_flows network=${view.network} address=${view.center} in_offset=${nextIn} out_offset=${nextOut}); in Claude Desktop, click an address in the picture instead.`,\n ]\n}\n\nfunction errorResult(text: string): FlowsToolResult {\n return { content: [{ type: 'text', text }], isError: true }\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return !!value && typeof value === 'object' && !Array.isArray(value)\n}\n\nfunction answerText(answer: GraphQueryAnswer): string {\n return (answer.content ?? [])\n .map((block) => (block.type === 'text' && typeof block.text === 'string' ? block.text : ''))\n .filter(Boolean)\n .join('\\n')\n .trim()\n}\n\n/**\n * The refusal of a read that did not answer, naming its code first:\n * error_detail.code when the graph endpoint sent its typed refusal, the code\n * its text starts with otherwise, and graph_query_failed when it names none.\n * The view marks the address it was expanding.\n */\nexport function refusalText(answer: GraphQueryAnswer): string {\n const text = answerText(answer)\n const detail = isRecord(answer.structuredContent)\n ? answer.structuredContent.error_detail\n : undefined\n const detailCode = isRecord(detail) && typeof detail.code === 'string' ? detail.code : ''\n const textCode = CODE_PREFIX.exec(text)?.[1] ?? ''\n const code = detailCode || textCode || 'graph_query_failed'\n const rest = textCode === code ? text.replace(CODE_PREFIX, '') : text\n return rest ? `${code}: ${rest}` : `${code}: the graph endpoint refused the read`\n}\n\nfunction rowsOf(answer: GraphQueryAnswer): Row[] | null {\n const facts = isRecord(answer.structuredContent) ? answer.structuredContent.facts : undefined\n const query = isRecord(facts) ? facts.query : undefined\n const results = isRecord(query) ? query.results : undefined\n return Array.isArray(results) ? results.filter(isRecord) : null\n}\n\ntype Checked = { network: string; address: string } | { error: string }\n\nfunction checkNetworkAndAddress(args: Record<string, unknown>): Checked {\n const rawNetwork = typeof args.network === 'string' ? args.network.trim().toLowerCase() : ''\n if (!rawNetwork) return { error: 'invalid_network: network is required; pass robinhood' }\n if (rawNetwork !== FLOWS_NETWORK) {\n return {\n error: `invalid_network: money flows read the robinhood topology graph only; got \"${String(args.network).trim()}\"`,\n }\n }\n const address = normalizeFlowsAddress(args.address)\n if (!address) return { error: INVALID_ADDRESS_TEXT }\n return { network: FLOWS_NETWORK, address }\n}\n\nfunction checkOffset(value: unknown): number | { error: string } {\n if (value === undefined || value === null) return 0\n if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) {\n return { error: OFFSET_NEGATIVE_TEXT }\n }\n if (value > FLOWS_MAX_OFFSET) return { error: OFFSET_TOO_LARGE_TEXT }\n return value\n}\n\n/**\n * Read and answer one page. The reads run one after another: the node first,\n * so an address the graph does not hold costs one read, not three; then the\n * receivers and the senders. The first read that does not answer ends the call.\n */\nasync function runFlows(\n deps: FlowsDependencies,\n network: string,\n address: string,\n inOffset: number,\n outOffset: number\n): Promise<FlowsToolResult> {\n const unavailable = deps.unavailable?.()\n if (unavailable) return errorResult(unavailable)\n const rows: Partial<Record<FlowsRead['id'], Row[]>> = {}\n for (const read of flowsReadsFor(address, inOffset, outOffset)) {\n let answer: GraphQueryAnswer\n try {\n answer = await deps.graphQuery({ network, query: read.query })\n } catch (err) {\n return errorResult(deps.describeFailure(err))\n }\n if (answer.isError === true) return errorResult(refusalText(answer))\n const found = rowsOf(answer)\n if (!found) {\n return errorResult(\n 'graph_query_failed: the graph endpoint answered without chain-insights.result.v1 rows'\n )\n }\n rows[read.id] = found\n if (read.id === 'node' && found.length === 0) {\n return errorResult(`${address} is not in the ${network} topology graph`)\n }\n }\n const view = buildFlowView(network, {\n center: rows.node?.[0] ?? {},\n senders: rows.in ?? [],\n receivers: rows.out ?? [],\n inOffset,\n outOffset,\n })\n const now = deps.now ? deps.now() : new Date()\n return {\n content: [{ type: 'text', text: flowsSummary(view, now) }],\n structuredContent: view as unknown as Record<string, unknown>,\n isError: false,\n }\n}\n\n/** money_flows {address, network, in_offset?, out_offset?}: one page of one address, the first by default. */\nexport async function handleMoneyFlows(\n args: unknown,\n deps: FlowsDependencies\n): Promise<FlowsToolResult> {\n const record = isRecord(args) ? args : {}\n const checked = checkNetworkAndAddress(record)\n if ('error' in checked) return errorResult(checked.error)\n const inOffset = checkOffset(record.in_offset)\n if (typeof inOffset !== 'number') return errorResult(inOffset.error)\n const outOffset = checkOffset(record.out_offset)\n if (typeof outOffset !== 'number') return errorResult(outOffset.error)\n return runFlows(deps, checked.network, checked.address, inOffset, outOffset)\n}\n\n/** graph_expand {network, address, in_offset, out_offset}: the next page of one address. */\nexport async function handleGraphExpand(\n args: unknown,\n deps: FlowsDependencies\n): Promise<FlowsToolResult> {\n const record = isRecord(args) ? args : {}\n const checked = checkNetworkAndAddress(record)\n if ('error' in checked) return errorResult(checked.error)\n const inOffset = checkOffset(record.in_offset)\n if (typeof inOffset !== 'number') return errorResult(inOffset.error)\n const outOffset = checkOffset(record.out_offset)\n if (typeof outOffset !== 'number') return errorResult(outOffset.error)\n return runFlows(deps, checked.network, checked.address, inOffset, outOffset)\n}\n\nexport const MONEY_FLOWS_DESCRIPTION =\n 'Show the most recent money flows around one robinhood address, read from the Chain Insights topology graph: ' +\n 'the address with its role and lifetime USD totals, its 12 most recent senders and its 12 most recent receivers, ' +\n 'each link with its lifetime USD, transaction count and last-seen time. The answer is a text summary, and an ' +\n 'interactive graph in hosts that draw MCP apps. An address not in the graph is an error. Read-only; billed as ' +\n 'three graph_query topology reads, or one when the address is not in the graph.'\n\nexport const GRAPH_EXPAND_DESCRIPTION =\n 'Load the next page of senders and receivers of one address into an open money-flow view. ' +\n 'Called by the view when the investigator clicks an address. Read-only; billed as three graph_query ' +\n 'topology reads, nothing when it is refused before a read.'\n\n/** The line of the server instructions that tells a model what money_flows answers. */\nexport const MONEY_FLOWS_INSTRUCTION =\n 'Use money_flows to see who sent money to one robinhood address and where it went: its 12 most recent senders and receivers, each with lifetime USD, transaction count and last-seen time.'\n"],"mappings":"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"}
|
|
@@ -214,13 +214,27 @@ function flowsSummary(view, now) {
|
|
|
214
214
|
const receivers = [];
|
|
215
215
|
for (const edge of view.edges) if (edge.to === view.center) senders.push(party(edge.from, edge));
|
|
216
216
|
else receivers.push(party(edge.to, edge));
|
|
217
|
-
const
|
|
217
|
+
const body = [
|
|
218
218
|
`Money flows of ${view.center} on ${view.network}: ${roleText(center)}; lifetime ${formatFlowsUsd(center?.total_in_usd ?? 0)} in, ${formatFlowsUsd(center?.total_out_usd ?? 0)} out; ${center?.degree_in ?? 0} senders and ${center?.degree_out ?? 0} receivers in the topology graph.`,
|
|
219
219
|
...sideLines("Recent senders", senders),
|
|
220
220
|
...sideLines("Recent receivers", receivers)
|
|
221
|
-
];
|
|
222
|
-
|
|
223
|
-
return
|
|
221
|
+
].slice(0, 18);
|
|
222
|
+
const footer = pagingLines(view, senders.length, receivers.length, center);
|
|
223
|
+
return [...body, ...footer].join("\n");
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Where this page sits and how to read the next one. Written for every reader:
|
|
227
|
+
* the model, a terminal (cia mcp call), and a person looking at the picture in
|
|
228
|
+
* Claude Desktop, who clicks an address instead.
|
|
229
|
+
*/
|
|
230
|
+
function pagingLines(view, shownSenders, shownReceivers, center) {
|
|
231
|
+
const totalIn = center?.degree_in ?? 0;
|
|
232
|
+
const totalOut = center?.degree_out ?? 0;
|
|
233
|
+
const { in_offset: nextIn, out_offset: nextOut } = view.cursor;
|
|
234
|
+
const range = (shown, next, total) => shown === 0 ? `none of ${total}` : `${next - shown + 1} to ${next} of ${total}`;
|
|
235
|
+
const where = `Showing senders ${range(shownSenders, nextIn, totalIn)} and receivers ${range(shownReceivers, nextOut, totalOut)}, newest first.`;
|
|
236
|
+
if (!view.truncated && nextIn >= totalIn && nextOut >= totalOut) return [where];
|
|
237
|
+
return [where, `Next page: money_flows with in_offset=${nextIn} out_offset=${nextOut} (terminal: cia mcp call money_flows network=${view.network} address=${view.center} in_offset=${nextIn} out_offset=${nextOut}); in Claude Desktop, click an address in the picture instead.`];
|
|
224
238
|
}
|
|
225
239
|
function errorResult(text) {
|
|
226
240
|
return {
|
|
@@ -316,11 +330,16 @@ async function runFlows(deps, network, address, inOffset, outOffset) {
|
|
|
316
330
|
isError: false
|
|
317
331
|
};
|
|
318
332
|
}
|
|
319
|
-
/** money_flows {address, network}:
|
|
333
|
+
/** money_flows {address, network, in_offset?, out_offset?}: one page of one address, the first by default. */
|
|
320
334
|
async function handleMoneyFlows(args, deps) {
|
|
321
|
-
const
|
|
335
|
+
const record = isRecord(args) ? args : {};
|
|
336
|
+
const checked = checkNetworkAndAddress(record);
|
|
322
337
|
if ("error" in checked) return errorResult(checked.error);
|
|
323
|
-
|
|
338
|
+
const inOffset = checkOffset(record.in_offset);
|
|
339
|
+
if (typeof inOffset !== "number") return errorResult(inOffset.error);
|
|
340
|
+
const outOffset = checkOffset(record.out_offset);
|
|
341
|
+
if (typeof outOffset !== "number") return errorResult(outOffset.error);
|
|
342
|
+
return runFlows(deps, checked.network, checked.address, inOffset, outOffset);
|
|
324
343
|
}
|
|
325
344
|
/** graph_expand {network, address, in_offset, out_offset}: the next page of one address. */
|
|
326
345
|
async function handleGraphExpand(args, deps) {
|
package/dist/mcp-proxy.cjs
CHANGED
|
@@ -2,12 +2,12 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
|
2
2
|
const require_rolldown_runtime = require("./rolldown-runtime-C0BPl7ul.cjs");
|
|
3
3
|
const require_print_result = require("./print-result-BEA_PyNz.cjs");
|
|
4
4
|
const require_client = require("./client-Ypw9BNbn.cjs");
|
|
5
|
-
const require_tool_visibility = require("./tool-visibility-
|
|
5
|
+
const require_tool_visibility = require("./tool-visibility-CfMxSzf8.cjs");
|
|
6
6
|
const require_usage_status = require("./usage-status-BdwwgYsu.cjs");
|
|
7
7
|
const require_subscription_status = require("./subscription-status-Cm7LqY_h.cjs");
|
|
8
8
|
const require_capabilities = require("./capabilities-A8fsfP24.cjs");
|
|
9
9
|
const require_action_log = require("./action-log-HniMMF7f.cjs");
|
|
10
|
-
const require_flows = require("./flows-
|
|
10
|
+
const require_flows = require("./flows-CJSNSXkH.cjs");
|
|
11
11
|
let node_url = require("node:url");
|
|
12
12
|
let node_path = require("node:path");
|
|
13
13
|
node_path = require_rolldown_runtime.__toESM(node_path, 1);
|
|
@@ -945,7 +945,9 @@ async function createProxy() {
|
|
|
945
945
|
description: require_flows.MONEY_FLOWS_DESCRIPTION,
|
|
946
946
|
inputSchema: {
|
|
947
947
|
address: zod.string().describe("One robinhood address: 0x and 40 hexadecimal characters."),
|
|
948
|
-
network: zod.string().describe("The network. Only robinhood is served.")
|
|
948
|
+
network: zod.string().describe("The network. Only robinhood is served."),
|
|
949
|
+
in_offset: zod.number().int().min(0).max(require_flows.FLOWS_MAX_OFFSET).optional().describe("Senders to skip, for the next page: the in_offset the previous answer names."),
|
|
950
|
+
out_offset: zod.number().int().min(0).max(require_flows.FLOWS_MAX_OFFSET).optional().describe("Receivers to skip, for the next page: the out_offset the previous answer names.")
|
|
949
951
|
},
|
|
950
952
|
annotations: VIEW_TOOL_ANNOTATIONS,
|
|
951
953
|
_meta: VIEW_TOOL_META
|
package/dist/mcp-proxy.mjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { a as PACKAGE_VERSION } from "./print-result-k9kXVR2c.mjs";
|
|
2
2
|
import { t as PaymentRequiredError } from "./client-gsLi528K.mjs";
|
|
3
|
-
import { i as isAppOnlyTool, n as PUBLIC_MCP_TOOL_ALLOWED_ARGS, r as PUBLIC_MCP_TOOL_REQUIRED_ARGS, t as HIDDEN_REMOTE_TOOL_NAMES } from "./tool-visibility-
|
|
3
|
+
import { i as isAppOnlyTool, n as PUBLIC_MCP_TOOL_ALLOWED_ARGS, r as PUBLIC_MCP_TOOL_REQUIRED_ARGS, t as HIDDEN_REMOTE_TOOL_NAMES } from "./tool-visibility-BdncNrVB.mjs";
|
|
4
4
|
import { t as primitiveBackendUsageStatus } from "./usage-status-CXOnrq-s.mjs";
|
|
5
5
|
import { n as unavailableSubscriptionStatus } from "./subscription-status-CJ-DaMW9.mjs";
|
|
6
6
|
import { n as mirrorGraphNetworkCapabilities } from "./capabilities-DfWL6n_g.mjs";
|
|
7
7
|
import { r as appendActionLog, t as actionLogSignalsFromResult } from "./action-log-D49Par9P.mjs";
|
|
8
|
-
import { a as MONEY_FLOWS_INSTRUCTION, c as handleGraphExpand, i as MONEY_FLOWS_DESCRIPTION, l as handleMoneyFlows, n as GRAPH_EXPAND_DESCRIPTION, o as MONEY_FLOWS_TOOL, r as GRAPH_EXPAND_TOOL, t as FLOWS_MAX_OFFSET } from "./flows-
|
|
8
|
+
import { a as MONEY_FLOWS_INSTRUCTION, c as handleGraphExpand, i as MONEY_FLOWS_DESCRIPTION, l as handleMoneyFlows, n as GRAPH_EXPAND_DESCRIPTION, o as MONEY_FLOWS_TOOL, r as GRAPH_EXPAND_TOOL, t as FLOWS_MAX_OFFSET } from "./flows-9XJmeF_M.mjs";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
10
|
import path from "node:path";
|
|
11
11
|
import { readFileSync } from "node:fs";
|
|
@@ -941,7 +941,9 @@ async function createProxy() {
|
|
|
941
941
|
description: MONEY_FLOWS_DESCRIPTION,
|
|
942
942
|
inputSchema: {
|
|
943
943
|
address: z.string().describe("One robinhood address: 0x and 40 hexadecimal characters."),
|
|
944
|
-
network: z.string().describe("The network. Only robinhood is served.")
|
|
944
|
+
network: z.string().describe("The network. Only robinhood is served."),
|
|
945
|
+
in_offset: z.number().int().min(0).max(FLOWS_MAX_OFFSET).optional().describe("Senders to skip, for the next page: the in_offset the previous answer names."),
|
|
946
|
+
out_offset: z.number().int().min(0).max(FLOWS_MAX_OFFSET).optional().describe("Receivers to skip, for the next page: the out_offset the previous answer names.")
|
|
945
947
|
},
|
|
946
948
|
annotations: VIEW_TOOL_ANNOTATIONS,
|
|
947
949
|
_meta: VIEW_TOOL_META
|
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 is the graph server's own: topology searches, while facts and chain\n// look up one known thing. Nothing here is a limit. A limit that the server\n// publishes is read from `meta_network_capabilities`, and the text names the\n// field, never the number.\n//\n// `USE chain` serves the three labels of the server's chain catalogue and no\n// other: a transaction by its hash, a block by its number or its hash, and the\n// head, which takes no key. There is no address lookup on the chain layer, so a\n// routing line that offered one sent an agent to a refusal.\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.\n\n/** The routing rule, in one sentence. The server states it in the same words. */\nexport const ROUTING_RULE =\n 'Route by what you know: topology searches, 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 do not know the thing yet: `USE topology`.',\n 'I know the pair and the day, or the transaction hash, and want the indexed rows: `USE facts`.',\n \"I want the chain's own record of a transaction by its hash, a block by its number or hash, or the head: `USE chain`.\",\n] as const\n\n/** The chain layer has no address lookup, so an address starts on topology. */\nexport const ROUTING_NO_ADDRESS_ON_CHAIN =\n '`USE chain` has no address lookup. Find the address on `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 'When two fit, as with a hash: ask `USE chain` first for the record and the result, then `USE facts` for the transfers it caused.'\n\n/** The key that each layer hands to the next. */\nexport const ROUTING_HANDOFF =\n 'The layers hand each other keys. Topology gives the pair and the first and last seen time. Chain gives the `block_date` of a hash or a height. Facts gives the `tx_id`.'\n\n/** A question that no layer serves. */\nexport const ROUTING_NOT_SERVED =\n 'A list, a range or a whole-chain question is served on no layer. Say so, and go back to an anchored `USE topology` search.'\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_NO_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 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_NO_ADDRESS_ON_CHAIN} ${ROUTING_TWO_FIT} ${ROUTING_HANDOFF} ${ROUTING_NOT_SERVED} 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 { routingHintLines } from './layer-routing.js'\nimport { actionLogSignalsFromResult, appendActionLog } from './action-log.js'\nimport {\n FLOWS_MAX_OFFSET,\n GRAPH_EXPAND_DESCRIPTION,\n GRAPH_EXPAND_TOOL,\n MONEY_FLOWS_DESCRIPTION,\n MONEY_FLOWS_INSTRUCTION,\n MONEY_FLOWS_TOOL,\n handleGraphExpand,\n handleMoneyFlows,\n type FlowsDependencies,\n type GraphQueryAnswer,\n} from './flows.js'\n\nconst LOCAL_TOOL_NAMES = new Set([\n 'meta_network_capabilities',\n 'meta_usage_status',\n 'meta_subscription_status',\n 'meta_help',\n 'wallet_balance',\n 'wallet_topup',\n MONEY_FLOWS_TOOL,\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: the money-flow graph (money_flows, graph_expand), the query\n// result table (graph_query) or the balance (meta_usage_status,\n// meta_subscription_status). The metadata below is the wire contract of the\n// 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, plus the node risk_score/risk_level verdict) 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 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_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 aml_address_risk:\n 'Screen one blockchain address for AML risk, behavior patterns, neighborhood context, exchange exposure, and optional comparison with another address. Topology reads cover full lifetime history in one unified graph. Omit version to use the latest contract, or pass version=v1 to pin the v1 contract.',\n graph_query: `Run a read-only GQL/Cypher query through the Chain Insights graph endpoint. ${GRAPH_LAYERS_TEXT} 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\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 aml_address_risk for single-address enrichment. Use graph_query(_batch) for graph-level questions that aml_address_risk does not answer.',\n `3. ${MONEY_FLOWS_INSTRUCTION}`,\n '4. 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. The network value on Address nodes matches the tool argument. There is no separate identity key.',\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 also carry a risk verdict (risk_score float, risk_level string) plus 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 '- The risk verdict lives on topology nodes (risk_score float, risk_level string). An absent risk_score means UNSCORED: the model gave the address no verdict, which is no signal and never low risk. Labels and per-label risk also 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. 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, or one tx_id, 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\", or WHERE t.tx_id = \"...\" on its own (on EVM networks tx_id is the 0x transaction hash). 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, or one tx_id. 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 '- 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.risk_score AS risk_score, n.risk_level AS risk_level 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, plus the node risk_score/risk_level verdict) 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 hash, a block by number or hash, or the head, and no address. 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\nconst SERVER_INSTRUCTIONS = [\n 'Chain Insights is an AML and graph-analysis MCP server for AI agents.',\n CHAIN_INSIGHTS_WORKFLOW,\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 aml_address_risk, graph_query, or graph_query_batch as needed.',\n MONEY_FLOWS_INSTRUCTION,\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 'aml_address_risk':\n return {\n address: z.string().min(1).describe('Blockchain address to screen.'),\n network: NETWORK_SCHEMA,\n compare_address: z\n .string()\n .optional()\n .describe('Optional address to compare against the screened address.'),\n version: z\n .string()\n .optional()\n .describe('Optional AML tool contract version. Omit to use the latest version.'),\n }\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 'aml-address-risk',\n {\n title: 'AML Address Risk',\n description:\n 'Screen a blockchain address for AML risk, behavioral patterns, neighborhood profile, member addresses, and exchange links.',\n argsSchema: {\n network: NETWORK_SCHEMA,\n address: z.string().describe('Blockchain address to screen'),\n compare_address: z\n .string()\n .optional()\n .describe('Optional address to compare against the screened address'),\n version: z\n .string()\n .optional()\n .describe('Optional AML tool contract version. Omit to use the latest version'),\n },\n },\n async ({ network, address, compare_address, version }) =>\n promptResult(\n [\n `Use Chain Insights aml_address_risk${version ? ` version ${version}` : ''} on ${network} for:`,\n '',\n `\\`${address}\\``,\n compare_address ? `\\nCompare with: \\`${compare_address}\\`` : '',\n '',\n 'Present the summary as-is. Do not add analysis, verdicts, or risk assessments; the tool output already contains the risk assessment.',\n ]\n .filter(Boolean)\n .join('\\n'),\n 'AML address risk screening'\n )\n )\n\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-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, money_flows, graph_expand) keeps its structuredContent\n// exactly as the hosted server 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 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 if (!remoteToolNames.has('aml_address_risk')) {\n server.registerTool(\n 'aml_address_risk',\n {\n title: 'Address Risk',\n description: KNOWN_PUBLIC_TOOL_DESCRIPTIONS.aml_address_risk,\n inputSchema: {\n address: z.string().min(1).describe('Blockchain address to screen'),\n network: NETWORK_SCHEMA,\n compare_address: z\n .string()\n .optional()\n .describe('Optional address to compare against the screened address'),\n version: z\n .string()\n .optional()\n .describe('Optional AML tool contract version. Omit to use the latest version'),\n },\n annotations: {\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: true,\n },\n },\n async ({ address, network, compare_address, version }) => {\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 { runAmlAddressRisk } = await import('../investigation/public-tools.js')\n const result = await runAmlAddressRisk(\n remoteClient,\n {\n address,\n network,\n compareAddress: compare_address,\n writeArtifacts: workspaceArtifactsEnabled,\n },\n version\n )\n return {\n content: [{ type: 'text' as const, text: result.summaryText }],\n structuredContent: result.structuredContent,\n isError: false,\n }\n } catch (err) {\n if (err instanceof PaymentRequiredError) {\n return { content: [{ type: 'text' as const, text: err.message }], isError: true }\n }\n return {\n content: [\n { type: 'text' as const, text: `Address risk failed: ${(err as Error).message}` },\n ],\n isError: true,\n }\n }\n }\n )\n }\n\n // money_flows and graph_expand: composed here from anchored graph_query\n // reads sent through the remote client, the path and payment wrapping a\n // graph_query call takes, so each read is billed 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 MONEY_FLOWS_TOOL,\n {\n title: 'Money flows',\n description: MONEY_FLOWS_DESCRIPTION,\n inputSchema: {\n address: z.string().describe('One robinhood address: 0x and 40 hexadecimal characters.'),\n network: z.string().describe('The network. Only robinhood is served.'),\n },\n annotations: VIEW_TOOL_ANNOTATIONS,\n _meta: VIEW_TOOL_META,\n },\n async (args) => handleMoneyFlows(args, flowsDependencies)\n )\n server.registerTool(\n GRAPH_EXPAND_TOOL,\n {\n title: 'Expand a money-flow node',\n description: GRAPH_EXPAND_DESCRIPTION,\n inputSchema: {\n network: z.string(),\n address: z.string(),\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 },\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_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 '- aml_address_risk: screen one blockchain address; optionally compare it with another address.',\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 '- money_flows: show the most recent senders and receivers of one robinhood address, with an interactive graph in hosts that draw MCP apps.',\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_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 '- aml_address_risk: screen one blockchain address; optionally compare it with another address.',\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 '- money_flows: show the most recent senders and receivers of one robinhood address, with an interactive graph in hosts that draw MCP apps.',\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/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 is the graph server's own: topology searches, while facts and chain\n// look up one known thing. Nothing here is a limit. A limit that the server\n// publishes is read from `meta_network_capabilities`, and the text names the\n// field, never the number.\n//\n// `USE chain` serves the three labels of the server's chain catalogue and no\n// other: a transaction by its hash, a block by its number or its hash, and the\n// head, which takes no key. There is no address lookup on the chain layer, so a\n// routing line that offered one sent an agent to a refusal.\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.\n\n/** The routing rule, in one sentence. The server states it in the same words. */\nexport const ROUTING_RULE =\n 'Route by what you know: topology searches, 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 do not know the thing yet: `USE topology`.',\n 'I know the pair and the day, or the transaction hash, and want the indexed rows: `USE facts`.',\n \"I want the chain's own record of a transaction by its hash, a block by its number or hash, or the head: `USE chain`.\",\n] as const\n\n/** The chain layer has no address lookup, so an address starts on topology. */\nexport const ROUTING_NO_ADDRESS_ON_CHAIN =\n '`USE chain` has no address lookup. Find the address on `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 'When two fit, as with a hash: ask `USE chain` first for the record and the result, then `USE facts` for the transfers it caused.'\n\n/** The key that each layer hands to the next. */\nexport const ROUTING_HANDOFF =\n 'The layers hand each other keys. Topology gives the pair and the first and last seen time. Chain gives the `block_date` of a hash or a height. Facts gives the `tx_id`.'\n\n/** A question that no layer serves. */\nexport const ROUTING_NOT_SERVED =\n 'A list, a range or a whole-chain question is served on no layer. Say so, and go back to an anchored `USE topology` search.'\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_NO_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 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_NO_ADDRESS_ON_CHAIN} ${ROUTING_TWO_FIT} ${ROUTING_HANDOFF} ${ROUTING_NOT_SERVED} 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 { routingHintLines } from './layer-routing.js'\nimport { actionLogSignalsFromResult, appendActionLog } from './action-log.js'\nimport {\n FLOWS_MAX_OFFSET,\n GRAPH_EXPAND_DESCRIPTION,\n GRAPH_EXPAND_TOOL,\n MONEY_FLOWS_DESCRIPTION,\n MONEY_FLOWS_INSTRUCTION,\n MONEY_FLOWS_TOOL,\n handleGraphExpand,\n handleMoneyFlows,\n type FlowsDependencies,\n type GraphQueryAnswer,\n} from './flows.js'\n\nconst LOCAL_TOOL_NAMES = new Set([\n 'meta_network_capabilities',\n 'meta_usage_status',\n 'meta_subscription_status',\n 'meta_help',\n 'wallet_balance',\n 'wallet_topup',\n MONEY_FLOWS_TOOL,\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: the money-flow graph (money_flows, graph_expand), the query\n// result table (graph_query) or the balance (meta_usage_status,\n// meta_subscription_status). The metadata below is the wire contract of the\n// 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, plus the node risk_score/risk_level verdict) 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 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_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 aml_address_risk:\n 'Screen one blockchain address for AML risk, behavior patterns, neighborhood context, exchange exposure, and optional comparison with another address. Topology reads cover full lifetime history in one unified graph. Omit version to use the latest contract, or pass version=v1 to pin the v1 contract.',\n graph_query: `Run a read-only GQL/Cypher query through the Chain Insights graph endpoint. ${GRAPH_LAYERS_TEXT} 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\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 aml_address_risk for single-address enrichment. Use graph_query(_batch) for graph-level questions that aml_address_risk does not answer.',\n `3. ${MONEY_FLOWS_INSTRUCTION}`,\n '4. 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. The network value on Address nodes matches the tool argument. There is no separate identity key.',\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 also carry a risk verdict (risk_score float, risk_level string) plus 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 '- The risk verdict lives on topology nodes (risk_score float, risk_level string). An absent risk_score means UNSCORED: the model gave the address no verdict, which is no signal and never low risk. Labels and per-label risk also 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. 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, or one tx_id, 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\", or WHERE t.tx_id = \"...\" on its own (on EVM networks tx_id is the 0x transaction hash). 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, or one tx_id. 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 '- 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.risk_score AS risk_score, n.risk_level AS risk_level 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, plus the node risk_score/risk_level verdict) 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 hash, a block by number or hash, or the head, and no address. 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\nconst SERVER_INSTRUCTIONS = [\n 'Chain Insights is an AML and graph-analysis MCP server for AI agents.',\n CHAIN_INSIGHTS_WORKFLOW,\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 aml_address_risk, graph_query, or graph_query_batch as needed.',\n MONEY_FLOWS_INSTRUCTION,\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 'aml_address_risk':\n return {\n address: z.string().min(1).describe('Blockchain address to screen.'),\n network: NETWORK_SCHEMA,\n compare_address: z\n .string()\n .optional()\n .describe('Optional address to compare against the screened address.'),\n version: z\n .string()\n .optional()\n .describe('Optional AML tool contract version. Omit to use the latest version.'),\n }\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 'aml-address-risk',\n {\n title: 'AML Address Risk',\n description:\n 'Screen a blockchain address for AML risk, behavioral patterns, neighborhood profile, member addresses, and exchange links.',\n argsSchema: {\n network: NETWORK_SCHEMA,\n address: z.string().describe('Blockchain address to screen'),\n compare_address: z\n .string()\n .optional()\n .describe('Optional address to compare against the screened address'),\n version: z\n .string()\n .optional()\n .describe('Optional AML tool contract version. Omit to use the latest version'),\n },\n },\n async ({ network, address, compare_address, version }) =>\n promptResult(\n [\n `Use Chain Insights aml_address_risk${version ? ` version ${version}` : ''} on ${network} for:`,\n '',\n `\\`${address}\\``,\n compare_address ? `\\nCompare with: \\`${compare_address}\\`` : '',\n '',\n 'Present the summary as-is. Do not add analysis, verdicts, or risk assessments; the tool output already contains the risk assessment.',\n ]\n .filter(Boolean)\n .join('\\n'),\n 'AML address risk screening'\n )\n )\n\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-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, money_flows, graph_expand) keeps its structuredContent\n// exactly as the hosted server 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 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 if (!remoteToolNames.has('aml_address_risk')) {\n server.registerTool(\n 'aml_address_risk',\n {\n title: 'Address Risk',\n description: KNOWN_PUBLIC_TOOL_DESCRIPTIONS.aml_address_risk,\n inputSchema: {\n address: z.string().min(1).describe('Blockchain address to screen'),\n network: NETWORK_SCHEMA,\n compare_address: z\n .string()\n .optional()\n .describe('Optional address to compare against the screened address'),\n version: z\n .string()\n .optional()\n .describe('Optional AML tool contract version. Omit to use the latest version'),\n },\n annotations: {\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: true,\n },\n },\n async ({ address, network, compare_address, version }) => {\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 { runAmlAddressRisk } = await import('../investigation/public-tools.js')\n const result = await runAmlAddressRisk(\n remoteClient,\n {\n address,\n network,\n compareAddress: compare_address,\n writeArtifacts: workspaceArtifactsEnabled,\n },\n version\n )\n return {\n content: [{ type: 'text' as const, text: result.summaryText }],\n structuredContent: result.structuredContent,\n isError: false,\n }\n } catch (err) {\n if (err instanceof PaymentRequiredError) {\n return { content: [{ type: 'text' as const, text: err.message }], isError: true }\n }\n return {\n content: [\n { type: 'text' as const, text: `Address risk failed: ${(err as Error).message}` },\n ],\n isError: true,\n }\n }\n }\n )\n }\n\n // money_flows and graph_expand: composed here from anchored graph_query\n // reads sent through the remote client, the path and payment wrapping a\n // graph_query call takes, so each read is billed 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 MONEY_FLOWS_TOOL,\n {\n title: 'Money flows',\n description: MONEY_FLOWS_DESCRIPTION,\n inputSchema: {\n address: z.string().describe('One robinhood address: 0x and 40 hexadecimal characters.'),\n network: z.string().describe('The network. Only robinhood is served.'),\n in_offset: z\n .number()\n .int()\n .min(0)\n .max(FLOWS_MAX_OFFSET)\n .optional()\n .describe('Senders to skip, for the next page: the in_offset the previous answer names.'),\n out_offset: z\n .number()\n .int()\n .min(0)\n .max(FLOWS_MAX_OFFSET)\n .optional()\n .describe(\n 'Receivers to skip, for the next page: the out_offset the previous answer names.'\n ),\n },\n annotations: VIEW_TOOL_ANNOTATIONS,\n _meta: VIEW_TOOL_META,\n },\n async (args) => handleMoneyFlows(args, flowsDependencies)\n )\n server.registerTool(\n GRAPH_EXPAND_TOOL,\n {\n title: 'Expand a money-flow node',\n description: GRAPH_EXPAND_DESCRIPTION,\n inputSchema: {\n network: z.string(),\n address: z.string(),\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 },\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_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 '- aml_address_risk: screen one blockchain address; optionally compare it with another address.',\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 '- money_flows: show the most recent senders and receivers of one robinhood address, with an interactive graph in hosts that draw MCP apps.',\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_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 '- aml_address_risk: screen one blockchain address; optionally compare it with another address.',\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 '- money_flows: show the most recent senders and receivers of one robinhood address, with an interactive graph in hosts that draw MCP apps.',\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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"}
|
|
@@ -55,7 +55,12 @@ const PUBLIC_MCP_TOOL_ALLOWED_ARGS = {
|
|
|
55
55
|
"per_query_timeout_seconds",
|
|
56
56
|
"time_scope"
|
|
57
57
|
],
|
|
58
|
-
money_flows: [
|
|
58
|
+
money_flows: [
|
|
59
|
+
"address",
|
|
60
|
+
"network",
|
|
61
|
+
"in_offset",
|
|
62
|
+
"out_offset"
|
|
63
|
+
]
|
|
59
64
|
};
|
|
60
65
|
function isHiddenRemoteToolName(name) {
|
|
61
66
|
return HIDDEN_REMOTE_TOOL_NAMES.has(name);
|
|
@@ -94,4 +99,4 @@ function validatePublicMcpToolArguments(name, args) {
|
|
|
94
99
|
//#endregion
|
|
95
100
|
export { tool_visibility_exports as a, isAppOnlyTool as i, PUBLIC_MCP_TOOL_ALLOWED_ARGS as n, PUBLIC_MCP_TOOL_REQUIRED_ARGS as r, HIDDEN_REMOTE_TOOL_NAMES as t };
|
|
96
101
|
|
|
97
|
-
//# sourceMappingURL=tool-visibility-
|
|
102
|
+
//# sourceMappingURL=tool-visibility-BdncNrVB.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-visibility-BdncNrVB.mjs","names":[],"sources":["../src/mcp/tool-visibility.ts"],"sourcesContent":["import { McpToolError, formatMcpToolError } from './print-result.js'\n\nexport const HIDDEN_REMOTE_TOOL_NAMES = new Set([\n 'topup',\n 'address_risk',\n 'trace_victim_funds',\n 'trace_suspect_funds',\n 'trace_deposit_sources',\n 'trace_funds',\n 'track_funds',\n // Retired AML trace tools: never exposed publicly. Built by concatenation\n // so the retired names do not appear as literals in this file.\n ['aml_trace_victim', '_funds'].join(''),\n ['aml_trace_suspect', '_funds'].join(''),\n ['aml_trace_deposit', '_sources'].join(''),\n 'network_capabilities',\n 'usage_status',\n 'subscription_status',\n 'balance',\n 'help',\n 'money_flows_between_exchanges',\n 'address_connection_risk',\n])\n\nexport const PUBLIC_MCP_TOOL_REQUIRED_ARGS: Record<string, string[]> = {\n aml_address_risk: ['address', 'network'],\n graph_query: ['query', 'network'],\n graph_query_batch: ['network', 'queries'],\n money_flows: ['address', 'network'],\n}\n\n// normalizeRemoteToolArguments FILTERS pass-through arguments to this list, so\n// an argument that is missing here is silently STRIPPED and the caller gets a\n// default-bounded result with no error and no warning. `time_scope` shipped\n// broken for exactly this reason. Every tunable search bound below\n// (per_address_limit, row_limit) must therefore be listed the moment it\n// appears on a tool schema, and is covered end-to-end through the proxy in\n// tests/mcp-proxy.test.ts.\nexport const PUBLIC_MCP_TOOL_ALLOWED_ARGS: Record<string, string[]> = {\n aml_address_risk: ['address', 'network', 'compare_address', 'version'],\n // `time_scope` narrows a `USE topology` query to a temporal-shard subset\n graph_query: ['query', 'network', 'time_scope'],\n graph_query_batch: ['network', 'queries', 'per_query_timeout_seconds', 'time_scope'],\n money_flows: ['address', 'network', 'in_offset', 'out_offset'],\n}\n\nexport function isHiddenRemoteToolName(name: string): boolean {\n return HIDDEN_REMOTE_TOOL_NAMES.has(name)\n}\n\n/**\n * True for an MCP Apps tool only a view may call: `_meta.ui.visibility` is set\n * and does not include \"model\" (for example graph_expand, visibility [\"app\"]).\n * Such a tool never appears in a tool list built for the model or the CLI.\n */\nexport function isAppOnlyTool(tool: { _meta?: Record<string, unknown> }): boolean {\n const ui = tool._meta?.ui\n if (!ui || typeof ui !== 'object' || Array.isArray(ui)) return false\n const visibility = (ui as Record<string, unknown>).visibility\n return Array.isArray(visibility) && !visibility.includes('model')\n}\n\nexport function visibleRemoteTools<T extends { name: string; _meta?: Record<string, unknown> }>(\n tools: T[]\n): T[] {\n return tools.filter((tool) => !isHiddenRemoteToolName(tool.name) && !isAppOnlyTool(tool))\n}\n\nexport function assertPublicMcpToolName(name: string): void {\n if (!isHiddenRemoteToolName(name)) return\n const replacement =\n name === 'address_risk'\n ? ' Use aml_address_risk instead.'\n : name === 'network_capabilities'\n ? ' Use meta_network_capabilities instead.'\n : name === 'usage_status'\n ? ' Use meta_usage_status instead.'\n : name === 'subscription_status'\n ? ' Use meta_subscription_status instead.'\n : name === 'balance'\n ? ' Use wallet_balance instead.'\n : name === 'help'\n ? ' Use meta_help instead.'\n : ''\n throw new Error(`MCP tool '${name}' is not exposed by Chain Insights.${replacement}`)\n}\n\nexport function formatMcpCallError(tool: string, err: unknown): string {\n // A tool error with an envelope is the server's own answer: its text, then the\n // line of its error_detail. It is never matched against the unknown-tool\n // pattern below. A tool error with no envelope keeps the old reading, because\n // a server can answer an unknown tool as an error reply.\n if (err instanceof McpToolError && err.errorDetail) return formatMcpToolError(err)\n const message = err instanceof Error ? err.message : String(err)\n if (/\\bunknown tool\\b|\\btool\\b.*\\bnot found\\b/i.test(message)) {\n return `Unknown MCP tool \"${tool}\". Run \\`cia mcp tools --refresh\\` to list available tools.`\n }\n return message\n}\n\nexport function validatePublicMcpToolArguments(name: string, args: Record<string, unknown>): void {\n const allowedArgs = PUBLIC_MCP_TOOL_ALLOWED_ARGS[name]\n if (!allowedArgs) return\n\n const unsupportedArgs = Object.keys(args).filter((argName) => !allowedArgs.includes(argName))\n if (unsupportedArgs.length === 0) return\n\n throw new Error(\n [\n `Unsupported argument${unsupportedArgs.length === 1 ? '' : 's'} for ${name}: ${unsupportedArgs.join(', ')}.`,\n `Allowed arguments: ${allowedArgs.join(', ')}.`,\n ].join(' ')\n )\n}\n"],"mappings":";;;;;;;;;;;;;;AAEA,MAAa,2CAA2B,IAAI,IAAI;CAC9C;CACA;CACA;CACA;CACA;CACA;CACA;CAGA,CAAC,oBAAoB,QAAQ,CAAC,CAAC,KAAK,EAAE;CACtC,CAAC,qBAAqB,QAAQ,CAAC,CAAC,KAAK,EAAE;CACvC,CAAC,qBAAqB,UAAU,CAAC,CAAC,KAAK,EAAE;CACzC;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;AAED,MAAa,gCAA0D;CACrE,kBAAkB,CAAC,WAAW,SAAS;CACvC,aAAa,CAAC,SAAS,SAAS;CAChC,mBAAmB,CAAC,WAAW,SAAS;CACxC,aAAa,CAAC,WAAW,SAAS;AACpC;AASA,MAAa,+BAAyD;CACpE,kBAAkB;EAAC;EAAW;EAAW;EAAmB;CAAS;CAErE,aAAa;EAAC;EAAS;EAAW;CAAY;CAC9C,mBAAmB;EAAC;EAAW;EAAW;EAA6B;CAAY;CACnF,aAAa;EAAC;EAAW;EAAW;EAAa;CAAY;AAC/D;AAEA,SAAgB,uBAAuB,MAAuB;CAC5D,OAAO,yBAAyB,IAAI,IAAI;AAC1C;;;;;;AAOA,SAAgB,cAAc,MAAoD;CAChF,MAAM,KAAK,KAAK,OAAO;CACvB,IAAI,CAAC,MAAM,OAAO,OAAO,YAAY,MAAM,QAAQ,EAAE,GAAG,OAAO;CAC/D,MAAM,aAAc,GAA+B;CACnD,OAAO,MAAM,QAAQ,UAAU,KAAK,CAAC,WAAW,SAAS,OAAO;AAClE;AAEA,SAAgB,mBACd,OACK;CACL,OAAO,MAAM,QAAQ,SAAS,CAAC,uBAAuB,KAAK,IAAI,KAAK,CAAC,cAAc,IAAI,CAAC;AAC1F;AAEA,SAAgB,wBAAwB,MAAoB;CAC1D,IAAI,CAAC,uBAAuB,IAAI,GAAG;CAenC,MAAM,IAAI,MAAM,aAAa,KAAK,qCAbhC,SAAS,iBACL,mCACA,SAAS,yBACP,4CACA,SAAS,iBACP,oCACA,SAAS,wBACP,2CACA,SAAS,YACP,iCACA,SAAS,SACP,4BACA,IACoE;AACtF;AAEA,SAAgB,mBAAmB,MAAc,KAAsB;CAKrE,IAAI,eAAe,gBAAgB,IAAI,aAAa,OAAO,mBAAmB,GAAG;CACjF,MAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;CAC/D,IAAI,4CAA4C,KAAK,OAAO,GAC1D,OAAO,qBAAqB,KAAK;CAEnC,OAAO;AACT;AAEA,SAAgB,+BAA+B,MAAc,MAAqC;CAChG,MAAM,cAAc,6BAA6B;CACjD,IAAI,CAAC,aAAa;CAElB,MAAM,kBAAkB,OAAO,KAAK,IAAI,CAAC,CAAC,QAAQ,YAAY,CAAC,YAAY,SAAS,OAAO,CAAC;CAC5F,IAAI,gBAAgB,WAAW,GAAG;CAElC,MAAM,IAAI,MACR,CACE,uBAAuB,gBAAgB,WAAW,IAAI,KAAK,IAAI,OAAO,KAAK,IAAI,gBAAgB,KAAK,IAAI,EAAE,IAC1G,sBAAsB,YAAY,KAAK,IAAI,EAAE,EAC/C,CAAC,CAAC,KAAK,GAAG,CACZ;AACF"}
|
|
@@ -55,7 +55,12 @@ const PUBLIC_MCP_TOOL_ALLOWED_ARGS = {
|
|
|
55
55
|
"per_query_timeout_seconds",
|
|
56
56
|
"time_scope"
|
|
57
57
|
],
|
|
58
|
-
money_flows: [
|
|
58
|
+
money_flows: [
|
|
59
|
+
"address",
|
|
60
|
+
"network",
|
|
61
|
+
"in_offset",
|
|
62
|
+
"out_offset"
|
|
63
|
+
]
|
|
59
64
|
};
|
|
60
65
|
function isHiddenRemoteToolName(name) {
|
|
61
66
|
return HIDDEN_REMOTE_TOOL_NAMES.has(name);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chain-insights",
|
|
3
|
-
"version": "0.42.
|
|
3
|
+
"version": "0.42.2",
|
|
4
4
|
"description": "AML investigation CLI and MCP proxy for blockchain risk screening and Chain Insights Graph access",
|
|
5
5
|
"homepage": "https://chain-insights.ai",
|
|
6
6
|
"repository": {
|
|
@@ -71,18 +71,18 @@
|
|
|
71
71
|
},
|
|
72
72
|
"dependencies": {
|
|
73
73
|
"@modelcontextprotocol/ext-apps": "1.7.5",
|
|
74
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
75
|
-
"@x402/evm": "^2.
|
|
76
|
-
"@x402/fetch": "^2.
|
|
74
|
+
"@modelcontextprotocol/sdk": "^1.32.1",
|
|
75
|
+
"@x402/evm": "^2.28.0",
|
|
76
|
+
"@x402/fetch": "^2.28.0",
|
|
77
77
|
"commander": "^14.0.3",
|
|
78
78
|
"open": "^11.0.0",
|
|
79
|
-
"viem": "^2.
|
|
79
|
+
"viem": "^2.57.3",
|
|
80
80
|
"zod": "^4.4.3"
|
|
81
81
|
},
|
|
82
82
|
"devDependencies": {
|
|
83
83
|
"@arethetypeswrong/cli": "^0.18.5",
|
|
84
84
|
"@types/node": "^26.2.0",
|
|
85
|
-
"@vitest/coverage-v8": "^5.0.
|
|
85
|
+
"@vitest/coverage-v8": "^5.0.3",
|
|
86
86
|
"jsqr": "1.4.0",
|
|
87
87
|
"oxlint": "^1.0.0",
|
|
88
88
|
"prettier": "^3.0.0",
|
|
@@ -90,7 +90,7 @@
|
|
|
90
90
|
"tsdown": "^0.23.0",
|
|
91
91
|
"tsx": "^4.23.12",
|
|
92
92
|
"typescript": "^7.0.2",
|
|
93
|
-
"vitest": "^5.0.
|
|
93
|
+
"vitest": "^5.0.3"
|
|
94
94
|
},
|
|
95
95
|
"overrides": {
|
|
96
96
|
"ws": "8.21.0"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"call-args-ysbB_xdA.mjs","names":[],"sources":["../src/mcp/call-args.ts"],"sourcesContent":["// `cia mcp call tool key=value` yields strings. A key listed here is coerced\n// to a number; anything else stays a string and is rejected by the tool's\n// numeric zod schema. Every tunable search bound must therefore appear here,\n// or passing it from the CLI fails with a confusing type error.\nconst NUMERIC_ARG_KEYS = new Set([\n 'per_query_timeout_seconds',\n 'incident_timestamp',\n 'max_hops',\n 'per_address_limit',\n 'row_limit',\n])\n\nfunction parseMcpArgValue(key: string, value: string): unknown {\n const trimmed = value.trim()\n if (!trimmed) return value\n\n if (trimmed.startsWith('{') || trimmed.startsWith('[')) {\n return JSON.parse(trimmed) as unknown\n }\n\n if (NUMERIC_ARG_KEYS.has(key) && /^-?\\d+(?:\\.\\d+)?$/.test(trimmed)) {\n return Number(trimmed)\n }\n\n return value\n}\n\nexport function parseMcpCallArgs(rawArgs: string[]): Record<string, unknown> {\n const args: Record<string, unknown> = {}\n for (const pair of rawArgs) {\n const eqIdx = pair.indexOf('=')\n if (eqIdx === -1) {\n throw new Error(`Invalid arg format: ${pair} (expected key=value)`)\n }\n const key = pair.slice(0, eqIdx)\n args[key] = parseMcpArgValue(key, pair.slice(eqIdx + 1))\n }\n return args\n}\n"],"mappings":";AAIA,MAAM,mCAAmB,IAAI,IAAI;CAC/B;CACA;CACA;CACA;CACA;AACF,CAAC;AAED,SAAS,iBAAiB,KAAa,OAAwB;CAC7D,MAAM,UAAU,MAAM,KAAK;CAC3B,IAAI,CAAC,SAAS,OAAO;CAErB,IAAI,QAAQ,WAAW,GAAG,KAAK,QAAQ,WAAW,GAAG,GACnD,OAAO,KAAK,MAAM,OAAO;CAG3B,IAAI,iBAAiB,IAAI,GAAG,KAAK,oBAAoB,KAAK,OAAO,GAC/D,OAAO,OAAO,OAAO;CAGvB,OAAO;AACT;AAEA,SAAgB,iBAAiB,SAA4C;CAC3E,MAAM,OAAgC,CAAC;CACvC,KAAK,MAAM,QAAQ,SAAS;EAC1B,MAAM,QAAQ,KAAK,QAAQ,GAAG;EAC9B,IAAI,UAAU,IACZ,MAAM,IAAI,MAAM,uBAAuB,KAAK,sBAAsB;EAEpE,MAAM,MAAM,KAAK,MAAM,GAAG,KAAK;EAC/B,KAAK,OAAO,iBAAiB,KAAK,KAAK,MAAM,QAAQ,CAAC,CAAC;CACzD;CACA,OAAO;AACT"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"flows-BfUlyWvJ.mjs","names":[],"sources":["../src/mcp/flows.ts"],"sourcesContent":["/**\n * The money-flow view tools of the local proxy: money_flows and graph_expand.\n *\n * money_flows answers one robinhood address with its most recent senders and\n * receivers: a short text the model reads, and chain-insights.flows.v1 in\n * structuredContent, which the Chain Insights view (ui://chain-insights/view)\n * draws. graph_expand is the same answer for the next page of one address. The\n * view calls it when the investigator clicks an address; its _meta says\n * visibility [\"app\"], so a host keeps it away from the model.\n *\n * Both tools are composed on the client PC from three anchored graph_query\n * reads (the node, its outgoing FLOWS_TO and its incoming FLOWS_TO, newest\n * first), sent through the proxy's remote client, the same path and payment\n * wrapping a graph_query call takes. Each read is billed as a graph_query.\n * Every argument is checked before the first read, so a refused call reaches\n * no graph endpoint and costs nothing.\n */\n\nexport const FLOWS_SCHEMA = 'chain-insights.flows.v1'\nexport const MONEY_FLOWS_TOOL = 'money_flows'\nexport const GRAPH_EXPAND_TOOL = 'graph_expand'\nexport const FLOWS_NETWORK = 'robinhood'\n\n/** How many senders and how many receivers one page shows. */\nexport const FLOWS_PER_SIDE = 12\n/** The view data of one answer: at most this many addresses... */\nexport const FLOWS_MAX_NODES = 60\n/** ...and under this many characters of JSON. */\nexport const FLOWS_MAX_CHARS = 40_000\n/** The text the model reads holds at most this many lines. */\nexport const FLOWS_SUMMARY_MAX_LINES = 20\n/** Where a graph_expand page may start on each side, at most. */\nexport const FLOWS_MAX_OFFSET = 10_000\n\nexport type FlowNode = {\n address: string\n role: string\n labels: string[]\n total_in_usd: number\n total_out_usd: number\n degree_in: number\n degree_out: number\n}\n\nexport type FlowEdge = {\n from: string\n to: string\n usd: number\n tx_count: number\n first_seen_ms: number\n last_seen_ms: number\n}\n\nexport type FlowCursor = { in_offset: number; out_offset: number }\n\nexport type FlowView = {\n schema: typeof FLOWS_SCHEMA\n network: string\n center: string\n nodes: FlowNode[]\n edges: FlowEdge[]\n cursor: FlowCursor\n truncated: boolean\n}\n\ntype Row = Record<string, unknown>\n\nexport type FlowsPage = {\n center: Row\n senders: Row[]\n receivers: Row[]\n inOffset: number\n outOffset: number\n}\n\n/** One graph_query answer as the remote client returns it. */\nexport type GraphQueryAnswer = {\n content?: Array<{ type: string; text?: string }>\n structuredContent?: Record<string, unknown>\n isError?: boolean\n}\n\nexport type FlowsToolResult = {\n content: Array<{ type: 'text'; text: string }>\n structuredContent?: Record<string, unknown>\n isError: boolean\n}\n\nexport type FlowsDependencies = {\n /** One graph_query call through the proxy's remote client. */\n graphQuery: (args: { network: string; query: string }) => Promise<GraphQueryAnswer>\n /** The text of a call that threw (payment required, transport failure). */\n describeFailure: (err: unknown) => string\n /** Why no read can run (the graph endpoint is not connected), checked after the arguments. */\n unavailable?: () => string | undefined\n now?: () => Date\n}\n\nexport const INVALID_ADDRESS_TEXT =\n 'invalid_address: a full 0x address of 40 hexadecimal characters is needed, for example 0x04911a118f11c75667e4d0dfb8e640af5a353550'\nexport const OFFSET_TOO_LARGE_TEXT = `invalid_offset: in_offset and out_offset must each be at most ${FLOWS_MAX_OFFSET}`\nexport const OFFSET_NEGATIVE_TEXT =\n 'invalid_offset: in_offset and out_offset must each be a whole number, 0 or more'\n\nconst ADDRESS_PATTERN = /^0x[0-9a-f]{40}$/\nconst CODE_PREFIX = /^([a-z][a-z0-9_]*):\\s*/\n\n/**\n * The address lower-cased, or null when it is not 0x and 40 hexadecimal\n * characters. Only an address that passes is ever written into a read, so the\n * reads can carry it as a literal.\n */\nexport function normalizeFlowsAddress(raw: unknown): string | null {\n if (typeof raw !== 'string') return null\n const address = raw.trim().toLowerCase()\n return ADDRESS_PATTERN.test(address) ? address : null\n}\n\nconst NODE_FIELDS =\n 'b.address AS address, b.labels AS labels, b.is_exchange AS is_exchange, ' +\n 'b.is_sanctioned AS is_sanctioned, b.is_scam AS is_scam, b.is_victim AS is_victim, b:Pool AS pool, ' +\n 'b.total_in_usd AS total_in_usd, b.total_out_usd AS total_out_usd, b.degree_in AS degree_in, b.degree_out AS degree_out'\nconst PARTY_FIELDS =\n NODE_FIELDS +\n ', f.amount_usd_sum AS usd, f.tx_count AS tx_count, ' +\n 'f.first_seen_timestamp AS first_seen, f.last_seen_timestamp AS last_seen'\n// Newest link first, the address breaking a tie, so a page and the next one\n// never share or skip a link.\nconst PAGE_ORDER = ' ORDER BY f.last_seen_timestamp DESC, b.address'\n\nexport type FlowsRead = { id: 'node' | 'out' | 'in'; query: string }\n\n/**\n * The three anchored reads of one page. Each side asks for one row more than\n * it shows: the extra row says another page exists.\n */\nexport function flowsReadsFor(address: string, inOffset: number, outOffset: number): FlowsRead[] {\n const page = (offset: number) =>\n `${PAGE_ORDER}${offset > 0 ? ` SKIP ${offset}` : ''} LIMIT ${FLOWS_PER_SIDE + 1}`\n return [\n {\n id: 'node',\n query: `USE topology MATCH (b:Address {address: '${address}'}) RETURN ${NODE_FIELDS} LIMIT 1`,\n },\n {\n id: 'out',\n query: `USE topology MATCH (a:Address {address: '${address}'})-[f:FLOWS_TO]->(b:Address) RETURN ${PARTY_FIELDS}${page(outOffset)}`,\n },\n {\n id: 'in',\n query: `USE topology MATCH (b:Address)-[f:FLOWS_TO]->(a:Address {address: '${address}'}) RETURN ${PARTY_FIELDS}${page(inOffset)}`,\n },\n ]\n}\n\n/** The role word of an address, first match wins. */\nexport function flowsRole(row: Row): string {\n if (row.is_exchange === true) return 'exchange'\n if (row.is_sanctioned === true) return 'sanctioned'\n if (row.is_scam === true) return 'scam'\n if (row.is_victim === true) return 'victim'\n if (row.pool === true) return 'pool'\n return 'other'\n}\n\nfunction flowsFloat(value: unknown): number {\n return typeof value === 'number' && Number.isFinite(value) ? value : 0\n}\n\nfunction flowsInt(value: unknown): number {\n return typeof value === 'number' && Number.isFinite(value) ? Math.trunc(value) : 0\n}\n\nfunction flowsNodeOf(row: Row): FlowNode {\n const labels = Array.isArray(row.labels)\n ? row.labels.filter((label): label is string => typeof label === 'string')\n : []\n return {\n address: typeof row.address === 'string' ? row.address : '',\n role: flowsRole(row),\n labels,\n total_in_usd: flowsFloat(row.total_in_usd),\n total_out_usd: flowsFloat(row.total_out_usd),\n degree_in: flowsInt(row.degree_in),\n degree_out: flowsInt(row.degree_out),\n }\n}\n\nfunction assembleFlowView(\n network: string,\n page: FlowsPage,\n senders: Row[],\n receivers: Row[],\n truncated: boolean\n): FlowView {\n const center = flowsNodeOf(page.center)\n const view: FlowView = {\n schema: FLOWS_SCHEMA,\n network,\n center: center.address,\n nodes: [center],\n edges: [],\n cursor: {\n in_offset: page.inOffset + senders.length,\n out_offset: page.outOffset + receivers.length,\n },\n truncated,\n }\n const seen = new Set([center.address])\n const add = (row: Row, incoming: boolean) => {\n const node = flowsNodeOf(row)\n if (!seen.has(node.address)) {\n seen.add(node.address)\n view.nodes.push(node)\n }\n view.edges.push({\n from: incoming ? node.address : center.address,\n to: incoming ? center.address : node.address,\n usd: flowsFloat(row.usd),\n tx_count: flowsInt(row.tx_count),\n first_seen_ms: flowsInt(row.first_seen),\n last_seen_ms: flowsInt(row.last_seen),\n })\n }\n for (const row of senders) add(row, true)\n for (const row of receivers) add(row, false)\n return view\n}\n\nfunction fitsFlowBudget(view: FlowView): boolean {\n return view.nodes.length <= FLOWS_MAX_NODES && JSON.stringify(view).length < FLOWS_MAX_CHARS\n}\n\n/**\n * flows.v1 from one page: the centre first, then the senders and the\n * receivers, each newest first, at most FLOWS_PER_SIDE a side. When the view\n * data would hold more than FLOWS_MAX_NODES addresses or reach FLOWS_MAX_CHARS\n * characters, the oldest links go first, the view is marked truncated, and the\n * cursor still says where the next page starts.\n */\nexport function buildFlowView(network: string, page: FlowsPage): FlowView {\n let truncated = page.senders.length > FLOWS_PER_SIDE || page.receivers.length > FLOWS_PER_SIDE\n let senders = page.senders.slice(0, FLOWS_PER_SIDE)\n let receivers = page.receivers.slice(0, FLOWS_PER_SIDE)\n for (;;) {\n const view = assembleFlowView(network, page, senders, receivers, truncated)\n if (fitsFlowBudget(view) || senders.length + receivers.length === 0) return view\n truncated = true\n if (senders.length >= receivers.length) senders = senders.slice(0, -1)\n else receivers = receivers.slice(0, -1)\n }\n}\n\nexport function formatFlowsUsd(value: number): string {\n if (!(value > 0)) return '$0'\n if (value < 0.01) return 'under $0.01'\n const whole = Math.floor(value)\n const cents = Math.round((value - whole) * 100)\n if (cents === 100) return formatFlowsUsd(whole + 1)\n const grouped = whole.toFixed(0).replace(/\\B(?=(\\d{3})+(?!\\d))/g, ',')\n return `$${grouped}.${String(cents).padStart(2, '0')}`\n}\n\nfunction formatFlowsAge(lastSeenMs: number, now: Date): string {\n if (lastSeenMs <= 0) return 'at an unknown time'\n const date = new Date(lastSeenMs).toISOString().slice(0, 10)\n const days = Math.trunc((now.getTime() - lastSeenMs) / 86_400_000)\n if (days < 1) return `${date} (less than a day ago)`\n if (days === 1) return `${date} (1 day ago)`\n return `${date} (${days} days ago)`\n}\n\n/** The role word, with up to three address labels beside it. */\nfunction roleText(node: FlowNode | undefined): string {\n if (!node) return 'other'\n if (node.labels.length === 0) return node.role\n return `${node.role} (${node.labels.slice(0, 3).join(', ')})`\n}\n\nfunction sideLines(title: string, parties: string[]): string[] {\n if (parties.length === 0) return [`${title}, newest first: none.`]\n const lines = [`${title}, newest first (${parties.length}):`]\n // A side with more than six counterparties lists two on a line, so a full\n // page stays within the line budget.\n const perLine = parties.length > 6 ? 2 : 1\n for (let start = 0; start < parties.length; start += perLine) {\n lines.push(`- ${parties.slice(start, start + perLine).join('; ')}`)\n }\n return lines\n}\n\n/**\n * The text the model reads: the centre, its role and lifetime totals, then\n * every counterparty of the view with its USD, transaction count and last-seen\n * date and age. At most FLOWS_SUMMARY_MAX_LINES lines.\n */\nexport function flowsSummary(view: FlowView, now: Date): string {\n const nodes = new Map(view.nodes.map((node) => [node.address, node]))\n const center = nodes.get(view.center)\n const party = (address: string, edge: FlowEdge) =>\n `${address} ${roleText(nodes.get(address))}, ${formatFlowsUsd(edge.usd)} in ${edge.tx_count} tx, last seen ${formatFlowsAge(edge.last_seen_ms, now)}`\n const senders: string[] = []\n const receivers: string[] = []\n for (const edge of view.edges) {\n if (edge.to === view.center) senders.push(party(edge.from, edge))\n else receivers.push(party(edge.to, edge))\n }\n const lines = [\n `Money flows of ${view.center} on ${view.network}: ${roleText(center)}; lifetime ${formatFlowsUsd(center?.total_in_usd ?? 0)} in, ${formatFlowsUsd(center?.total_out_usd ?? 0)} out; ${center?.degree_in ?? 0} senders and ${center?.degree_out ?? 0} receivers in the topology graph.`,\n ...sideLines('Recent senders', senders),\n ...sideLines('Recent receivers', receivers),\n ]\n if (view.truncated)\n lines.push('More flows exist than shown; the view loads the next page on click.')\n return lines.slice(0, FLOWS_SUMMARY_MAX_LINES).join('\\n')\n}\n\nfunction errorResult(text: string): FlowsToolResult {\n return { content: [{ type: 'text', text }], isError: true }\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return !!value && typeof value === 'object' && !Array.isArray(value)\n}\n\nfunction answerText(answer: GraphQueryAnswer): string {\n return (answer.content ?? [])\n .map((block) => (block.type === 'text' && typeof block.text === 'string' ? block.text : ''))\n .filter(Boolean)\n .join('\\n')\n .trim()\n}\n\n/**\n * The refusal of a read that did not answer, naming its code first:\n * error_detail.code when the graph endpoint sent its typed refusal, the code\n * its text starts with otherwise, and graph_query_failed when it names none.\n * The view marks the address it was expanding.\n */\nexport function refusalText(answer: GraphQueryAnswer): string {\n const text = answerText(answer)\n const detail = isRecord(answer.structuredContent)\n ? answer.structuredContent.error_detail\n : undefined\n const detailCode = isRecord(detail) && typeof detail.code === 'string' ? detail.code : ''\n const textCode = CODE_PREFIX.exec(text)?.[1] ?? ''\n const code = detailCode || textCode || 'graph_query_failed'\n const rest = textCode === code ? text.replace(CODE_PREFIX, '') : text\n return rest ? `${code}: ${rest}` : `${code}: the graph endpoint refused the read`\n}\n\nfunction rowsOf(answer: GraphQueryAnswer): Row[] | null {\n const facts = isRecord(answer.structuredContent) ? answer.structuredContent.facts : undefined\n const query = isRecord(facts) ? facts.query : undefined\n const results = isRecord(query) ? query.results : undefined\n return Array.isArray(results) ? results.filter(isRecord) : null\n}\n\ntype Checked = { network: string; address: string } | { error: string }\n\nfunction checkNetworkAndAddress(args: Record<string, unknown>): Checked {\n const rawNetwork = typeof args.network === 'string' ? args.network.trim().toLowerCase() : ''\n if (!rawNetwork) return { error: 'invalid_network: network is required; pass robinhood' }\n if (rawNetwork !== FLOWS_NETWORK) {\n return {\n error: `invalid_network: money flows read the robinhood topology graph only; got \"${String(args.network).trim()}\"`,\n }\n }\n const address = normalizeFlowsAddress(args.address)\n if (!address) return { error: INVALID_ADDRESS_TEXT }\n return { network: FLOWS_NETWORK, address }\n}\n\nfunction checkOffset(value: unknown): number | { error: string } {\n if (value === undefined || value === null) return 0\n if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) {\n return { error: OFFSET_NEGATIVE_TEXT }\n }\n if (value > FLOWS_MAX_OFFSET) return { error: OFFSET_TOO_LARGE_TEXT }\n return value\n}\n\n/**\n * Read and answer one page. The reads run one after another: the node first,\n * so an address the graph does not hold costs one read, not three; then the\n * receivers and the senders. The first read that does not answer ends the call.\n */\nasync function runFlows(\n deps: FlowsDependencies,\n network: string,\n address: string,\n inOffset: number,\n outOffset: number\n): Promise<FlowsToolResult> {\n const unavailable = deps.unavailable?.()\n if (unavailable) return errorResult(unavailable)\n const rows: Partial<Record<FlowsRead['id'], Row[]>> = {}\n for (const read of flowsReadsFor(address, inOffset, outOffset)) {\n let answer: GraphQueryAnswer\n try {\n answer = await deps.graphQuery({ network, query: read.query })\n } catch (err) {\n return errorResult(deps.describeFailure(err))\n }\n if (answer.isError === true) return errorResult(refusalText(answer))\n const found = rowsOf(answer)\n if (!found) {\n return errorResult(\n 'graph_query_failed: the graph endpoint answered without chain-insights.result.v1 rows'\n )\n }\n rows[read.id] = found\n if (read.id === 'node' && found.length === 0) {\n return errorResult(`${address} is not in the ${network} topology graph`)\n }\n }\n const view = buildFlowView(network, {\n center: rows.node?.[0] ?? {},\n senders: rows.in ?? [],\n receivers: rows.out ?? [],\n inOffset,\n outOffset,\n })\n const now = deps.now ? deps.now() : new Date()\n return {\n content: [{ type: 'text', text: flowsSummary(view, now) }],\n structuredContent: view as unknown as Record<string, unknown>,\n isError: false,\n }\n}\n\n/** money_flows {address, network}: the first page of one address. */\nexport async function handleMoneyFlows(\n args: unknown,\n deps: FlowsDependencies\n): Promise<FlowsToolResult> {\n const checked = checkNetworkAndAddress(isRecord(args) ? args : {})\n if ('error' in checked) return errorResult(checked.error)\n return runFlows(deps, checked.network, checked.address, 0, 0)\n}\n\n/** graph_expand {network, address, in_offset, out_offset}: the next page of one address. */\nexport async function handleGraphExpand(\n args: unknown,\n deps: FlowsDependencies\n): Promise<FlowsToolResult> {\n const record = isRecord(args) ? args : {}\n const checked = checkNetworkAndAddress(record)\n if ('error' in checked) return errorResult(checked.error)\n const inOffset = checkOffset(record.in_offset)\n if (typeof inOffset !== 'number') return errorResult(inOffset.error)\n const outOffset = checkOffset(record.out_offset)\n if (typeof outOffset !== 'number') return errorResult(outOffset.error)\n return runFlows(deps, checked.network, checked.address, inOffset, outOffset)\n}\n\nexport const MONEY_FLOWS_DESCRIPTION =\n 'Show the most recent money flows around one robinhood address, read from the Chain Insights topology graph: ' +\n 'the address with its role and lifetime USD totals, its 12 most recent senders and its 12 most recent receivers, ' +\n 'each link with its lifetime USD, transaction count and last-seen time. The answer is a text summary, and an ' +\n 'interactive graph in hosts that draw MCP apps. An address not in the graph is an error. Read-only; billed as ' +\n 'three graph_query topology reads, or one when the address is not in the graph.'\n\nexport const GRAPH_EXPAND_DESCRIPTION =\n 'Load the next page of senders and receivers of one address into an open money-flow view. ' +\n 'Called by the view when the investigator clicks an address. Read-only; billed as three graph_query ' +\n 'topology reads, nothing when it is refused before a read.'\n\n/** The line of the server instructions that tells a model what money_flows answers. */\nexport const MONEY_FLOWS_INSTRUCTION =\n 'Use money_flows to see who sent money to one robinhood address and where it went: its 12 most recent senders and receivers, each with lifetime USD, transaction count and last-seen time.'\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"tool-visibility-wW2vAaj9.mjs","names":[],"sources":["../src/mcp/tool-visibility.ts"],"sourcesContent":["import { McpToolError, formatMcpToolError } from './print-result.js'\n\nexport const HIDDEN_REMOTE_TOOL_NAMES = new Set([\n 'topup',\n 'address_risk',\n 'trace_victim_funds',\n 'trace_suspect_funds',\n 'trace_deposit_sources',\n 'trace_funds',\n 'track_funds',\n // Retired AML trace tools: never exposed publicly. Built by concatenation\n // so the retired names do not appear as literals in this file.\n ['aml_trace_victim', '_funds'].join(''),\n ['aml_trace_suspect', '_funds'].join(''),\n ['aml_trace_deposit', '_sources'].join(''),\n 'network_capabilities',\n 'usage_status',\n 'subscription_status',\n 'balance',\n 'help',\n 'money_flows_between_exchanges',\n 'address_connection_risk',\n])\n\nexport const PUBLIC_MCP_TOOL_REQUIRED_ARGS: Record<string, string[]> = {\n aml_address_risk: ['address', 'network'],\n graph_query: ['query', 'network'],\n graph_query_batch: ['network', 'queries'],\n money_flows: ['address', 'network'],\n}\n\n// normalizeRemoteToolArguments FILTERS pass-through arguments to this list, so\n// an argument that is missing here is silently STRIPPED and the caller gets a\n// default-bounded result with no error and no warning. `time_scope` shipped\n// broken for exactly this reason. Every tunable search bound below\n// (per_address_limit, row_limit) must therefore be listed the moment it\n// appears on a tool schema, and is covered end-to-end through the proxy in\n// tests/mcp-proxy.test.ts.\nexport const PUBLIC_MCP_TOOL_ALLOWED_ARGS: Record<string, string[]> = {\n aml_address_risk: ['address', 'network', 'compare_address', 'version'],\n // `time_scope` narrows a `USE topology` query to a temporal-shard subset\n graph_query: ['query', 'network', 'time_scope'],\n graph_query_batch: ['network', 'queries', 'per_query_timeout_seconds', 'time_scope'],\n money_flows: ['address', 'network'],\n}\n\nexport function isHiddenRemoteToolName(name: string): boolean {\n return HIDDEN_REMOTE_TOOL_NAMES.has(name)\n}\n\n/**\n * True for an MCP Apps tool only a view may call: `_meta.ui.visibility` is set\n * and does not include \"model\" (for example graph_expand, visibility [\"app\"]).\n * Such a tool never appears in a tool list built for the model or the CLI.\n */\nexport function isAppOnlyTool(tool: { _meta?: Record<string, unknown> }): boolean {\n const ui = tool._meta?.ui\n if (!ui || typeof ui !== 'object' || Array.isArray(ui)) return false\n const visibility = (ui as Record<string, unknown>).visibility\n return Array.isArray(visibility) && !visibility.includes('model')\n}\n\nexport function visibleRemoteTools<T extends { name: string; _meta?: Record<string, unknown> }>(\n tools: T[]\n): T[] {\n return tools.filter((tool) => !isHiddenRemoteToolName(tool.name) && !isAppOnlyTool(tool))\n}\n\nexport function assertPublicMcpToolName(name: string): void {\n if (!isHiddenRemoteToolName(name)) return\n const replacement =\n name === 'address_risk'\n ? ' Use aml_address_risk instead.'\n : name === 'network_capabilities'\n ? ' Use meta_network_capabilities instead.'\n : name === 'usage_status'\n ? ' Use meta_usage_status instead.'\n : name === 'subscription_status'\n ? ' Use meta_subscription_status instead.'\n : name === 'balance'\n ? ' Use wallet_balance instead.'\n : name === 'help'\n ? ' Use meta_help instead.'\n : ''\n throw new Error(`MCP tool '${name}' is not exposed by Chain Insights.${replacement}`)\n}\n\nexport function formatMcpCallError(tool: string, err: unknown): string {\n // A tool error with an envelope is the server's own answer: its text, then the\n // line of its error_detail. It is never matched against the unknown-tool\n // pattern below. A tool error with no envelope keeps the old reading, because\n // a server can answer an unknown tool as an error reply.\n if (err instanceof McpToolError && err.errorDetail) return formatMcpToolError(err)\n const message = err instanceof Error ? err.message : String(err)\n if (/\\bunknown tool\\b|\\btool\\b.*\\bnot found\\b/i.test(message)) {\n return `Unknown MCP tool \"${tool}\". Run \\`cia mcp tools --refresh\\` to list available tools.`\n }\n return message\n}\n\nexport function validatePublicMcpToolArguments(name: string, args: Record<string, unknown>): void {\n const allowedArgs = PUBLIC_MCP_TOOL_ALLOWED_ARGS[name]\n if (!allowedArgs) return\n\n const unsupportedArgs = Object.keys(args).filter((argName) => !allowedArgs.includes(argName))\n if (unsupportedArgs.length === 0) return\n\n throw new Error(\n [\n `Unsupported argument${unsupportedArgs.length === 1 ? '' : 's'} for ${name}: ${unsupportedArgs.join(', ')}.`,\n `Allowed arguments: ${allowedArgs.join(', ')}.`,\n ].join(' ')\n )\n}\n"],"mappings":";;;;;;;;;;;;;;AAEA,MAAa,2CAA2B,IAAI,IAAI;CAC9C;CACA;CACA;CACA;CACA;CACA;CACA;CAGA,CAAC,oBAAoB,QAAQ,CAAC,CAAC,KAAK,EAAE;CACtC,CAAC,qBAAqB,QAAQ,CAAC,CAAC,KAAK,EAAE;CACvC,CAAC,qBAAqB,UAAU,CAAC,CAAC,KAAK,EAAE;CACzC;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;AAED,MAAa,gCAA0D;CACrE,kBAAkB,CAAC,WAAW,SAAS;CACvC,aAAa,CAAC,SAAS,SAAS;CAChC,mBAAmB,CAAC,WAAW,SAAS;CACxC,aAAa,CAAC,WAAW,SAAS;AACpC;AASA,MAAa,+BAAyD;CACpE,kBAAkB;EAAC;EAAW;EAAW;EAAmB;CAAS;CAErE,aAAa;EAAC;EAAS;EAAW;CAAY;CAC9C,mBAAmB;EAAC;EAAW;EAAW;EAA6B;CAAY;CACnF,aAAa,CAAC,WAAW,SAAS;AACpC;AAEA,SAAgB,uBAAuB,MAAuB;CAC5D,OAAO,yBAAyB,IAAI,IAAI;AAC1C;;;;;;AAOA,SAAgB,cAAc,MAAoD;CAChF,MAAM,KAAK,KAAK,OAAO;CACvB,IAAI,CAAC,MAAM,OAAO,OAAO,YAAY,MAAM,QAAQ,EAAE,GAAG,OAAO;CAC/D,MAAM,aAAc,GAA+B;CACnD,OAAO,MAAM,QAAQ,UAAU,KAAK,CAAC,WAAW,SAAS,OAAO;AAClE;AAEA,SAAgB,mBACd,OACK;CACL,OAAO,MAAM,QAAQ,SAAS,CAAC,uBAAuB,KAAK,IAAI,KAAK,CAAC,cAAc,IAAI,CAAC;AAC1F;AAEA,SAAgB,wBAAwB,MAAoB;CAC1D,IAAI,CAAC,uBAAuB,IAAI,GAAG;CAenC,MAAM,IAAI,MAAM,aAAa,KAAK,qCAbhC,SAAS,iBACL,mCACA,SAAS,yBACP,4CACA,SAAS,iBACP,oCACA,SAAS,wBACP,2CACA,SAAS,YACP,iCACA,SAAS,SACP,4BACA,IACoE;AACtF;AAEA,SAAgB,mBAAmB,MAAc,KAAsB;CAKrE,IAAI,eAAe,gBAAgB,IAAI,aAAa,OAAO,mBAAmB,GAAG;CACjF,MAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;CAC/D,IAAI,4CAA4C,KAAK,OAAO,GAC1D,OAAO,qBAAqB,KAAK;CAEnC,OAAO;AACT;AAEA,SAAgB,+BAA+B,MAAc,MAAqC;CAChG,MAAM,cAAc,6BAA6B;CACjD,IAAI,CAAC,aAAa;CAElB,MAAM,kBAAkB,OAAO,KAAK,IAAI,CAAC,CAAC,QAAQ,YAAY,CAAC,YAAY,SAAS,OAAO,CAAC;CAC5F,IAAI,gBAAgB,WAAW,GAAG;CAElC,MAAM,IAAI,MACR,CACE,uBAAuB,gBAAgB,WAAW,IAAI,KAAK,IAAI,OAAO,KAAK,IAAI,gBAAgB,KAAK,IAAI,EAAE,IAC1G,sBAAsB,YAAY,KAAK,IAAI,EAAE,EAC/C,CAAC,CAAC,KAAK,GAAG,CACZ;AACF"}
|