Skip to main content

MCP Server (Off‑chain)

Open GitHub repository →

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 includes total, 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(...) — Mapping name -> 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' and decimals to 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/json for all resources.

URIs (examples)

  • chunk://median/chunk/metrics
  • chunk://median/chunk/metric/ETH
  • chunk://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_metrics tool 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_metrics with format: 'decimal' if needed.

Autocomplete

  • {chain} currently completes to chunk.
  • {name} autocompletes from on‑chain metrics (up to 50 suggestions).
  • Use list_metrics/has_metric/ids_for_names to 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/metrics
  • chunk://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_staleness and apply thresholds via check_thresholds.
  • Handle transient RPC errors with retries on the client side.

Error Handling

Common errors

  • Unsupported chain — only chunk is supported by default.
  • Median contract address is required — set CHUNK_MEDIAN_ADDR or pass contractAddress.
  • names array is required — pass a non‑empty names array 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_thresholds with explicit format.

Performance

  • Batch queries: prefer quote_metrics or quote_by_ids with multiple names/ids.
  • Use list_metrics filters (currency, tagIncludes, nameContains) and pagination to limit payloads.