chain-insights 0.45.0 → 0.46.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/README.md +8 -7
- package/dist/capabilities-Cur8x6c7.mjs.map +1 -1
- package/dist/mcp-proxy.cjs +31 -18
- package/dist/mcp-proxy.d.cts.map +1 -1
- package/dist/mcp-proxy.d.mts.map +1 -1
- package/dist/mcp-proxy.mjs +31 -18
- package/dist/mcp-proxy.mjs.map +1 -1
- package/dist/public-tools-CZyeptJQ.mjs.map +1 -1
- package/docs/graph-query-compatibility.md +56 -46
- package/docs/graph-tools.md +78 -34
- package/package.json +1 -1
- package/skills/chain-insights-cypher/SKILL.md +101 -40
package/README.md
CHANGED
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
[](https://securityscorecards.dev/viewer/?uri=github.com/chainswarm/chain-insights)
|
|
9
9
|
[](https://github.com/chainswarm/chain-insights/blob/main/LICENSE)
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
_Follow the funds. Find the obvious._
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
[Website](https://chain-insights.ai) | [npm](https://www.npmjs.com/package/chain-insights) | [Quickstart](#quickstart)
|
|
14
14
|
</div>
|
|
15
15
|
|
|
16
16
|
Chain Insights is open-source AML and forensics infrastructure for AI agents
|
|
@@ -181,13 +181,14 @@ Graph queries choose the read graph explicitly:
|
|
|
181
181
|
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
182
182
|
| `topology` | The unified address / FLOWS_TO / OPERATED_BY / LINKED graph — recent and full historical fund-flow traversal, and swap, liquidity-pool and bridge totals (see [Graph tools](docs/graph-tools.md#swaps-liquidity-pools-and-bridges)) |
|
|
183
183
|
| `facts` | Bounded individual `TRANSFER` rows with amount, `amount_usd`, asset, transaction, and block facts, plus single `SWAP`, `LIQUIDITY_ADD`, `LIQUIDITY_REMOVE` and `BRIDGE_CROSSING` rows |
|
|
184
|
+
| `chain` | One keyed lookup on the chain node: a transaction by `tx_id`, a block by `block_height` or `block_hash`, an address at one block (balance, nonce, kind), or the head |
|
|
184
185
|
|
|
185
186
|
One rule is worth reading before writing a query by hand: the `network`
|
|
186
|
-
argument selects the graph
|
|
187
|
-
|
|
188
|
-
on `:
|
|
189
|
-
|
|
190
|
-
|
|
187
|
+
argument selects the graph. `network` is also a property of every node and
|
|
188
|
+
relationship on every layer, and its value is the query's `network`. It is never
|
|
189
|
+
stored, except on a `:Chain` node, so a filter on it narrows nothing: anchor on
|
|
190
|
+
an address instead. A thing has one name on every layer: `address`, `tx_id`,
|
|
191
|
+
`block_height` and `block_hash`, with time in epoch milliseconds. See
|
|
191
192
|
[Graph query compatibility](docs/graph-query-compatibility.md).
|
|
192
193
|
|
|
193
194
|
Agent installs include `chain-insights-cypher` for graph-query dialect rules,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"capabilities-Cur8x6c7.mjs","names":[],"sources":["../src/mcp/capabilities.ts"],"sourcesContent":["import type { InvestigatorConfig } from '../config/schema.js'\nimport { applyMcpAuthHeaders, resolveGraphMcpEndpoint } from './client.js'\n\nexport interface NetworkTopologyLayer {\n enabled: boolean\n coverage?: {\n from_block?: number\n to_block?: number\n from_timestamp?: string\n to_timestamp?: string\n chain_tip_block?: number\n blocks_behind_tip?: number\n /** Lowest height among the raw-data lanes that have started. Above it an empty answer may mean \"not indexed yet\". */\n complete_through_block?: number\n }\n}\n\nexport interface NetworkLayerCapability {\n enabled: boolean\n live?: NetworkTopologyLayer\n archive?: NetworkTopologyLayer\n /** The relationships a facts layer serves, when the server lists them. */\n relationships?: string[]\n}\n\n/**\n * A block the server publishes about one layer's limits. The mirror repeats it\n * exactly as it came: the members named below are the ones known today, and\n * any other member the server adds is kept. Read a limit from the block, never\n * from a number written elsewhere.\n */\nexport type NetworkAdmissionBlock = Record<string, unknown>\n\n/** `USE chain`: the lookups, the ceilings and the slots of the chain layer. */\nexport interface ChainAdmission extends NetworkAdmissionBlock {\n enabled: boolean\n /** ok, or the reason the layer is off or behind. */\n status?: string\n rules_version?: string\n grammar?: string\n /** The labels of `USE chain`, such as Transaction, Block and Head. */\n lookups?: string[]\n ceiling_seconds?: Record<string, number>\n slots?: number\n past_slots?: number\n slots_per_caller?: number\n calls_per_second_per_caller?: number\n batch_max?: number\n call_gas?: number\n head_ttl_ms?: number\n max_head_age_seconds?: number\n}\n\n/** `USE topology`: the shape rules, when the server publishes them. */\nexport interface TopologyAdmission extends NetworkAdmissionBlock {\n rules_version?: string\n max_hops_per_path?: number\n max_limit?: number\n slots?: number\n}\n\n/** `USE facts`: the read contract, when the server publishes it. */\nexport interface FactsAdmission extends NetworkAdmissionBlock {\n rules_version?: string\n window_days?: number\n max_rows?: number\n max_hops?: number\n}\n\nexport interface NetworkCapability {\n network: string\n display_name?: string\n status: string\n default?: boolean\n /** The layers as the server sent them. Absent when the server sent none. */\n layers?: Record<string, NetworkLayerCapability>\n tools: Record<string, string>\n /** The three layer blocks below are repeated exactly as sent, and absent when not sent. */\n chain_admission?: ChainAdmission\n topology_admission?: TopologyAdmission\n facts_admission?: FactsAdmission\n coverage?: {\n from_block?: number\n to_block?: number\n from_timestamp?: string\n to_timestamp?: string\n chain_tip_block?: number\n blocks_behind_tip?: number\n /** Lowest height among the raw-data lanes that have started. Above it an empty answer may mean \"not indexed yet\". */\n complete_through_block?: number\n }\n freshness?: {\n last_processed_at?: string\n last_successful_sync_at?: string\n max_data_age_seconds?: number\n last_processing_duration_seconds?: number\n }\n lane_progress?: NetworkLaneProgress[]\n graph_progress?: NetworkGraphProgress\n}\n\n/** One writer lane's committed height. null: the lane has not started. */\nexport interface NetworkLaneProgress {\n lane: string\n height: number | null\n}\n\n/** How far each block layer of the topology graph reaches, and their lowest. */\nexport interface NetworkGraphProgress {\n complete_through_block: number\n layers: { layer: string; position: number }[]\n}\n\nexport interface NetworkCapabilitiesDocument {\n schema: 'chain-insights.network-capabilities.v1'\n networks: NetworkCapability[]\n}\n\nconst AVAILABLE_TOOLS_PER_LINE = 3\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\nfunction advertisedTools(raw: unknown): Record<string, string> {\n if (!isRecord(raw)) return {}\n const tools: Record<string, string> = {}\n for (const [name, status] of Object.entries(raw)) {\n if (name.trim() !== '' && typeof status === 'string') tools[name] = status\n }\n return tools\n}\n\nfunction advertisedLaneProgress(raw: unknown): NetworkLaneProgress[] | undefined {\n if (!Array.isArray(raw)) return undefined\n const lanes: NetworkLaneProgress[] = []\n for (const entry of raw) {\n if (!isRecord(entry) || typeof entry.lane !== 'string' || entry.lane.trim() === '') continue\n if (typeof entry.height === 'number') lanes.push({ lane: entry.lane, height: entry.height })\n else if (entry.height === null) lanes.push({ lane: entry.lane, height: null })\n }\n return lanes\n}\n\nfunction advertisedGraphProgress(raw: unknown): NetworkGraphProgress | undefined {\n if (!isRecord(raw) || typeof raw.complete_through_block !== 'number') return undefined\n if (!Array.isArray(raw.layers)) return undefined\n const layers: NetworkGraphProgress['layers'] = []\n for (const entry of raw.layers) {\n if (!isRecord(entry) || typeof entry.layer !== 'string' || typeof entry.position !== 'number')\n continue\n layers.push({ layer: entry.layer, position: entry.position })\n }\n return { complete_through_block: raw.complete_through_block, layers }\n}\n\nfunction advertisedNetwork(raw: unknown): NetworkCapability | null {\n if (!isRecord(raw) || typeof raw.network !== 'string' || raw.network.trim() === '') {\n return null\n }\n const network = raw.network.trim()\n const capability: NetworkCapability = {\n network,\n display_name:\n typeof raw.display_name === 'string' && raw.display_name.trim() !== ''\n ? raw.display_name\n : network,\n status: typeof raw.status === 'string' && raw.status.trim() !== '' ? raw.status : 'live',\n tools: advertisedTools(raw.tools),\n }\n // The layer blocks are the server's own words about its limits. They pass\n // through exactly as sent: nothing is added, no default is invented and no\n // member is dropped. A block the server did not send stays absent.\n if (isRecord(raw.layers)) capability.layers = raw.layers as NetworkCapability['layers']\n if (isRecord(raw.chain_admission)) {\n capability.chain_admission = raw.chain_admission as unknown as ChainAdmission\n }\n if (isRecord(raw.topology_admission)) {\n capability.topology_admission = raw.topology_admission as TopologyAdmission\n }\n if (isRecord(raw.facts_admission)) {\n capability.facts_admission = raw.facts_admission as FactsAdmission\n }\n if (raw.default === true) capability.default = true\n if (raw.default === false) capability.default = false\n if (isRecord(raw.coverage)) capability.coverage = raw.coverage as NetworkCapability['coverage']\n if (isRecord(raw.freshness))\n capability.freshness = raw.freshness as NetworkCapability['freshness']\n const laneProgress = advertisedLaneProgress(raw.lane_progress)\n if (laneProgress) capability.lane_progress = laneProgress\n const graphProgress = advertisedGraphProgress(raw.graph_progress)\n if (graphProgress) capability.graph_progress = graphProgress\n return capability\n}\n\n/** Repeat GraphRAG's network list. CIA does not add, drop, or invent names. */\nexport function mirrorGraphNetworkCapabilities(document: {\n networks: unknown[]\n}): NetworkCapabilitiesDocument {\n return {\n schema: 'chain-insights.network-capabilities.v1',\n networks: document.networks\n .map((network) => advertisedNetwork(network))\n .filter((network): network is NetworkCapability => network !== null),\n }\n}\n\nfunction publicNetworkCapabilities(\n document: NetworkCapabilitiesDocument\n): NetworkCapabilitiesDocument {\n return mirrorGraphNetworkCapabilities(document)\n}\n\nfunction metadataNetworksUrl(endpoint: string): URL {\n const url = new URL(endpoint)\n url.pathname = '/metadata/networks'\n url.search = ''\n url.hash = ''\n return url\n}\n\nexport async function fetchNetworkCapabilities(\n config: Pick<InvestigatorConfig, 'graphMcpAuthToken' | 'graphMcpMode' | 'graphMcpEndpoint'>\n): Promise<NetworkCapabilitiesDocument> {\n const endpoint = resolveGraphMcpEndpoint(config)\n const request = metadataNetworksUrl(endpoint)\n const headers = new Headers()\n const token = config.graphMcpAuthToken?.trim()\n if (token) {\n applyMcpAuthHeaders(headers, token)\n }\n let response: Response\n try {\n response = await fetch(request, { headers })\n } catch (err) {\n throw new Error(`network capabilities unavailable at ${request}: ${(err as Error).message}`)\n }\n if (!response.ok) {\n throw new Error(`network capabilities unavailable at ${request}: HTTP ${response.status}`)\n }\n const parsed = (await response.json()) as NetworkCapabilitiesDocument\n if (\n parsed.schema !== 'chain-insights.network-capabilities.v1' ||\n !Array.isArray(parsed.networks)\n ) {\n throw new Error('network capabilities response has unsupported schema')\n }\n return publicNetworkCapabilities(parsed)\n}\n\nfunction availableTools(network: NetworkCapability): string[] {\n const tools = Object.entries(network.tools ?? {})\n .filter(([, status]) => status === 'available')\n .map(([name]) => name)\n return tools.sort()\n}\n\nfunction availableToolLines(network: NetworkCapability): string[] {\n const tools = availableTools(network)\n if (tools.length === 0) return ['none']\n const lines: string[] = []\n for (let index = 0; index < tools.length; index += AVAILABLE_TOOLS_PER_LINE) {\n lines.push(tools.slice(index, index + AVAILABLE_TOOLS_PER_LINE).join(', '))\n }\n return lines\n}\n\nfunction shortDate(value?: string): string {\n if (!value) return ''\n return value.slice(0, 10)\n}\n\nfunction datasetLabel(network: NetworkCapability): string {\n const coverage = network.coverage\n if (!coverage) return 'unknown'\n const blockRange =\n coverage.from_block !== undefined && coverage.to_block !== undefined\n ? `${coverage.from_block}..${coverage.to_block}`\n : 'blocks unknown'\n const dateRange =\n coverage.from_timestamp && coverage.to_timestamp\n ? `${shortDate(coverage.from_timestamp)}..${shortDate(coverage.to_timestamp)}`\n : ''\n if (blockRange === 'blocks unknown' && dateRange === '') return 'unknown'\n if (blockRange === 'blocks unknown') return dateRange\n if (dateRange === '') return blockRange\n return `${blockRange} / ${dateRange}`\n}\n\nfunction displayName(network: NetworkCapability): string {\n return network.display_name || network.network\n}\n\nfunction statusLabel(network: NetworkCapability): string {\n return network.default ? `${network.status} (default)` : network.status\n}\n\nfunction formatTable(headers: string[], rows: string[][], minimumWidths: number[]): string {\n const widths = headers.map((header, index) =>\n Math.max(\n minimumWidths[index] ?? 0,\n header.length,\n ...rows.map((row) => row[index]?.length ?? 0)\n )\n )\n const row = (values: string[]) =>\n values.map((value, index) => value.padEnd(widths[index]!)).join(' ')\n return [row(headers), widths.map((width) => '-'.repeat(width)).join(' '), ...rows.map(row)].join(\n '\\n'\n )\n}\n\nexport function formatNetworkOverview(document: NetworkCapabilitiesDocument): string {\n if (document.networks.length === 0) return 'No supported networks advertised.'\n const rows = document.networks.map((network) => [\n displayName(network),\n statusLabel(network),\n datasetLabel(network),\n ])\n return formatTable(['Network', 'Status', 'Dataset'], rows, [14, 10, 38])\n}\n\nexport function formatNetworkCapabilities(document: NetworkCapabilitiesDocument): string {\n if (document.networks.length === 0) return 'No supported networks advertised.'\n const networkRows = document.networks.flatMap((network) => {\n const toolLines = availableToolLines(network)\n return toolLines.map((toolLine, index) => [\n index === 0 ? displayName(network) : '',\n index === 0 ? datasetLabel(network) : '',\n toolLine,\n ])\n })\n return formatTable(['Network', 'Dataset', 'Chain Insights tools'], networkRows, [14, 38, 64])\n}\n\nexport function findNetworkCapability(\n document: NetworkCapabilitiesDocument,\n name: string\n): NetworkCapability | undefined {\n const normalizedName = name.trim().toLowerCase()\n if (!normalizedName) return undefined\n return document.networks.find((network) =>\n [network.network, network.display_name].some(\n (candidate) => candidate?.trim().toLowerCase() === normalizedName\n )\n )\n}\n\nexport function formatNetworkCapability(network: NetworkCapability): string {\n const rows = [\n ['Network', network.display_name || network.network],\n ['Identifier', network.network],\n ['Status', network.default ? `${network.status} (default)` : network.status],\n ['Dataset', datasetLabel(network)],\n ['Available tools', availableTools(network).join(', ') || 'none'],\n ]\n const labelWidth = Math.max(...rows.map(([label]) => label.length))\n return rows.map(([label, value]) => `${label.padEnd(labelWidth)} ${value}`).join('\\n')\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"capabilities-Cur8x6c7.mjs","names":[],"sources":["../src/mcp/capabilities.ts"],"sourcesContent":["import type { InvestigatorConfig } from '../config/schema.js'\nimport { applyMcpAuthHeaders, resolveGraphMcpEndpoint } from './client.js'\n\nexport interface NetworkTopologyLayer {\n enabled: boolean\n coverage?: {\n from_block?: number\n to_block?: number\n from_timestamp?: string\n to_timestamp?: string\n chain_tip_block?: number\n blocks_behind_tip?: number\n /** Lowest height among the raw-data lanes that have started. Above it an empty answer may mean \"not indexed yet\". */\n complete_through_block?: number\n }\n}\n\nexport interface NetworkLayerCapability {\n enabled: boolean\n live?: NetworkTopologyLayer\n archive?: NetworkTopologyLayer\n /** The relationships a facts layer serves, when the server lists them. */\n relationships?: string[]\n}\n\n/**\n * A block the server publishes about one layer's limits. The mirror repeats it\n * exactly as it came: the members named below are the ones known today, and\n * any other member the server adds is kept. Read a limit from the block, never\n * from a number written elsewhere.\n */\nexport type NetworkAdmissionBlock = Record<string, unknown>\n\n/** `USE chain`: the lookups, the ceilings and the slots of the chain layer. */\nexport interface ChainAdmission extends NetworkAdmissionBlock {\n enabled: boolean\n /** ok, or the reason the layer is off or behind. */\n status?: string\n rules_version?: string\n grammar?: string\n /** The labels of `USE chain`, such as Transaction, Block, Head and Address. */\n lookups?: string[]\n ceiling_seconds?: Record<string, number>\n slots?: number\n past_slots?: number\n slots_per_caller?: number\n calls_per_second_per_caller?: number\n batch_max?: number\n call_gas?: number\n head_ttl_ms?: number\n max_head_age_seconds?: number\n /** How many blocks below the tip a past `Address` read (`at_block`) must be. Read it here, never write it down. */\n at_block_min_depth?: number\n}\n\n/** `USE topology`: the shape rules, when the server publishes them. */\nexport interface TopologyAdmission extends NetworkAdmissionBlock {\n rules_version?: string\n max_hops_per_path?: number\n max_limit?: number\n slots?: number\n}\n\n/** `USE facts`: the read contract, when the server publishes it. */\nexport interface FactsAdmission extends NetworkAdmissionBlock {\n rules_version?: string\n window_days?: number\n max_rows?: number\n max_hops?: number\n}\n\nexport interface NetworkCapability {\n network: string\n display_name?: string\n status: string\n default?: boolean\n /** The layers as the server sent them. Absent when the server sent none. */\n layers?: Record<string, NetworkLayerCapability>\n tools: Record<string, string>\n /** The three layer blocks below are repeated exactly as sent, and absent when not sent. */\n chain_admission?: ChainAdmission\n topology_admission?: TopologyAdmission\n facts_admission?: FactsAdmission\n coverage?: {\n from_block?: number\n to_block?: number\n from_timestamp?: string\n to_timestamp?: string\n chain_tip_block?: number\n blocks_behind_tip?: number\n /** Lowest height among the raw-data lanes that have started. Above it an empty answer may mean \"not indexed yet\". */\n complete_through_block?: number\n }\n freshness?: {\n last_processed_at?: string\n last_successful_sync_at?: string\n max_data_age_seconds?: number\n last_processing_duration_seconds?: number\n }\n lane_progress?: NetworkLaneProgress[]\n graph_progress?: NetworkGraphProgress\n}\n\n/** One writer lane's committed height. null: the lane has not started. */\nexport interface NetworkLaneProgress {\n lane: string\n height: number | null\n}\n\n/** How far each block layer of the topology graph reaches, and their lowest. */\nexport interface NetworkGraphProgress {\n complete_through_block: number\n layers: { layer: string; position: number }[]\n}\n\nexport interface NetworkCapabilitiesDocument {\n schema: 'chain-insights.network-capabilities.v1'\n networks: NetworkCapability[]\n}\n\nconst AVAILABLE_TOOLS_PER_LINE = 3\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\nfunction advertisedTools(raw: unknown): Record<string, string> {\n if (!isRecord(raw)) return {}\n const tools: Record<string, string> = {}\n for (const [name, status] of Object.entries(raw)) {\n if (name.trim() !== '' && typeof status === 'string') tools[name] = status\n }\n return tools\n}\n\nfunction advertisedLaneProgress(raw: unknown): NetworkLaneProgress[] | undefined {\n if (!Array.isArray(raw)) return undefined\n const lanes: NetworkLaneProgress[] = []\n for (const entry of raw) {\n if (!isRecord(entry) || typeof entry.lane !== 'string' || entry.lane.trim() === '') continue\n if (typeof entry.height === 'number') lanes.push({ lane: entry.lane, height: entry.height })\n else if (entry.height === null) lanes.push({ lane: entry.lane, height: null })\n }\n return lanes\n}\n\nfunction advertisedGraphProgress(raw: unknown): NetworkGraphProgress | undefined {\n if (!isRecord(raw) || typeof raw.complete_through_block !== 'number') return undefined\n if (!Array.isArray(raw.layers)) return undefined\n const layers: NetworkGraphProgress['layers'] = []\n for (const entry of raw.layers) {\n if (!isRecord(entry) || typeof entry.layer !== 'string' || typeof entry.position !== 'number')\n continue\n layers.push({ layer: entry.layer, position: entry.position })\n }\n return { complete_through_block: raw.complete_through_block, layers }\n}\n\nfunction advertisedNetwork(raw: unknown): NetworkCapability | null {\n if (!isRecord(raw) || typeof raw.network !== 'string' || raw.network.trim() === '') {\n return null\n }\n const network = raw.network.trim()\n const capability: NetworkCapability = {\n network,\n display_name:\n typeof raw.display_name === 'string' && raw.display_name.trim() !== ''\n ? raw.display_name\n : network,\n status: typeof raw.status === 'string' && raw.status.trim() !== '' ? raw.status : 'live',\n tools: advertisedTools(raw.tools),\n }\n // The layer blocks are the server's own words about its limits. They pass\n // through exactly as sent: nothing is added, no default is invented and no\n // member is dropped. A block the server did not send stays absent.\n if (isRecord(raw.layers)) capability.layers = raw.layers as NetworkCapability['layers']\n if (isRecord(raw.chain_admission)) {\n capability.chain_admission = raw.chain_admission as unknown as ChainAdmission\n }\n if (isRecord(raw.topology_admission)) {\n capability.topology_admission = raw.topology_admission as TopologyAdmission\n }\n if (isRecord(raw.facts_admission)) {\n capability.facts_admission = raw.facts_admission as FactsAdmission\n }\n if (raw.default === true) capability.default = true\n if (raw.default === false) capability.default = false\n if (isRecord(raw.coverage)) capability.coverage = raw.coverage as NetworkCapability['coverage']\n if (isRecord(raw.freshness))\n capability.freshness = raw.freshness as NetworkCapability['freshness']\n const laneProgress = advertisedLaneProgress(raw.lane_progress)\n if (laneProgress) capability.lane_progress = laneProgress\n const graphProgress = advertisedGraphProgress(raw.graph_progress)\n if (graphProgress) capability.graph_progress = graphProgress\n return capability\n}\n\n/** Repeat GraphRAG's network list. CIA does not add, drop, or invent names. */\nexport function mirrorGraphNetworkCapabilities(document: {\n networks: unknown[]\n}): NetworkCapabilitiesDocument {\n return {\n schema: 'chain-insights.network-capabilities.v1',\n networks: document.networks\n .map((network) => advertisedNetwork(network))\n .filter((network): network is NetworkCapability => network !== null),\n }\n}\n\nfunction publicNetworkCapabilities(\n document: NetworkCapabilitiesDocument\n): NetworkCapabilitiesDocument {\n return mirrorGraphNetworkCapabilities(document)\n}\n\nfunction metadataNetworksUrl(endpoint: string): URL {\n const url = new URL(endpoint)\n url.pathname = '/metadata/networks'\n url.search = ''\n url.hash = ''\n return url\n}\n\nexport async function fetchNetworkCapabilities(\n config: Pick<InvestigatorConfig, 'graphMcpAuthToken' | 'graphMcpMode' | 'graphMcpEndpoint'>\n): Promise<NetworkCapabilitiesDocument> {\n const endpoint = resolveGraphMcpEndpoint(config)\n const request = metadataNetworksUrl(endpoint)\n const headers = new Headers()\n const token = config.graphMcpAuthToken?.trim()\n if (token) {\n applyMcpAuthHeaders(headers, token)\n }\n let response: Response\n try {\n response = await fetch(request, { headers })\n } catch (err) {\n throw new Error(`network capabilities unavailable at ${request}: ${(err as Error).message}`)\n }\n if (!response.ok) {\n throw new Error(`network capabilities unavailable at ${request}: HTTP ${response.status}`)\n }\n const parsed = (await response.json()) as NetworkCapabilitiesDocument\n if (\n parsed.schema !== 'chain-insights.network-capabilities.v1' ||\n !Array.isArray(parsed.networks)\n ) {\n throw new Error('network capabilities response has unsupported schema')\n }\n return publicNetworkCapabilities(parsed)\n}\n\nfunction availableTools(network: NetworkCapability): string[] {\n const tools = Object.entries(network.tools ?? {})\n .filter(([, status]) => status === 'available')\n .map(([name]) => name)\n return tools.sort()\n}\n\nfunction availableToolLines(network: NetworkCapability): string[] {\n const tools = availableTools(network)\n if (tools.length === 0) return ['none']\n const lines: string[] = []\n for (let index = 0; index < tools.length; index += AVAILABLE_TOOLS_PER_LINE) {\n lines.push(tools.slice(index, index + AVAILABLE_TOOLS_PER_LINE).join(', '))\n }\n return lines\n}\n\nfunction shortDate(value?: string): string {\n if (!value) return ''\n return value.slice(0, 10)\n}\n\nfunction datasetLabel(network: NetworkCapability): string {\n const coverage = network.coverage\n if (!coverage) return 'unknown'\n const blockRange =\n coverage.from_block !== undefined && coverage.to_block !== undefined\n ? `${coverage.from_block}..${coverage.to_block}`\n : 'blocks unknown'\n const dateRange =\n coverage.from_timestamp && coverage.to_timestamp\n ? `${shortDate(coverage.from_timestamp)}..${shortDate(coverage.to_timestamp)}`\n : ''\n if (blockRange === 'blocks unknown' && dateRange === '') return 'unknown'\n if (blockRange === 'blocks unknown') return dateRange\n if (dateRange === '') return blockRange\n return `${blockRange} / ${dateRange}`\n}\n\nfunction displayName(network: NetworkCapability): string {\n return network.display_name || network.network\n}\n\nfunction statusLabel(network: NetworkCapability): string {\n return network.default ? `${network.status} (default)` : network.status\n}\n\nfunction formatTable(headers: string[], rows: string[][], minimumWidths: number[]): string {\n const widths = headers.map((header, index) =>\n Math.max(\n minimumWidths[index] ?? 0,\n header.length,\n ...rows.map((row) => row[index]?.length ?? 0)\n )\n )\n const row = (values: string[]) =>\n values.map((value, index) => value.padEnd(widths[index]!)).join(' ')\n return [row(headers), widths.map((width) => '-'.repeat(width)).join(' '), ...rows.map(row)].join(\n '\\n'\n )\n}\n\nexport function formatNetworkOverview(document: NetworkCapabilitiesDocument): string {\n if (document.networks.length === 0) return 'No supported networks advertised.'\n const rows = document.networks.map((network) => [\n displayName(network),\n statusLabel(network),\n datasetLabel(network),\n ])\n return formatTable(['Network', 'Status', 'Dataset'], rows, [14, 10, 38])\n}\n\nexport function formatNetworkCapabilities(document: NetworkCapabilitiesDocument): string {\n if (document.networks.length === 0) return 'No supported networks advertised.'\n const networkRows = document.networks.flatMap((network) => {\n const toolLines = availableToolLines(network)\n return toolLines.map((toolLine, index) => [\n index === 0 ? displayName(network) : '',\n index === 0 ? datasetLabel(network) : '',\n toolLine,\n ])\n })\n return formatTable(['Network', 'Dataset', 'Chain Insights tools'], networkRows, [14, 38, 64])\n}\n\nexport function findNetworkCapability(\n document: NetworkCapabilitiesDocument,\n name: string\n): NetworkCapability | undefined {\n const normalizedName = name.trim().toLowerCase()\n if (!normalizedName) return undefined\n return document.networks.find((network) =>\n [network.network, network.display_name].some(\n (candidate) => candidate?.trim().toLowerCase() === normalizedName\n )\n )\n}\n\nexport function formatNetworkCapability(network: NetworkCapability): string {\n const rows = [\n ['Network', network.display_name || network.network],\n ['Identifier', network.network],\n ['Status', network.default ? `${network.status} (default)` : network.status],\n ['Dataset', datasetLabel(network)],\n ['Available tools', availableTools(network).join(', ') || 'none'],\n ]\n const labelWidth = Math.max(...rows.map(([label]) => label.length))\n return rows.map(([label, value]) => `${label.padEnd(labelWidth)} ${value}`).join('\\n')\n}\n"],"mappings":";;;;;;;;;;;AAwHA,MAAM,2BAA2B;AAEjC,SAAS,SAAS,OAAkD;CAClE,OAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC5E;AAEA,SAAS,gBAAgB,KAAsC;CAC7D,IAAI,CAAC,SAAS,GAAG,GAAG,OAAO,CAAC;CAC5B,MAAM,QAAgC,CAAC;CACvC,KAAK,MAAM,CAAC,MAAM,WAAW,OAAO,QAAQ,GAAG,GAC7C,IAAI,KAAK,KAAK,MAAM,MAAM,OAAO,WAAW,UAAU,MAAM,QAAQ;CAEtE,OAAO;AACT;AAEA,SAAS,uBAAuB,KAAiD;CAC/E,IAAI,CAAC,MAAM,QAAQ,GAAG,GAAG,OAAO,KAAA;CAChC,MAAM,QAA+B,CAAC;CACtC,KAAK,MAAM,SAAS,KAAK;EACvB,IAAI,CAAC,SAAS,KAAK,KAAK,OAAO,MAAM,SAAS,YAAY,MAAM,KAAK,KAAK,MAAM,IAAI;EACpF,IAAI,OAAO,MAAM,WAAW,UAAU,MAAM,KAAK;GAAE,MAAM,MAAM;GAAM,QAAQ,MAAM;EAAO,CAAC;OACtF,IAAI,MAAM,WAAW,MAAM,MAAM,KAAK;GAAE,MAAM,MAAM;GAAM,QAAQ;EAAK,CAAC;CAC/E;CACA,OAAO;AACT;AAEA,SAAS,wBAAwB,KAAgD;CAC/E,IAAI,CAAC,SAAS,GAAG,KAAK,OAAO,IAAI,2BAA2B,UAAU,OAAO,KAAA;CAC7E,IAAI,CAAC,MAAM,QAAQ,IAAI,MAAM,GAAG,OAAO,KAAA;CACvC,MAAM,SAAyC,CAAC;CAChD,KAAK,MAAM,SAAS,IAAI,QAAQ;EAC9B,IAAI,CAAC,SAAS,KAAK,KAAK,OAAO,MAAM,UAAU,YAAY,OAAO,MAAM,aAAa,UACnF;EACF,OAAO,KAAK;GAAE,OAAO,MAAM;GAAO,UAAU,MAAM;EAAS,CAAC;CAC9D;CACA,OAAO;EAAE,wBAAwB,IAAI;EAAwB;CAAO;AACtE;AAEA,SAAS,kBAAkB,KAAwC;CACjE,IAAI,CAAC,SAAS,GAAG,KAAK,OAAO,IAAI,YAAY,YAAY,IAAI,QAAQ,KAAK,MAAM,IAC9E,OAAO;CAET,MAAM,UAAU,IAAI,QAAQ,KAAK;CACjC,MAAM,aAAgC;EACpC;EACA,cACE,OAAO,IAAI,iBAAiB,YAAY,IAAI,aAAa,KAAK,MAAM,KAChE,IAAI,eACJ;EACN,QAAQ,OAAO,IAAI,WAAW,YAAY,IAAI,OAAO,KAAK,MAAM,KAAK,IAAI,SAAS;EAClF,OAAO,gBAAgB,IAAI,KAAK;CAClC;CAIA,IAAI,SAAS,IAAI,MAAM,GAAG,WAAW,SAAS,IAAI;CAClD,IAAI,SAAS,IAAI,eAAe,GAC9B,WAAW,kBAAkB,IAAI;CAEnC,IAAI,SAAS,IAAI,kBAAkB,GACjC,WAAW,qBAAqB,IAAI;CAEtC,IAAI,SAAS,IAAI,eAAe,GAC9B,WAAW,kBAAkB,IAAI;CAEnC,IAAI,IAAI,YAAY,MAAM,WAAW,UAAU;CAC/C,IAAI,IAAI,YAAY,OAAO,WAAW,UAAU;CAChD,IAAI,SAAS,IAAI,QAAQ,GAAG,WAAW,WAAW,IAAI;CACtD,IAAI,SAAS,IAAI,SAAS,GACxB,WAAW,YAAY,IAAI;CAC7B,MAAM,eAAe,uBAAuB,IAAI,aAAa;CAC7D,IAAI,cAAc,WAAW,gBAAgB;CAC7C,MAAM,gBAAgB,wBAAwB,IAAI,cAAc;CAChE,IAAI,eAAe,WAAW,iBAAiB;CAC/C,OAAO;AACT;;AAGA,SAAgB,+BAA+B,UAEf;CAC9B,OAAO;EACL,QAAQ;EACR,UAAU,SAAS,SAChB,KAAK,YAAY,kBAAkB,OAAO,CAAC,CAAC,CAC5C,QAAQ,YAA0C,YAAY,IAAI;CACvE;AACF;AAEA,SAAS,0BACP,UAC6B;CAC7B,OAAO,+BAA+B,QAAQ;AAChD;AAEA,SAAS,oBAAoB,UAAuB;CAClD,MAAM,MAAM,IAAI,IAAI,QAAQ;CAC5B,IAAI,WAAW;CACf,IAAI,SAAS;CACb,IAAI,OAAO;CACX,OAAO;AACT;AAEA,eAAsB,yBACpB,QACsC;CAEtC,MAAM,UAAU,oBADC,wBAAwB,MACE,CAAC;CAC5C,MAAM,UAAU,IAAI,QAAQ;CAC5B,MAAM,QAAQ,OAAO,mBAAmB,KAAK;CAC7C,IAAI,OACF,oBAAoB,SAAS,KAAK;CAEpC,IAAI;CACJ,IAAI;EACF,WAAW,MAAM,MAAM,SAAS,EAAE,QAAQ,CAAC;CAC7C,SAAS,KAAK;EACZ,MAAM,IAAI,MAAM,uCAAuC,QAAQ,IAAK,IAAc,SAAS;CAC7F;CACA,IAAI,CAAC,SAAS,IACZ,MAAM,IAAI,MAAM,uCAAuC,QAAQ,SAAS,SAAS,QAAQ;CAE3F,MAAM,SAAU,MAAM,SAAS,KAAK;CACpC,IACE,OAAO,WAAW,4CAClB,CAAC,MAAM,QAAQ,OAAO,QAAQ,GAE9B,MAAM,IAAI,MAAM,sDAAsD;CAExE,OAAO,0BAA0B,MAAM;AACzC;AAEA,SAAS,eAAe,SAAsC;CAI5D,OAHc,OAAO,QAAQ,QAAQ,SAAS,CAAC,CAAC,CAAC,CAC9C,QAAQ,GAAG,YAAY,WAAW,WAAW,CAAC,CAC9C,KAAK,CAAC,UAAU,IACR,CAAC,CAAC,KAAK;AACpB;AAEA,SAAS,mBAAmB,SAAsC;CAChE,MAAM,QAAQ,eAAe,OAAO;CACpC,IAAI,MAAM,WAAW,GAAG,OAAO,CAAC,MAAM;CACtC,MAAM,QAAkB,CAAC;CACzB,KAAK,IAAI,QAAQ,GAAG,QAAQ,MAAM,QAAQ,SAAS,0BACjD,MAAM,KAAK,MAAM,MAAM,OAAO,QAAQ,wBAAwB,CAAC,CAAC,KAAK,IAAI,CAAC;CAE5E,OAAO;AACT;AAEA,SAAS,UAAU,OAAwB;CACzC,IAAI,CAAC,OAAO,OAAO;CACnB,OAAO,MAAM,MAAM,GAAG,EAAE;AAC1B;AAEA,SAAS,aAAa,SAAoC;CACxD,MAAM,WAAW,QAAQ;CACzB,IAAI,CAAC,UAAU,OAAO;CACtB,MAAM,aACJ,SAAS,eAAe,KAAA,KAAa,SAAS,aAAa,KAAA,IACvD,GAAG,SAAS,WAAW,IAAI,SAAS,aACpC;CACN,MAAM,YACJ,SAAS,kBAAkB,SAAS,eAChC,GAAG,UAAU,SAAS,cAAc,EAAE,IAAI,UAAU,SAAS,YAAY,MACzE;CACN,IAAI,eAAe,oBAAoB,cAAc,IAAI,OAAO;CAChE,IAAI,eAAe,kBAAkB,OAAO;CAC5C,IAAI,cAAc,IAAI,OAAO;CAC7B,OAAO,GAAG,WAAW,KAAK;AAC5B;AAEA,SAAS,YAAY,SAAoC;CACvD,OAAO,QAAQ,gBAAgB,QAAQ;AACzC;AAEA,SAAS,YAAY,SAAoC;CACvD,OAAO,QAAQ,UAAU,GAAG,QAAQ,OAAO,cAAc,QAAQ;AACnE;AAEA,SAAS,YAAY,SAAmB,MAAkB,eAAiC;CACzF,MAAM,SAAS,QAAQ,KAAK,QAAQ,UAClC,KAAK,IACH,cAAc,UAAU,GACxB,OAAO,QACP,GAAG,KAAK,KAAK,QAAQ,IAAI,MAAM,EAAE,UAAU,CAAC,CAC9C,CACF;CACA,MAAM,OAAO,WACX,OAAO,KAAK,OAAO,UAAU,MAAM,OAAO,OAAO,MAAO,CAAC,CAAC,CAAC,KAAK,IAAI;CACtE,OAAO;EAAC,IAAI,OAAO;EAAG,OAAO,KAAK,UAAU,IAAI,OAAO,KAAK,CAAC,CAAC,CAAC,KAAK,IAAI;EAAG,GAAG,KAAK,IAAI,GAAG;CAAC,CAAC,CAAC,KAC3F,IACF;AACF;AAEA,SAAgB,sBAAsB,UAA+C;CACnF,IAAI,SAAS,SAAS,WAAW,GAAG,OAAO;CAM3C,OAAO,YAAY;EAAC;EAAW;EAAU;CAAS,GALrC,SAAS,SAAS,KAAK,YAAY;EAC9C,YAAY,OAAO;EACnB,YAAY,OAAO;EACnB,aAAa,OAAO;CACtB,CACwD,GAAG;EAAC;EAAI;EAAI;CAAE,CAAC;AACzE;AAEA,SAAgB,0BAA0B,UAA+C;CACvF,IAAI,SAAS,SAAS,WAAW,GAAG,OAAO;CAS3C,OAAO,YAAY;EAAC;EAAW;EAAW;CAAsB,GAR5C,SAAS,SAAS,SAAS,YAAY;EAEzD,OADkB,mBAAmB,OACtB,CAAC,CAAC,KAAK,UAAU,UAAU;GACxC,UAAU,IAAI,YAAY,OAAO,IAAI;GACrC,UAAU,IAAI,aAAa,OAAO,IAAI;GACtC;EACF,CAAC;CACH,CAC6E,GAAG;EAAC;EAAI;EAAI;CAAE,CAAC;AAC9F;AAEA,SAAgB,sBACd,UACA,MAC+B;CAC/B,MAAM,iBAAiB,KAAK,KAAK,CAAC,CAAC,YAAY;CAC/C,IAAI,CAAC,gBAAgB,OAAO,KAAA;CAC5B,OAAO,SAAS,SAAS,MAAM,YAC7B,CAAC,QAAQ,SAAS,QAAQ,YAAY,CAAC,CAAC,MACrC,cAAc,WAAW,KAAK,CAAC,CAAC,YAAY,MAAM,cACrD,CACF;AACF;AAEA,SAAgB,wBAAwB,SAAoC;CAC1E,MAAM,OAAO;EACX,CAAC,WAAW,QAAQ,gBAAgB,QAAQ,OAAO;EACnD,CAAC,cAAc,QAAQ,OAAO;EAC9B,CAAC,UAAU,QAAQ,UAAU,GAAG,QAAQ,OAAO,cAAc,QAAQ,MAAM;EAC3E,CAAC,WAAW,aAAa,OAAO,CAAC;EACjC,CAAC,mBAAmB,eAAe,OAAO,CAAC,CAAC,KAAK,IAAI,KAAK,MAAM;CAClE;CACA,MAAM,aAAa,KAAK,IAAI,GAAG,KAAK,KAAK,CAAC,WAAW,MAAM,MAAM,CAAC;CAClE,OAAO,KAAK,KAAK,CAAC,OAAO,WAAW,GAAG,MAAM,OAAO,UAAU,EAAE,IAAI,OAAO,CAAC,CAAC,KAAK,IAAI;AACxF"}
|
package/dist/mcp-proxy.cjs
CHANGED
|
@@ -21,21 +21,21 @@ let _modelcontextprotocol_sdk_client_index_js = require("@modelcontextprotocol/s
|
|
|
21
21
|
let _modelcontextprotocol_sdk_client_streamableHttp_js = require("@modelcontextprotocol/sdk/client/streamableHttp.js");
|
|
22
22
|
//#region src/mcp/layer-routing.ts
|
|
23
23
|
/** The routing rule, in one sentence. The server states it in the same words. */
|
|
24
|
-
const ROUTING_RULE = "Route by what
|
|
24
|
+
const ROUTING_RULE = "Route by what the question names: an address, a pair with a day, a transaction or a block. Topology searches outward from an address, while facts and chain look up one known thing.";
|
|
25
25
|
/** The rule in three lines, one for each layer. */
|
|
26
26
|
const ROUTING_LINES = [
|
|
27
|
-
"I
|
|
28
|
-
"I know
|
|
29
|
-
"I want the chain's own record of a transaction by its
|
|
27
|
+
"I know an address and want its links, senders, receivers, hops or a route: `USE topology`, anchored on that address.",
|
|
28
|
+
"I know both addresses of a pair and one day, and want the rows of that day: `USE facts`.",
|
|
29
|
+
"I want the chain's own record of a transaction by its `tx_id`, a block by its `block_height` or `block_hash`, an address at one block (`Address`), or the head: `USE chain`."
|
|
30
30
|
];
|
|
31
|
-
/** The chain layer
|
|
32
|
-
const
|
|
31
|
+
/** The chain layer reads one address at one block. A search for counterparties stays on topology. */
|
|
32
|
+
const ROUTING_ADDRESS_ON_CHAIN = "`USE chain` with `Address` reads the balance, the nonce or the kind of one address, now or at a past block with `at_block`. A past block must be at least `chain_admission.at_block_min_depth` blocks below the tip. To find the counterparties of an address, search `USE topology`, then read the pair and one day on `USE facts`.";
|
|
33
33
|
/** What to do when two layers fit. */
|
|
34
|
-
const ROUTING_TWO_FIT = "
|
|
35
|
-
/** The key that each layer hands to the next. */
|
|
36
|
-
const ROUTING_HANDOFF = "The layers hand each other keys. Topology gives the pair and the first and last seen time.
|
|
37
|
-
/** A question that no layer serves. */
|
|
38
|
-
const ROUTING_NOT_SERVED = "A list, a range or a whole-chain question
|
|
34
|
+
const ROUTING_TWO_FIT = "A transaction is a chain question: read it on `USE chain` by its `tx_id`. Chain gives its `block_date`; the transfers between a pair on that day are a `USE facts` read.";
|
|
35
|
+
/** The key that each layer hands to the next. Each key keeps its name. */
|
|
36
|
+
const ROUTING_HANDOFF = "The layers hand each other keys, and each key keeps its name. Topology gives the pair and the first and last seen time. Facts gives the `tx_id` and the `block_height`. Chain takes an `address`, a `tx_id`, a `block_height` or a `block_hash`, and gives the `block_date` of a transaction or a block.";
|
|
37
|
+
/** A question that no layer serves today. The agent says so once and asks for an anchor. */
|
|
38
|
+
const ROUTING_NOT_SERVED = "A list, a range or a whole-chain question names no address, pair, transaction or block, so no layer serves it today. Say so in one line, ask the user for one of those, and send no workaround query.";
|
|
39
39
|
/** What a refusal carries, and which part of it decides the move. */
|
|
40
40
|
const MOVE_INTRO = "A refused, killed, busy or failed query comes back with `error_detail`: `code`, `rule`, `class`, `fix` and `example`. The `class` decides your next move.";
|
|
41
41
|
/** The move for each class of the refusal envelope. */
|
|
@@ -68,11 +68,20 @@ function moveLines() {
|
|
|
68
68
|
];
|
|
69
69
|
}
|
|
70
70
|
/**
|
|
71
|
+
* The routing rule and its three lines, with the question that no layer serves,
|
|
72
|
+
* as one paragraph. The proxy serves it first among its instructions: a host
|
|
73
|
+
* that keeps only the start of the instructions (Claude Code keeps 2 KB) still
|
|
74
|
+
* shows the agent where each kind of question goes, and which kind goes nowhere.
|
|
75
|
+
*/
|
|
76
|
+
function routingHead() {
|
|
77
|
+
return `Pick the layer first. ${ROUTING_RULE} 1. ${ROUTING_LINES[0]} 2. ${ROUTING_LINES[1]} 3. ${ROUTING_LINES[2]} ${ROUTING_NOT_SERVED}`;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
71
80
|
* The two hints that the MCP proxy serves for routing and for refusals. Each is
|
|
72
81
|
* one line of the served instructions.
|
|
73
82
|
*/
|
|
74
83
|
function routingHintLines() {
|
|
75
|
-
return [`- Pick the layer first. ${ROUTING_RULE} 1. ${ROUTING_LINES[0]} 2. ${ROUTING_LINES[1]} 3. ${ROUTING_LINES[2]} ${
|
|
84
|
+
return [`- Pick the layer first. ${ROUTING_RULE} 1. ${ROUTING_LINES[0]} 2. ${ROUTING_LINES[1]} 3. ${ROUTING_LINES[2]} ${ROUTING_NOT_SERVED} ${ROUTING_ADDRESS_ON_CHAIN} ${ROUTING_TWO_FIT} ${ROUTING_HANDOFF} The lookups that USE chain serves are listed in chain_admission.lookups of meta_network_capabilities. Read every limit that the server publishes from meta_network_capabilities (chain_admission and, when the server sends them, topology_admission and facts_admission). Never write a limit down.`, `- Act on a refusal by its class. ${moveLines().join(" ")}`];
|
|
76
85
|
}
|
|
77
86
|
//#endregion
|
|
78
87
|
//#region src/mcp/proxy.ts
|
|
@@ -131,7 +140,7 @@ function resolveMcpProxyMode(env = process.env) {
|
|
|
131
140
|
if (raw === "no-workspace" || raw === "workspace-less") return "stateless";
|
|
132
141
|
throw new Error(`CHAIN_INSIGHTS_MCP_PROXY_MODE must be workspace or stateless; got "${raw}"`);
|
|
133
142
|
}
|
|
134
|
-
const GRAPH_LAYERS_TEXT = "Use USE topology for topology (address/FLOWS_TO/OPERATED_BY/LINKED graph with SWAPPED, ADDED_LIQUIDITY, REMOVED_LIQUIDITY, BRIDGED and the Pool label, unified recent+historical) and USE facts for bounded TRANSFER, SWAP, LIQUIDITY_ADD, LIQUIDITY_REMOVE and BRIDGE_CROSSING rows and enrichment, and USE chain for the chain node's own record of one known transaction, block or the head.";
|
|
143
|
+
const GRAPH_LAYERS_TEXT = "Use USE topology for topology (address/FLOWS_TO/OPERATED_BY/LINKED graph with SWAPPED, ADDED_LIQUIDITY, REMOVED_LIQUIDITY, BRIDGED and the Pool label, unified recent+historical) and USE facts for bounded TRANSFER, SWAP, LIQUIDITY_ADD, LIQUIDITY_REMOVE and BRIDGE_CROSSING rows and enrichment, and USE chain for the chain node's own record of one known transaction, block, address or the head.";
|
|
135
144
|
const KNOWN_PUBLIC_TOOL_DESCRIPTIONS = {
|
|
136
145
|
meta_network_capabilities: "Return the current Chain Insights network and tool support matrix.",
|
|
137
146
|
meta_schema: require_graph_schema.META_SCHEMA_DESCRIPTION,
|
|
@@ -139,7 +148,7 @@ const KNOWN_PUBLIC_TOOL_DESCRIPTIONS = {
|
|
|
139
148
|
meta_subscription_status: "Return the caller's CIA subscription window end, daily allowance, consumption, and tier.",
|
|
140
149
|
meta_help: "Show a short guide to Chain Insights tools and workflow.",
|
|
141
150
|
wallet_balance: "Show the local Chain Insights payment wallet address, payment network, token, and amount.",
|
|
142
|
-
graph_query: `Run a read-only GQL/Cypher query through the Chain Insights graph endpoint. ${GRAPH_LAYERS_TEXT} Preserve full addresses exactly.`,
|
|
151
|
+
graph_query: `Run a read-only GQL/Cypher query through the Chain Insights graph endpoint. ${GRAPH_LAYERS_TEXT} ${ROUTING_NOT_SERVED} Preserve full addresses exactly.`,
|
|
143
152
|
graph_query_batch: "Run multiple read-only GQL/Cypher queries through the Chain Insights graph endpoint in one paid batch. Prefer this for related topology/facts reads."
|
|
144
153
|
};
|
|
145
154
|
const KNOWN_PUBLIC_TOOL_TITLES = {
|
|
@@ -168,13 +177,14 @@ const GRAPH_SCHEMA_HINTS = [
|
|
|
168
177
|
"- Call meta_network_capabilities first. Pass network= exactly as GraphRAG advertised it. CIA does not pick a default network.",
|
|
169
178
|
...routingHintLines(),
|
|
170
179
|
"- 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.",
|
|
171
|
-
"- 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.
|
|
180
|
+
"- The graph is address-grain. The only topology money node label is Address, keyed by the raw chain-native H160 address on EVM networks, for example 0x1874a43d7c6d888f9eda3d22a3a49704e3cadb24. There is no separate identity key. network is the query's network on every node and relationship, never stored, except on :Chain, where it names the remote chain.",
|
|
181
|
+
"- An Address carries a kind from chain facts. topology carries :Account or :Contract as a second label on an Address (:Contract:SmartAccount for a contract wallet), with is_contract kept beside :Contract, and an address that only received has none. facts serves no kind, and a facts read that names :Account or :Contract is refused: ask USE topology or USE chain. chain Address returns is_contract, nonce and delegated_to at the newest block, or at the block named by at_block. block_timestamp is epoch milliseconds on every layer.",
|
|
172
182
|
"- 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.",
|
|
173
183
|
"- 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.",
|
|
174
184
|
"- Address nodes carry base activity rollups: degree_in/degree_out/degree_total (distinct counterparty addresses), tx_in_count/tx_out_count/tx_total_count, total_in_usd/total_out_usd/total_volume_usd, net_flow_usd (in minus out; positive = net receiver) — all computed from external flows only — and first_activity_timestamp/last_activity_timestamp/activity_span_days, which include all flows (self-loops included). FLOWS_TO edges carry exactly tx_count, amount_usd_sum (total money flow, token and native value merged), first_seen_timestamp, last_seen_timestamp. Lifetime aggregates are the only serving window. Averages are computed inline (amount_usd_sum / toFloat(tx_count)). tx_count counts token and native transfers plus internal native transfers (a contract sending ETH during a call), and amount_usd_sum prices them all. USE facts TRANSFER lists all three, so the rows of a pair are the transfers its link counts, up to the height the link was built to. A link covers all time and a USE facts read covers one day: read the transfers of a pair one day at a time. A transaction anchor of a pair resolves through USE facts, on the UTC day of the first_seen_timestamp or last_seen_timestamp of the link: MATCH (a:Address {address: $from})-[t:TRANSFER]->(b:Address {address: $to}) WHERE t.block_date = \"YYYY-MM-DD\" RETURN t.tx_id, t.block_timestamp LIMIT 1.",
|
|
175
185
|
"- 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.",
|
|
176
186
|
"- Labels and per-label risk live on the address node: the labels array plus three parallel lists, label_risk_labels, label_risk_levels and label_risk_updated_timestamps, where entry i of each list is one label row. No overall risk score is served. USE facts serves bounded single-event rows (TRANSFER, SWAP, LIQUIDITY_ADD, LIQUIDITY_REMOVE and BRIDGE_CROSSING edges) only; lifetime address metrics (degrees, totals, activity window) are node properties on USE topology.",
|
|
177
|
-
"- (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,
|
|
187
|
+
"- (from:Address)-[t:TRANSFER]->(to:Address) on USE facts returns individual transfer rows, not aggregates, with properties amount, amount_usd, asset_symbol, asset_contract, tx_id, block_height, block_timestamp, event_index, edge_index, kind, price_usd, and price_missing. Every TRANSFER row has a kind: token, native or internal. An internal row is ETH a contract sends while it runs a call, such as the ETH leg of a wrap. Add t.kind = \"internal\" to the pair and the day to read only the internal rows. A facts read names an address pair with one day, whether it is a TRANSFER row-select or a count()/sum() aggregate: both endpoint addresses as {address: \"...\"} on from and to, with WHERE t.block_date = \"YYYY-MM-DD\". A transaction is read on USE chain by its tx_id, not on facts. t.block_timestamp bounds in epoch milliseconds may narrow the day to a time window. One address, a day alone, a window of days, a block range or a bare LIMIT is not enough. A facts read holds at most 200 rows in a reply, has one relationship and no hop, and takes no ORDER BY: sort the page yourself.",
|
|
178
188
|
"- 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.",
|
|
179
189
|
"- 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.",
|
|
180
190
|
"- (: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.",
|
|
@@ -183,7 +193,7 @@ const GRAPH_SCHEMA_HINTS = [
|
|
|
183
193
|
"- 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.",
|
|
184
194
|
"- (: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.",
|
|
185
195
|
"- 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.",
|
|
186
|
-
"- 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
|
|
196
|
+
"- USE facts serves the single events. (payer:Address)-[s:SWAP]->(recipient:Address) is one row per swap route, self swaps included, with strength, reason, route_id and the sold_ and bought_ asset, amount and usd columns. (provider:Address)-[:LIQUIDITY_ADD]->(pool:Address) and (pool:Address)-[:LIQUIDITY_REMOVE]->(receiver:Address) are one row per liquidity event, with party_state and evidence_state. (sender:Address)-[c:BRIDGE_CROSSING]->(recipient:Address) is one row per bridge event. SWAP, LIQUIDITY_* and BRIDGE_CROSSING take the same rule as TRANSFER: an address pair with one day. USD comes from the daily price services, never from a swap: with no price, USD is empty and the matching price_missing column is true (price_missing on TRANSFER, sold_price_missing and bought_price_missing on SWAP, amount0_price_missing and amount1_price_missing on LIQUIDITY_*). block_timestamp on TRANSFER, SWAP and LIQUIDITY_* rows is epoch milliseconds, in filters and in results; BRIDGE_CROSSING has none.",
|
|
187
197
|
"- 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.",
|
|
188
198
|
"- 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.",
|
|
189
199
|
"- 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.",
|
|
@@ -191,13 +201,15 @@ const GRAPH_SCHEMA_HINTS = [
|
|
|
191
201
|
"- Start schema discovery with endpoint-safe property reads: MATCH (n:Address) RETURN n.address AS address, n.network AS network, n.labels AS labels, n.last_activity_timestamp AS last_activity_timestamp LIMIT 20",
|
|
192
202
|
"- 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",
|
|
193
203
|
"- 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.",
|
|
194
|
-
"- graph_query uses the active Chain Insights graph endpoint. Select the graph with USE topology for topology (address/FLOWS_TO/OPERATED_BY/LINKED graph with SWAPPED, ADDED_LIQUIDITY, REMOVED_LIQUIDITY, BRIDGED and the Pool label, unified recent+historical) and USE facts for bounded TRANSFER, SWAP, LIQUIDITY_ADD, LIQUIDITY_REMOVE and BRIDGE_CROSSING rows and enrichment, and USE chain for one keyed lookup on the chain node: a transaction by
|
|
204
|
+
"- graph_query uses the active Chain Insights graph endpoint. Select the graph with USE topology for topology (address/FLOWS_TO/OPERATED_BY/LINKED graph with SWAPPED, ADDED_LIQUIDITY, REMOVED_LIQUIDITY, BRIDGED and the Pool label, unified recent+historical) and USE facts for bounded TRANSFER, SWAP, LIQUIDITY_ADD, LIQUIDITY_REMOVE and BRIDGE_CROSSING rows and enrichment, and USE chain for one keyed lookup on the chain node: a transaction by tx_id, a block by block_height or block_hash, an address at one block, or the head. On topology, address is the node grain, not the topology name.",
|
|
195
205
|
"- 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.",
|
|
196
206
|
"- Use USE facts graph patterns for fact and enrichment reads. Do not query internal table namespaces directly."
|
|
197
207
|
].join("\n");
|
|
208
|
+
const ROUTING_HEAD = routingHead();
|
|
198
209
|
const SERVER_INSTRUCTIONS = [
|
|
199
210
|
"Chain Insights is an AML and graph-analysis MCP server for AI agents.",
|
|
200
211
|
CHAIN_INSIGHTS_WORKFLOW,
|
|
212
|
+
ROUTING_HEAD,
|
|
201
213
|
PICTURE_RULES,
|
|
202
214
|
GRAPH_SCHEMA_HINTS,
|
|
203
215
|
"Presentation rules: preserve tool summaries as returned; never truncate blockchain addresses or identity_resolution audit mappings."
|
|
@@ -205,6 +217,7 @@ const SERVER_INSTRUCTIONS = [
|
|
|
205
217
|
const STATELESS_SERVER_INSTRUCTIONS = [
|
|
206
218
|
"Chain Insights is running as a stateless AML proxy for a host application.",
|
|
207
219
|
"Use meta_network_capabilities first when network support is unknown, then call graph_query or graph_query_batch as needed.",
|
|
220
|
+
ROUTING_HEAD,
|
|
208
221
|
PICTURE_RULES,
|
|
209
222
|
"Use wallet_balance to inspect the local payment wallet when payment setup is needed.",
|
|
210
223
|
GRAPH_SCHEMA_HINTS,
|
package/dist/mcp-proxy.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-proxy.d.cts","names":[],"sources":["../src/mcp/proxy.ts"],"mappings":";;qBA+Da;qBACA;;;;;wBAqBG;YAkBJ;wBAEI,oBAAoB,MAAK,OAAO,aAA2B;KA+BtE,iBAAiB,eAAe,EAAE;
|
|
1
|
+
{"version":3,"file":"mcp-proxy.d.cts","names":[],"sources":["../src/mcp/proxy.ts"],"mappings":";;qBA+Da;qBACA;;;;;wBAqBG;YAkBJ;wBAEI,oBAAoB,MAAK,OAAO,aAA2B;KA+BtE,iBAAiB,eAAe,EAAE;wBAgGvB,2BAA2B,mBAAmB;;;;;;;;wBA+lBxC,eAAe"}
|
package/dist/mcp-proxy.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-proxy.d.mts","names":[],"sources":["../src/mcp/proxy.ts"],"mappings":";;qBA+Da;qBACA;;;;;wBAqBG;YAkBJ;wBAEI,oBAAoB,MAAK,OAAO,aAA2B;KA+BtE,iBAAiB,eAAe,EAAE;
|
|
1
|
+
{"version":3,"file":"mcp-proxy.d.mts","names":[],"sources":["../src/mcp/proxy.ts"],"mappings":";;qBA+Da;qBACA;;;;;wBAqBG;YAkBJ;wBAEI,oBAAoB,MAAK,OAAO,aAA2B;KA+BtE,iBAAiB,eAAe,EAAE;wBAgGvB,2BAA2B,mBAAmB;;;;;;;;wBA+lBxC,eAAe"}
|
package/dist/mcp-proxy.mjs
CHANGED
|
@@ -17,21 +17,21 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
|
17
17
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
18
18
|
//#region src/mcp/layer-routing.ts
|
|
19
19
|
/** The routing rule, in one sentence. The server states it in the same words. */
|
|
20
|
-
const ROUTING_RULE = "Route by what
|
|
20
|
+
const ROUTING_RULE = "Route by what the question names: an address, a pair with a day, a transaction or a block. Topology searches outward from an address, while facts and chain look up one known thing.";
|
|
21
21
|
/** The rule in three lines, one for each layer. */
|
|
22
22
|
const ROUTING_LINES = [
|
|
23
|
-
"I
|
|
24
|
-
"I know
|
|
25
|
-
"I want the chain's own record of a transaction by its
|
|
23
|
+
"I know an address and want its links, senders, receivers, hops or a route: `USE topology`, anchored on that address.",
|
|
24
|
+
"I know both addresses of a pair and one day, and want the rows of that day: `USE facts`.",
|
|
25
|
+
"I want the chain's own record of a transaction by its `tx_id`, a block by its `block_height` or `block_hash`, an address at one block (`Address`), or the head: `USE chain`."
|
|
26
26
|
];
|
|
27
|
-
/** The chain layer
|
|
28
|
-
const
|
|
27
|
+
/** The chain layer reads one address at one block. A search for counterparties stays on topology. */
|
|
28
|
+
const ROUTING_ADDRESS_ON_CHAIN = "`USE chain` with `Address` reads the balance, the nonce or the kind of one address, now or at a past block with `at_block`. A past block must be at least `chain_admission.at_block_min_depth` blocks below the tip. To find the counterparties of an address, search `USE topology`, then read the pair and one day on `USE facts`.";
|
|
29
29
|
/** What to do when two layers fit. */
|
|
30
|
-
const ROUTING_TWO_FIT = "
|
|
31
|
-
/** The key that each layer hands to the next. */
|
|
32
|
-
const ROUTING_HANDOFF = "The layers hand each other keys. Topology gives the pair and the first and last seen time.
|
|
33
|
-
/** A question that no layer serves. */
|
|
34
|
-
const ROUTING_NOT_SERVED = "A list, a range or a whole-chain question
|
|
30
|
+
const ROUTING_TWO_FIT = "A transaction is a chain question: read it on `USE chain` by its `tx_id`. Chain gives its `block_date`; the transfers between a pair on that day are a `USE facts` read.";
|
|
31
|
+
/** The key that each layer hands to the next. Each key keeps its name. */
|
|
32
|
+
const ROUTING_HANDOFF = "The layers hand each other keys, and each key keeps its name. Topology gives the pair and the first and last seen time. Facts gives the `tx_id` and the `block_height`. Chain takes an `address`, a `tx_id`, a `block_height` or a `block_hash`, and gives the `block_date` of a transaction or a block.";
|
|
33
|
+
/** A question that no layer serves today. The agent says so once and asks for an anchor. */
|
|
34
|
+
const ROUTING_NOT_SERVED = "A list, a range or a whole-chain question names no address, pair, transaction or block, so no layer serves it today. Say so in one line, ask the user for one of those, and send no workaround query.";
|
|
35
35
|
/** What a refusal carries, and which part of it decides the move. */
|
|
36
36
|
const MOVE_INTRO = "A refused, killed, busy or failed query comes back with `error_detail`: `code`, `rule`, `class`, `fix` and `example`. The `class` decides your next move.";
|
|
37
37
|
/** The move for each class of the refusal envelope. */
|
|
@@ -64,11 +64,20 @@ function moveLines() {
|
|
|
64
64
|
];
|
|
65
65
|
}
|
|
66
66
|
/**
|
|
67
|
+
* The routing rule and its three lines, with the question that no layer serves,
|
|
68
|
+
* as one paragraph. The proxy serves it first among its instructions: a host
|
|
69
|
+
* that keeps only the start of the instructions (Claude Code keeps 2 KB) still
|
|
70
|
+
* shows the agent where each kind of question goes, and which kind goes nowhere.
|
|
71
|
+
*/
|
|
72
|
+
function routingHead() {
|
|
73
|
+
return `Pick the layer first. ${ROUTING_RULE} 1. ${ROUTING_LINES[0]} 2. ${ROUTING_LINES[1]} 3. ${ROUTING_LINES[2]} ${ROUTING_NOT_SERVED}`;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
67
76
|
* The two hints that the MCP proxy serves for routing and for refusals. Each is
|
|
68
77
|
* one line of the served instructions.
|
|
69
78
|
*/
|
|
70
79
|
function routingHintLines() {
|
|
71
|
-
return [`- Pick the layer first. ${ROUTING_RULE} 1. ${ROUTING_LINES[0]} 2. ${ROUTING_LINES[1]} 3. ${ROUTING_LINES[2]} ${
|
|
80
|
+
return [`- Pick the layer first. ${ROUTING_RULE} 1. ${ROUTING_LINES[0]} 2. ${ROUTING_LINES[1]} 3. ${ROUTING_LINES[2]} ${ROUTING_NOT_SERVED} ${ROUTING_ADDRESS_ON_CHAIN} ${ROUTING_TWO_FIT} ${ROUTING_HANDOFF} The lookups that USE chain serves are listed in chain_admission.lookups of meta_network_capabilities. Read every limit that the server publishes from meta_network_capabilities (chain_admission and, when the server sends them, topology_admission and facts_admission). Never write a limit down.`, `- Act on a refusal by its class. ${moveLines().join(" ")}`];
|
|
72
81
|
}
|
|
73
82
|
//#endregion
|
|
74
83
|
//#region src/mcp/proxy.ts
|
|
@@ -127,7 +136,7 @@ function resolveMcpProxyMode(env = process.env) {
|
|
|
127
136
|
if (raw === "no-workspace" || raw === "workspace-less") return "stateless";
|
|
128
137
|
throw new Error(`CHAIN_INSIGHTS_MCP_PROXY_MODE must be workspace or stateless; got "${raw}"`);
|
|
129
138
|
}
|
|
130
|
-
const GRAPH_LAYERS_TEXT = "Use USE topology for topology (address/FLOWS_TO/OPERATED_BY/LINKED graph with SWAPPED, ADDED_LIQUIDITY, REMOVED_LIQUIDITY, BRIDGED and the Pool label, unified recent+historical) and USE facts for bounded TRANSFER, SWAP, LIQUIDITY_ADD, LIQUIDITY_REMOVE and BRIDGE_CROSSING rows and enrichment, and USE chain for the chain node's own record of one known transaction, block or the head.";
|
|
139
|
+
const GRAPH_LAYERS_TEXT = "Use USE topology for topology (address/FLOWS_TO/OPERATED_BY/LINKED graph with SWAPPED, ADDED_LIQUIDITY, REMOVED_LIQUIDITY, BRIDGED and the Pool label, unified recent+historical) and USE facts for bounded TRANSFER, SWAP, LIQUIDITY_ADD, LIQUIDITY_REMOVE and BRIDGE_CROSSING rows and enrichment, and USE chain for the chain node's own record of one known transaction, block, address or the head.";
|
|
131
140
|
const KNOWN_PUBLIC_TOOL_DESCRIPTIONS = {
|
|
132
141
|
meta_network_capabilities: "Return the current Chain Insights network and tool support matrix.",
|
|
133
142
|
meta_schema: META_SCHEMA_DESCRIPTION,
|
|
@@ -135,7 +144,7 @@ const KNOWN_PUBLIC_TOOL_DESCRIPTIONS = {
|
|
|
135
144
|
meta_subscription_status: "Return the caller's CIA subscription window end, daily allowance, consumption, and tier.",
|
|
136
145
|
meta_help: "Show a short guide to Chain Insights tools and workflow.",
|
|
137
146
|
wallet_balance: "Show the local Chain Insights payment wallet address, payment network, token, and amount.",
|
|
138
|
-
graph_query: `Run a read-only GQL/Cypher query through the Chain Insights graph endpoint. ${GRAPH_LAYERS_TEXT} Preserve full addresses exactly.`,
|
|
147
|
+
graph_query: `Run a read-only GQL/Cypher query through the Chain Insights graph endpoint. ${GRAPH_LAYERS_TEXT} ${ROUTING_NOT_SERVED} Preserve full addresses exactly.`,
|
|
139
148
|
graph_query_batch: "Run multiple read-only GQL/Cypher queries through the Chain Insights graph endpoint in one paid batch. Prefer this for related topology/facts reads."
|
|
140
149
|
};
|
|
141
150
|
const KNOWN_PUBLIC_TOOL_TITLES = {
|
|
@@ -164,13 +173,14 @@ const GRAPH_SCHEMA_HINTS = [
|
|
|
164
173
|
"- Call meta_network_capabilities first. Pass network= exactly as GraphRAG advertised it. CIA does not pick a default network.",
|
|
165
174
|
...routingHintLines(),
|
|
166
175
|
"- 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.",
|
|
167
|
-
"- 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.
|
|
176
|
+
"- The graph is address-grain. The only topology money node label is Address, keyed by the raw chain-native H160 address on EVM networks, for example 0x1874a43d7c6d888f9eda3d22a3a49704e3cadb24. There is no separate identity key. network is the query's network on every node and relationship, never stored, except on :Chain, where it names the remote chain.",
|
|
177
|
+
"- An Address carries a kind from chain facts. topology carries :Account or :Contract as a second label on an Address (:Contract:SmartAccount for a contract wallet), with is_contract kept beside :Contract, and an address that only received has none. facts serves no kind, and a facts read that names :Account or :Contract is refused: ask USE topology or USE chain. chain Address returns is_contract, nonce and delegated_to at the newest block, or at the block named by at_block. block_timestamp is epoch milliseconds on every layer.",
|
|
168
178
|
"- 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.",
|
|
169
179
|
"- 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.",
|
|
170
180
|
"- Address nodes carry base activity rollups: degree_in/degree_out/degree_total (distinct counterparty addresses), tx_in_count/tx_out_count/tx_total_count, total_in_usd/total_out_usd/total_volume_usd, net_flow_usd (in minus out; positive = net receiver) — all computed from external flows only — and first_activity_timestamp/last_activity_timestamp/activity_span_days, which include all flows (self-loops included). FLOWS_TO edges carry exactly tx_count, amount_usd_sum (total money flow, token and native value merged), first_seen_timestamp, last_seen_timestamp. Lifetime aggregates are the only serving window. Averages are computed inline (amount_usd_sum / toFloat(tx_count)). tx_count counts token and native transfers plus internal native transfers (a contract sending ETH during a call), and amount_usd_sum prices them all. USE facts TRANSFER lists all three, so the rows of a pair are the transfers its link counts, up to the height the link was built to. A link covers all time and a USE facts read covers one day: read the transfers of a pair one day at a time. A transaction anchor of a pair resolves through USE facts, on the UTC day of the first_seen_timestamp or last_seen_timestamp of the link: MATCH (a:Address {address: $from})-[t:TRANSFER]->(b:Address {address: $to}) WHERE t.block_date = \"YYYY-MM-DD\" RETURN t.tx_id, t.block_timestamp LIMIT 1.",
|
|
171
181
|
"- 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.",
|
|
172
182
|
"- Labels and per-label risk live on the address node: the labels array plus three parallel lists, label_risk_labels, label_risk_levels and label_risk_updated_timestamps, where entry i of each list is one label row. No overall risk score is served. USE facts serves bounded single-event rows (TRANSFER, SWAP, LIQUIDITY_ADD, LIQUIDITY_REMOVE and BRIDGE_CROSSING edges) only; lifetime address metrics (degrees, totals, activity window) are node properties on USE topology.",
|
|
173
|
-
"- (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,
|
|
183
|
+
"- (from:Address)-[t:TRANSFER]->(to:Address) on USE facts returns individual transfer rows, not aggregates, with properties amount, amount_usd, asset_symbol, asset_contract, tx_id, block_height, block_timestamp, event_index, edge_index, kind, price_usd, and price_missing. Every TRANSFER row has a kind: token, native or internal. An internal row is ETH a contract sends while it runs a call, such as the ETH leg of a wrap. Add t.kind = \"internal\" to the pair and the day to read only the internal rows. A facts read names an address pair with one day, whether it is a TRANSFER row-select or a count()/sum() aggregate: both endpoint addresses as {address: \"...\"} on from and to, with WHERE t.block_date = \"YYYY-MM-DD\". A transaction is read on USE chain by its tx_id, not on facts. t.block_timestamp bounds in epoch milliseconds may narrow the day to a time window. One address, a day alone, a window of days, a block range or a bare LIMIT is not enough. A facts read holds at most 200 rows in a reply, has one relationship and no hop, and takes no ORDER BY: sort the page yourself.",
|
|
174
184
|
"- 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.",
|
|
175
185
|
"- 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.",
|
|
176
186
|
"- (: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.",
|
|
@@ -179,7 +189,7 @@ const GRAPH_SCHEMA_HINTS = [
|
|
|
179
189
|
"- 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.",
|
|
180
190
|
"- (: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.",
|
|
181
191
|
"- 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.",
|
|
182
|
-
"- 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
|
|
192
|
+
"- USE facts serves the single events. (payer:Address)-[s:SWAP]->(recipient:Address) is one row per swap route, self swaps included, with strength, reason, route_id and the sold_ and bought_ asset, amount and usd columns. (provider:Address)-[:LIQUIDITY_ADD]->(pool:Address) and (pool:Address)-[:LIQUIDITY_REMOVE]->(receiver:Address) are one row per liquidity event, with party_state and evidence_state. (sender:Address)-[c:BRIDGE_CROSSING]->(recipient:Address) is one row per bridge event. SWAP, LIQUIDITY_* and BRIDGE_CROSSING take the same rule as TRANSFER: an address pair with one day. USD comes from the daily price services, never from a swap: with no price, USD is empty and the matching price_missing column is true (price_missing on TRANSFER, sold_price_missing and bought_price_missing on SWAP, amount0_price_missing and amount1_price_missing on LIQUIDITY_*). block_timestamp on TRANSFER, SWAP and LIQUIDITY_* rows is epoch milliseconds, in filters and in results; BRIDGE_CROSSING has none.",
|
|
183
193
|
"- 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.",
|
|
184
194
|
"- 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.",
|
|
185
195
|
"- 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.",
|
|
@@ -187,13 +197,15 @@ const GRAPH_SCHEMA_HINTS = [
|
|
|
187
197
|
"- Start schema discovery with endpoint-safe property reads: MATCH (n:Address) RETURN n.address AS address, n.network AS network, n.labels AS labels, n.last_activity_timestamp AS last_activity_timestamp LIMIT 20",
|
|
188
198
|
"- 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",
|
|
189
199
|
"- 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.",
|
|
190
|
-
"- graph_query uses the active Chain Insights graph endpoint. Select the graph with USE topology for topology (address/FLOWS_TO/OPERATED_BY/LINKED graph with SWAPPED, ADDED_LIQUIDITY, REMOVED_LIQUIDITY, BRIDGED and the Pool label, unified recent+historical) and USE facts for bounded TRANSFER, SWAP, LIQUIDITY_ADD, LIQUIDITY_REMOVE and BRIDGE_CROSSING rows and enrichment, and USE chain for one keyed lookup on the chain node: a transaction by
|
|
200
|
+
"- graph_query uses the active Chain Insights graph endpoint. Select the graph with USE topology for topology (address/FLOWS_TO/OPERATED_BY/LINKED graph with SWAPPED, ADDED_LIQUIDITY, REMOVED_LIQUIDITY, BRIDGED and the Pool label, unified recent+historical) and USE facts for bounded TRANSFER, SWAP, LIQUIDITY_ADD, LIQUIDITY_REMOVE and BRIDGE_CROSSING rows and enrichment, and USE chain for one keyed lookup on the chain node: a transaction by tx_id, a block by block_height or block_hash, an address at one block, or the head. On topology, address is the node grain, not the topology name.",
|
|
191
201
|
"- 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.",
|
|
192
202
|
"- Use USE facts graph patterns for fact and enrichment reads. Do not query internal table namespaces directly."
|
|
193
203
|
].join("\n");
|
|
204
|
+
const ROUTING_HEAD = routingHead();
|
|
194
205
|
const SERVER_INSTRUCTIONS = [
|
|
195
206
|
"Chain Insights is an AML and graph-analysis MCP server for AI agents.",
|
|
196
207
|
CHAIN_INSIGHTS_WORKFLOW,
|
|
208
|
+
ROUTING_HEAD,
|
|
197
209
|
PICTURE_RULES,
|
|
198
210
|
GRAPH_SCHEMA_HINTS,
|
|
199
211
|
"Presentation rules: preserve tool summaries as returned; never truncate blockchain addresses or identity_resolution audit mappings."
|
|
@@ -201,6 +213,7 @@ const SERVER_INSTRUCTIONS = [
|
|
|
201
213
|
const STATELESS_SERVER_INSTRUCTIONS = [
|
|
202
214
|
"Chain Insights is running as a stateless AML proxy for a host application.",
|
|
203
215
|
"Use meta_network_capabilities first when network support is unknown, then call graph_query or graph_query_batch as needed.",
|
|
216
|
+
ROUTING_HEAD,
|
|
204
217
|
PICTURE_RULES,
|
|
205
218
|
"Use wallet_balance to inspect the local payment wallet when payment setup is needed.",
|
|
206
219
|
GRAPH_SCHEMA_HINTS,
|