Skip to content

feat: add lens_list_pairs, lens_get_pools and lens_screen_pairs tools - #482

Open
Ugasutun wants to merge 2 commits into
Divinity-Labs:mainfrom
Ugasutun:feat/mcp-lens-tools
Open

Ugasutun wants to merge 2 commits into
Divinity-Labs:mainfrom
Ugasutun:feat/mcp-lens-tools

Conversation

@Ugasutun

@Ugasutun Ugasutun commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Closes #409

Summary

MCP exposed prices (L213) but nothing for discovery. An agent couldn't list
which pairs exist, see the liquidity behind them, or find what's moving, so it
had to guess asset codes. This PR adds three MCP tools that wrap the existing
/pairs, /pools and /screener logic.

Changes

  • Added lens_list_pairs, lens_get_pools and lens_screen_pairs MCP tools.
  • Extracted the query logic behind /pairs, /pools and /screener into
    shared service functions. The REST routes and MCP tools both call them, so
    there is a single implementation and no duplicated queries.
  • All three tools return bounded results (default limit of N, hard max of M)
    with a next_cursor / offset field for fetching the next page.
  • Every asset in a result includes its full issuer address, never truncated.
  • Tool descriptions state when to use each tool:
    • lens_list_pairs: enumerate which pairs exist, e.g. to discover valid
      asset codes before asking for a price.
    • lens_get_pools: inspect the liquidity behind a specific pair.
    • lens_screen_pairs: find pairs by movement or criteria, e.g. top movers.

Pagination

Pass the next_cursor from a previous response as cursor to get the next
page. When next_cursor is null, there are no more results. The parameters
are documented in each tool's input schema and in docs/mcp.md.

Testing

  • Parity tests call each REST route and its MCP tool with the same inputs and
    assert the payloads match.
  • Tests assert that results never exceed the page limit and that following the
    cursor yields the next page with no overlap.
  • Tests assert that issuers are returned in full (no shortened form like
    GABC…XYZ).
  • Tests assert that the three tool descriptions are distinct.

Checklist

  • All three tools exist and share implementations with the REST routes
  • Results are bounded, with a documented way to get the next page
  • Issuers appear in full
  • Descriptions distinguish the tools from one another
  • Tests assert parity with the corresponding REST route
  • Docs updated

…bs#409)

Agents could ask for a price but had no way to discover what is quotable,
what liquidity sits behind a pair, or what is moving — REST answered all
three while MCP answered none, leaving agents to guess asset codes.

lens_list_pairs, lens_get_pools and lens_screen_pairs call the same
listPairs/listPools/screenPairs the REST routes now delegate to, so the two
transports cannot drift apart. Each result is bounded by a limit (max 100)
and carries nextCursor/hasMore, assets always include the full issuer, and
the tool descriptions spell out which question each one answers.
@drips-wave

drips-wave Bot commented Oct 17, 2026

Copy link
Copy Markdown

@Ugasutun Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@Miracle656

Copy link
Copy Markdown
Collaborator

Coordination note — ten PRs are editing one 179-line file

If your MCP PR has just gone red with a conflict, this is why, and it is my fault rather than yours.

