MCP Server (Off‑chain)
Production‑ready MCP server to query Chunk MedianFeeds over stdio.
Code: mcp-server
Defaults
- RPC:
https://rpc.chunknet.org - MedianFeeds (chunk):
0x78D02A47fA898ffF4B37A9B414Eace5eed3e7fAD
Production environment (defaults; overrides optional):
CHUNK_RPC=https://rpc.chunknet.org
CHUNK_MEDIAN_ADDR=0x78D02A47fA898ffF4B37A9B414Eace5eed3e7fAD
METRICS_CACHE_TTL_MS=30000
Notes
- Overrides are not required: the server uses these values by default.
- Change only if you run against a custom RPC/contract.
Quickstart
cd mcp-server
npm install
npm run build
npm start # starts stdio MCP server
Requirements
- Node.js 18+ (ESM)
Tools
list_metrics(chain?, rpcUrl?, contractAddress?, currency?, tagIncludes?, nameContains?, limit?, offset?)— List metric definitions (paginated; response includestotal,offset,limit).quote_metrics(names: string[], chain?, rpcUrl?, contractAddress?, format?, decimals?)— Latest quotes.get_signed_root(chain?, rpcUrl?, contractAddress?)— Current signed Merkle tree root (epoch).get_metrics_count(...)— Total metrics count.has_metric(name, ...)— Presence and id.get_metrics_map(...)— Mappingname -> id(index based).quote_by_ids(ids: number[], ..., format?, decimals?)— Quote by numeric ids.quote_metrics_at_block(names, blockNumber, ..., format?, decimals?)— Quote at block.quote_metrics_at_epoch_end(names, epochDuration?, epochId?, ..., format?, decimals?)— Quote at epoch end.quote_metrics_at_timestamp(names, timestamp, ..., format?, decimals?)— Quote at UNIX timestamp (seconds).get_health(rpcUrl?)— RPC health{ chainId, blockNumber }.ids_for_names(names[], ...)— Resolve on‑chain ids for names.check_staleness(names[], maxAgeSeconds, ...)— Staleness status.check_thresholds(rules[], format=decimal, ...)— Threshold checks (op:lt|lte|gt|gte|eq|neq).
Formatting
- Raw on‑chain values are 2**112‑scaled integers.
- Use
format: 'decimal'anddecimalsto get rounded strings;format: 'int'returns integer division by 2**112;format: 'all'returns both.
Client (Node.js)
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
const transport = new StdioClientTransport({
command: 'node',
args: ['dist/server.js'],
env: process.env,
});
const client = new Client({ name: 'chunk-mcp', version: '1.0.0' });
await client.connect(transport);
// List tools
const tools = await client.listTools({});
console.log('Tools:', tools.tools.map(t => t.name));
// List metrics (first 5)
const list = await client.callTool({ name: 'list_metrics', arguments: { limit: 5 } });
console.log('Metrics:', list.structuredContent.metrics);
// Quote metrics as decimals
const names = list.structuredContent.metrics.slice(0, 2).map(m => m.name);
const quotes = await client.callTool({ name: 'quote_metrics', arguments: { names, format: 'decimal', decimals: 6 } });
console.log('Quotes:', quotes.structuredContent.quotes);
// Signed root (epoch)
const root = await client.callTool({ name: 'get_signed_root', arguments: {} });
console.log('Signed root:', root.structuredContent);
await client.close();
Examples (Real Output)
Note: values change over time; samples captured from the live Chunk RPC.
Example — list_metrics (limit 3)
const list = await client.callTool({ name: 'list_metrics', arguments: { limit: 3 } });
{
"metrics": [
{
"name": "0x0000000000085d4780b73119b644ae5ecd22b376",
"description": "TrueUSD",
"currency": "USD",
"tags": ["Symbol:TUSD", "Chain:Ethereum:0x0000000000085d4780b73119b644ae5ecd22b376", "…"]
},
{
"name": "0x00f3c42833c3170159af4e92dbb451fb3f708917",
"description": "Internet Computer",
"currency": "USD",
"tags": ["Symbol:ICP", "Chain:Ethereum:0x00f3c4…", "…"]
},
{
"name": "0x0f5d2fb29fb7d3cfee444a200298f468908cc942",
"description": "Decentraland",
"currency": "USD",
"tags": ["Symbol:MANA", "Chain:Ethereum:0x0f5d2f…", "…"]
}
],
"total": 185,
"offset": 0,
"limit": 3
}
Example — quote_metrics (decimal formatting)
const names = list.structuredContent.metrics.map(m => m.name);
const quotes = await client.callTool({ name: 'quote_metrics', arguments: { names, format: 'decimal', decimals: 6 } });
{
"quotes": [
{ "name": "0x0000000000085d4780b73119b644ae5ecd22b376", "rawValue": "5174582366866901766366772880936719", "normalized": 0, "updateTS": 1761197400, "decimal": "0.996588" },
{ "name": "0x00f3c42833c3170159af4e92dbb451fb3f708917", "rawValue": "15546964585275390483323795256013778", "normalized": 2, "updateTS": 1761197400, "decimal": "2.994236" },
{ "name": "0x0f5d2fb29fb7d3cfee444a200298f468908cc942", "rawValue": "1196138411594040635516945093806199", "normalized": 0, "updateTS": 1761197400, "decimal": "0.230368" }
]
}
Example — get_signed_root
const root = await client.callTool({ name: 'get_signed_root', arguments: {} });
{
"epoch": 1761196500,
"v": 27,
"r": "0x9f792636f8195c089ffa958323905fe20bb7f14576170daba03d945d7ced9f1d",
"s": "0x593eb26d83c34857bcdc6f37f68233a65e957beb71189f3fa9b824175f8e4aab",
"root": "0x2ce99e3219fb25041e20f949824b4d56ae03c5c02fd6af4a9a2598d1122996ce"
}
Example — ids_for_names → quote_by_ids
const ids = await client.callTool({ name: 'ids_for_names', arguments: { names } });
const idList = Object.values(ids.structuredContent.ids).map(s => Number(s));
const byIds = await client.callTool({ name: 'quote_by_ids', arguments: { ids: idList, format: 'decimal', decimals: 6 } });
{
"ids": {
"0x0000000000085d4780b73119b644ae5ecd22b376": "16",
"0x00f3c42833c3170159af4e92dbb451fb3f708917": "165",
"0x0f5d2fb29fb7d3cfee444a200298f468908cc942": "131"
}
}
Example — quote_metrics_at_block
const health = await client.callTool({ name: 'get_health', arguments: {} });
const block = health.structuredContent.blockNumber - 2;
const atBlock = await client.callTool({ name: 'quote_metrics_at_block', arguments: { names, blockNumber: block, format: 'decimal', decimals: 6 } });
{
"blockNumber": 1021884,
"quotes": [
{ "name": "0x0000000000085d4780b73119b644ae5ecd22b376", "decimal": "0.996588", "updateTS": 1761197400, "rawValue": "5174582366866901766366772880936719", "normalized": 0 },
{ "name": "0x00f3c42833c3170159af4e92dbb451fb3f708917", "decimal": "2.994236", "updateTS": 1761197400, "rawValue": "15546964585275390483323795256013778", "normalized": 2 },
{ "name": "0x0f5d2fb29fb7d3cfee444a200298f468908cc942", "decimal": "0.230368", "updateTS": 1761197400, "rawValue": "1196138411594040635516945093806199", "normalized": 0 }
]
}
Resources
Content type
application/jsonfor all resources.
URIs (examples)
chunk://median/chunk/metricschunk://median/chunk/metric/ETHchunk://median/chunk/metric/0x0000000000085d4780b73119b644ae5ecd22b376
chunk://median/{chain}/metrics
- Returns a JSON array of metric objects.
- Shape:
{ name: string, description: string, currency: string, tags: string[] }. - Note: for large datasets prefer
list_metricstool with pagination.
chunk://median/{chain}/metric/{name}
- Returns a JSON object with the latest quote for a single metric.
- Shape:
{ name: string, rawValue: string, normalized: number, updateTS: number }. - Decimal formatting is not included in resources; use
quote_metricswithformat: 'decimal'if needed.
Autocomplete
{chain}currently completes tochunk.{name}autocompletes from on‑chain metrics (up to 50 suggestions).- Use
list_metrics/has_metric/ids_for_namesto discover valid names before resolving a quote resource.
Subscriptions
The server registers resources.subscribe. On epoch changes it emits resources/updated for subscribed URIs:
chunk://median/chunk/metricschunk://median/chunk/metric/{name}(per‑metric)
Notes
- Notifications are sent only for URIs that are currently subscribed by the client.
- Quote values change at epoch boundaries; subscribe if you need push updates.
Example (Node.js):
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import { ResourceUpdatedNotificationSchema } from '@modelcontextprotocol/sdk/types.js';
const transport = new StdioClientTransport({ command: 'node', args: ['dist/server.js'], env: process.env });
const client = new Client({ name: 'chunk-mcp', version: '1.0.0' });
await client.connect(transport);
client.setNotificationHandler(ResourceUpdatedNotificationSchema, (n) => {
console.log('Updated:', n.params.uri);
});
await client.subscribeResource({ uri: 'chunk://median/chunk/metrics' });
await client.subscribeResource({ uri: 'chunk://median/chunk/metric/ETH' });
Claude Desktop (MCP)
Add a stdio server in your Claude configuration:
{
"mcpServers": {
"chunk-mcp": {
"command": "node",
"args": ["dist/server.js"],
"env": {
"CHUNK_RPC": "https://rpc.chunknet.org",
"CHUNK_MEDIAN_ADDR": "0x78D02A47fA898ffF4B37A9B414Eace5eed3e7fAD"
}
}
}
}
Operational Notes
- Node 18+ required; ESM modules only.
- Public RPC may be rate‑limited; prefer a dedicated endpoint in production.
- The server caches
getMetrics()responses (METRICS_CACHE_TTL_MS, default 30s) to reduce RPC load. - For critical paths, validate staleness via
check_stalenessand apply thresholds viacheck_thresholds. - Handle transient RPC errors with retries on the client side.
Error Handling
Common errors
Unsupported chain— onlychunkis supported by default.Median contract address is required— setCHUNK_MEDIAN_ADDRor passcontractAddress.names array is required— pass a non‑emptynamesarray to quoting tools.
Security & Correctness
- Always check timestamp (
updateTS) and staleness before consuming quotes. - Treat decimal formatting as convenience; raw values are canonical (2**112 scaled integers).
- For protocol decisions, compare raw values or use
check_thresholdswith explicitformat.
Performance
- Batch queries: prefer
quote_metricsorquote_by_idswith multiple names/ids. - Use
list_metricsfilters (currency,tagIncludes,nameContains) and pagination to limit payloads.