fix(mcp): cap get_chains, report totalMatched, add status filter#100
Merged
Conversation
The assistant answered "list chains" with 1257 (the testnet count) as the
total and couldn't list them. Root cause: get_chains returned all ~2932
chains as full objects (with a price lookup each) — a payload that overflows
the model's context, so it lost the true total and reused a category count
from an earlier get_stats.
- Cap the returned list (default 50, max 200) and return totalMatched (full
count matching the filter), count (returned), and truncated. The honest
total is now always in the response.
- Only price-enrich the returned slice, not all 2932 chains.
- Add a `status` filter (active/incubating/deprecated/unknown) so "list
deprecated/active chains" works directly.
- Description tells the model totalMatched is the full count, the list is a
sample, and never to treat a tag/category count as the overall total;
points at get_stats for exact totals and search_chains to find one.
Verified against the live registry: {} -> totalMatched 2932 count 50;
status=deprecated -> 141; status=active -> 2693; tag=L2+deprecated -> 33.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvKASF2guQdnLuivCfTPH8
There was a problem hiding this comment.
Pull request overview
This PR updates the MCP get_chains tool to avoid returning the full registry payload (and doing price enrichment for every chain), instead returning a capped sample plus metadata (totalMatched, count, truncated) and adding a lifecycle status filter so assistants can directly list active/deprecated/incubating chains without losing the true totals.
Changes:
- Cap
get_chainsresults (default 50, max 200) while reportingtotalMatchedandtruncated, and only price-enrich the returned slice. - Add
statusfiltering (active/incubating/deprecated/unknown) toget_chains. - Add unit tests covering capping/defaults,
totalMatched/truncated, and status filtering.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| mcp-tools.js | Implements the capped get_chains response shape, adds status and limit inputs, and limits price enrichment to the returned slice. |
| tests/unit/mcp-tools.test.js | Adds unit tests validating the new get_chains capping/metadata behavior and lifecycle status filtering. |
| // 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)); |
Comment on lines
+53
to
+56
| limit: { | ||
| type: 'number', | ||
| description: 'Max chains to return (default 50, max 200)', | ||
| }, |
Per feedback: the 50-item cap is fine, but the assistant must say it's showing a subset (and never state a wrong total). Extend the get_chains description so that when `truncated` is true the model tells the user it's showing the first `count` of `totalMatched` (a display limit) and offers to narrow by tag/status or search — instead of presenting the sample as the whole list. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CvKASF2guQdnLuivCfTPH8
| // 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)); |
Comment on lines
+53
to
+56
| limit: { | ||
| type: 'number', | ||
| description: 'Max chains to return (default 50, max 200)', | ||
| }, |
Comment on lines
+306
to
+315
| 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); | ||
| }); | ||
|
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Asked to "list the chains", the assistant said the total was 1257 — which is
totalTestnets, not the ~2932 total — then couldn't list them. Root cause:get_chainsreturned all ~2932 chains as full objects (with a price lookup for each). That payload overflows the model's context, so it lost the real total and reused a category count from an earlierget_stats.Fix (
mcp-tools.js)totalMatched(full count matching the filter),count(returned), andtruncated. The honest total is now always in the response even when the list is capped.statusfilter (active/incubating/deprecated/unknown) so "list deprecated/active chains" works directly.totalMatchedis the full count, the list is a sample, never treat a tag/category count as the overall total; useget_statsfor exact totals andsearch_chainsto find a specific chain.Verification
Against the live registry:
{}{status:'deprecated', limit:3}{status:'active'}{tag:'L2', status:'deprecated'}Unit tests added (cap, default cap,
totalMatched,truncated,statusfilter); full suite 919 passed / 4 skipped; lint clean.Deploy
Server-side MCP/assistant change — ships on the next version bump.
🤖 Generated with Claude Code