I published 22 MCP issues (#404–#425) as if they were independent. They are not: almost all of them land in src/mcp/server.ts, which is 179 lines long. Every merge therefore conflicts everything else. The batch should have been sequenced, or scoped so the work genuinely separated — that was a mistake in how I wrote the issues, not in how you answered them.

Already merged:

Order I intend to take the rest in, so you can rebase once rather than repeatedly:

  1. feat: add configurable host allow-list to paid_call, refuse before any request #487 — paid_call host allow-list (paid_call will pay whatever URL the agent names #404). The gate that decides which hosts may be paid. This one goes next because everything else about spending is narrower than it.
  2. feat(mcp): bound cumulative spend, audit payments, serve over HTTP #491 / fix(mcp): bound cumulative daily paid-call spend, not just per call #492 — cumulative spend ceiling and payment audit (The spend cap is per call, so it is not a cap #405, Nothing records what the agent spent #406). These bound and record what happens to hosts the allow-list permits.
  3. fix: classify MCP paid-call errors by structured signals #480 — structural error classification (Errors are classified by searching the message text #410).
  4. fix(mcp): bound tool result size, stream-cap upstream reads, and report truncation explicitly #489 — bounded tool results (Tool results are unbounded #416).
  5. feat: add lens_get_price tool returning aggregated pair price with issuer-strict assets #493 / feat: add lens_list_pairs, lens_get_pools and lens_screen_pairs tools #482 / feat: declare the resources capability and expose pairs, venues, OpenAPI spec, and status #494 — the new Lens tools and the resources capability (An agent cannot ask Lens for a price #408, No tool exposes pairs, pools or the screener #409, The server advertises only tools, not resources #418).
  6. test(mcp): add end-to-end 402 payment loop test against a stub facilitator #490 — the end-to-end 402 loop test (No test drives a real 402 payment loop #423).

Please do not all rebase at once. If you are below the one currently in flight, wait until the one above you lands — otherwise you rebase, I merge someone else, and you rebase again. I will comment on your PR when it is your turn.

One thing to settle between you: src/mcp/codes.ts is being extended by at least four of these, and #488 has already added a separate set of refusal reasons in src/net/ssrf.ts. We should end up with one vocabulary for "I refused to do that", not four. Whoever lands next, please look at what is already there and extend it rather than adding a parallel set — and say in your PR description what you added, so the next person can see it without reading the diff.

Nothing here reflects on the quality of the work. Several of these are among the best PRs this repo has had.

Miracle656 added a commit that referenced this pull request Oct 22, 2026
  tsc --noEmit   clean
  vitest         97 files, 1127 passing (up from 93 / 1068)
  dropped-tests  none

  #325  GET /movers for market gainers and losers
  #327  GET /correlation for basket construction
  #485  net arbitrage spread against fees
  #494  declare the resources capability and expose pages over MCP

Two fixes on merge.

#325 shipped the route file and the OpenAPI entry but never registered it.
The repo's own guard caught it:

  AssertionError: Documented but unregistered: ["GET /movers"]

So the endpoint was documented, generated into clients, and did not exist.
registerMoversRoutes already followed the house signature, so wiring it was an
import and a call next to registerScreenerRoutes. That guard is worth its
weight -- a documented route that 404s is worse than an undocumented one,
because a client is generated against it.

#327 added a console.error to src/routes/correlation.ts, which the logging
hygiene test forbids. Now createLogger, the same change #388 and #318 needed
in the previous batch. Three PRs in two batches have tripped that test, which
suggests the rule is not discoverable enough from the outside -- worth a line
in CONTRIBUTING rather than catching it per-PR.

NOT merged, each failing on its own branch rather than from merge order:
  #460  Phoenix AMM adapter -- 5 failures alone, including its own
        networkVenueConfig and venues/registry tests
  #353  1 tsc error     #354  1 tsc error     #482  4 tsc errors
  #393 #431 #434 #472 conflict

Claude-Session: https://claude.ai/code/session_01USgemLt4Rnz4SGB1Srf3GB

@Miracle656 Miracle656 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screened against current main and this does not pass on its own branch, so a rebase alone will not fix it.

Merged alone it gives type errors against main. Run npx tsc --noEmit on a fresh rebase and you will see them.

Please rebase, get npx tsc --noEmit and npx vitest run both clean locally, and re-request. I would rather take this than not — it is held on correctness, not on wanting it.

@Miracle656 Miracle656 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The design is right and I want this in. It needs a rebase, because the screener was extracted on main in the week since you branched — differently from how you extracted it here, and in the same file.

The principle is the one I'd have asked for

 * `lens_screen_pairs` — over the exact implementations the REST routes use
 * (`listPairs`, `listPools`, `screenPairs`), so a page from a tool is a page

That is exactly right, and it is the thing MCP tool PRs usually get wrong: re-querying the database from the tool layer, so lens_screen_pairs and GET /screener slowly stop agreeing about what sortBy=liquidity or limit=500 means. Routing the tools through the same functions means there is one answer and one place to fix it.

Pulling /pools out of src/api/rest.ts into src/routes/pools.ts is also a genuine improvement independent of the tools — rest.ts has been a grab-bag for a while.

What changed under you

main now has src/services/screener.ts, exporting runScreener. It arrived with the GraphQL screener query (#609), which needed the same thing you need: one implementation behind more than one surface. src/routes/screener.ts is now a thin handler that validates with a Zod schema and calls it:

return await runScreener({ network, filters, sortBy, order, cursor, limit })

Your branch extracts the same logic as screenPairs inside src/routes/screener.ts. So we have two extractions of one query, and git cannot reconcile them — merging main gives 266 conflicted lines across three regions in that file alone.

I stopped there rather than picking a winner by hand. This is the query behind published volume and ranking numbers, and a merge I resolve by choosing between two restructurings is exactly where a subtle wrong answer hides. You know what you intended; I would be guessing.

What the rebase needs

  1. Drop the screenPairs extraction. src/routes/screener.ts on main is already thin — leave it alone.
  2. Point lensTools.ts at the service: import { runScreener } from '../services/screener'. Note it is src/services/, not src/routes/.
  3. Check the argument shape. You currently go through stringifyScreenerArgs because the route parsed strings. runScreener takes typed values — { network, filters: { volume, change_24h_min, ... }, sortBy, order, cursor, limit } — so the MCP tool can pass numbers straight through and that helper can go. The route still stringifies only because query parameters arrive as strings; a tool's arguments do not.
  4. Keep listPairs and listPools. Neither exists on main — /pools is still inline in rest.ts and pairs.ts exports no listPairs — so those extractions are yours and conflict with nothing conceptually.
  5. src/mcp/server.ts and src/api/rest.ts have one conflict region each, both small.

Two things worth knowing that landed while you were out, because they affect the tool surface:

  • Limit clamping lives in runScreener, not the route: Math.min(SCREENER_MAX_LIMIT, Math.max(1, limit ?? SCREENER_DEFAULT_LIMIT)). Pass limit through and the tool is clamped identically to REST and GraphQL for free. Do not re-clamp in the tool — one clamp or they drift.
  • sortBy and order are allow-listed inside the service before being interpolated into the SQL, since a column name cannot be a bind parameter. That protection is in runScreener, so going through it is also what keeps the tool from becoming an injection surface. Worth a test asserting lens_screen_pairs with sortBy: "price; DROP TABLE" is refused rather than passed along.

After the rebase: npx tsc --noEmit, npm test, and if you touch openapi.yaml at all, npm run openapi:gen plus npx --yes openapi-typescript@7.13.0 openapi.yaml -o clients/typescript/src/schema.d.ts — both are byte-compared in CI now.


Separately: #423 is closed and credited to you. You were right that the work was already on main — it went in with #487 (94bd8ea), which is why #490's branch was empty after the rebase. I checked it rather than taking it on trust, and the sigValidOnMainnet: false assertion in that test is the best thing in it; details on the issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

No tool exposes pairs, pools or the screener

2 participants