Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 32 additions & 5 deletions mcp-tools.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ export function getToolDefinitions() {
return [
{
name: 'get_chains',
description: 'Get all blockchain chains, optionally filtered by tag (Testnet, L2, or Beacon)',
description:
'List blockchain chains, optionally filtered by tag (Testnet/L2/Beacon) and/or lifecycle status. Returns a CAPPED sample (default 50, max 200) — `totalMatched` is the full number of chains matching the filter, `count` is how many are in this response, and `truncated` is true when there are more. When `truncated` is true, tell the user you are showing the first `count` of `totalMatched` (a display limit) and offer to narrow by tag/status or search by name — do NOT present the sample as the complete list. For an exact total or category breakdown use get_stats; to find a specific chain use search_chains. Never treat a tag/category count as the overall registry total.',
inputSchema: {
type: 'object',
properties: {
Expand All @@ -44,6 +45,15 @@ export function getToolDefinitions() {
description: 'Optional tag to filter chains (e.g., "Testnet", "L2", "Beacon")',
enum: ['Testnet', 'L2', 'Beacon'],
},
status: {
type: 'string',
description: 'Optional lifecycle status filter (e.g. "deprecated" for retired chains, "active" for live ones)',
enum: ['active', 'incubating', 'deprecated', 'unknown'],
},
limit: {
type: 'number',
description: 'Max chains to return (default 50, max 200)',
},
Comment on lines +53 to +56
Comment on lines +53 to +56
},
},
},
Expand Down Expand Up @@ -352,18 +362,35 @@ function isValidChainId(chainId) {

// --- Individual tool handlers ---

const GET_CHAINS_DEFAULT_LIMIT = 50;
const GET_CHAINS_MAX_LIMIT = 200;

async function handleGetChains(args) {
let chains = getAllChains();
if (args.tag) {
chains = chains.filter((chain) => chain.tags?.includes(args.tag));
}
const chainIds = chains.map((c) => c.chainId);
const priceMap = await getPricesForChains(chainIds);
const enrichedChains = chains.map((chain) => ({
if (args.status) {
chains = chains.filter((chain) => (chain.status || 'unknown') === args.status);
}
// The registry has ~3000 chains; returning them all (with a price lookup
// each) is a huge payload that overflows an LLM's context and makes it lose
// the true total. Cap the list, report totalMatched so callers still get the
// honest count, and only price-enrich the slice actually returned.
const totalMatched = chains.length;
const limit = Math.max(1, Math.min(Number(args.limit) || GET_CHAINS_DEFAULT_LIMIT, GET_CHAINS_MAX_LIMIT));
const sliced = chains.slice(0, limit);
const priceMap = await getPricesForChains(sliced.map((c) => c.chainId));
const enrichedChains = sliced.map((chain) => ({
...chain,
price: priceMap.get(chain.chainId) ?? null,
}));
return textResponse({ count: enrichedChains.length, chains: enrichedChains });
return textResponse({
totalMatched,
count: enrichedChains.length,
truncated: totalMatched > enrichedChains.length,
chains: enrichedChains,
});
}

async function handleGetChainById(args) {
Expand Down
37 changes: 37 additions & 0 deletions tests/unit/mcp-tools.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -285,10 +285,47 @@ describe('MCP Tools - Shared Module', () => {
const result = await handleToolCall('get_chains', {});
const data = JSON.parse(result.content[0].text);
expect(data.count).toBe(2);
expect(data.totalMatched).toBe(2);
expect(data.truncated).toBe(false);
expect(data.chains.length).toBe(2);
expect(result.isError).toBeUndefined();
});

it('caps the returned list but reports the full total via totalMatched', async () => {
const many = Array.from({ length: 130 }, (_, i) => ({ chainId: i + 1, name: `Chain ${i + 1}`, tags: [] }));
vi.mocked(dataService.getAllChains).mockReturnValue(many);

const result = await handleToolCall('get_chains', { limit: 20 });
const data = JSON.parse(result.content[0].text);
expect(data.totalMatched).toBe(130);
expect(data.count).toBe(20);
expect(data.chains.length).toBe(20);
expect(data.truncated).toBe(true);
});

it('applies the default cap (50) when no limit is given', async () => {
const many = Array.from({ length: 130 }, (_, i) => ({ chainId: i + 1, name: `Chain ${i + 1}`, tags: [] }));
vi.mocked(dataService.getAllChains).mockReturnValue(many);

const result = await handleToolCall('get_chains', {});
const data = JSON.parse(result.content[0].text);
expect(data.count).toBe(50);
expect(data.totalMatched).toBe(130);
});

Comment on lines +306 to +315
it('filters by lifecycle status', async () => {
vi.mocked(dataService.getAllChains).mockReturnValue([
{ chainId: 1, name: 'Ethereum', tags: [], status: 'active' },
{ chainId: 5, name: 'Goerli', tags: ['Testnet'], status: 'deprecated' },
{ chainId: 11155111, name: 'Sepolia', tags: ['Testnet'], status: 'active' },
]);

const result = await handleToolCall('get_chains', { status: 'deprecated' });
const data = JSON.parse(result.content[0].text);
expect(data.totalMatched).toBe(1);
expect(data.chains.map((c) => c.chainId)).toEqual([5]);
});

it('should filter chains by tag', async () => {
vi.mocked(dataService.getAllChains).mockReturnValue([
{ chainId: 1, name: 'Ethereum', tags: [] },
Expand Down
Loading