Skip to content

feat: declare the resources capability and expose pairs, venues, OpenAPI spec, and status - #494

Merged
Miracle656 merged 7 commits into
Divinity-Labs:mainfrom
Jonathan-ai9:feat/mcp-resources
Oct 22, 2026
Merged

Miracle656 merged 7 commits into
Divinity-Labs:mainfrom
Jonathan-ai9:feat/mcp-resources

Conversation

@Jonathan-ai9

Copy link
Copy Markdown
Contributor

Closes #418

Summaryfeat(mcp): declare the resources capability and expose pairs, venues, OpenAPI spec, and status

Related issue

## Summary

Lens advertised only tools, so agents had to call a function every time they wanted data that rarely changes. This PR declares the MCP resources capability and implements resources/list and resources/read for the stable, cacheable content: watched pairs, venues, the OpenAPI document, and service status. Each resource has a stable URI, an accurate MIME type, and honest freshness information.

Why

Exposing this data as resources lets an agent attach it to its context once instead of calling a tool repeatedly. That is cheaper for the agent (fewer tool calls, less context churn) and for the service (fewer upstream requests). Freshness metadata lets the agent tell whether what it cached is still current.

Changes

  • Declare capabilities: { tools: {}, resources: {} } on the server

  • Implement resources/list and resources/read handlers

  • Expose these resources with stable URIs:

    URI Content MIME type
    lens://pairs Watched pairs application/json
    lens://venues Venue list application/json
    lens://openapi OpenAPI document application/json (or application/yaml if served as YAML)
    lens://status Current service status application/json
  • Each resource has a name, description, and mimeType in the listing

  • Freshness: every read result includes a version and last-modified value derived from the real data (content hash for pairs/venues/OpenAPI, upstream timestamp where one exists, generation time for status), not the time of the request

  • lens://status is marked as volatile in its description so agents do not cache it for long; the others document their expected change frequency

  • Unknown URIs return a proper MCP "resource not found" error, with no internal details leaked

  • Resource payloads go through the same size cap as tool results (see Tool results are unbounded #416), with truncation stated explicitly

  • Document the URIs, MIME types, and freshness semantics in the README and in each resource description

Tests

  • Server initialisation reports the resources capability
  • resources/list returns all four resources with URI, name, and MIME type
  • resources/read for lens://pairs returns valid JSON with the expected MIME type
  • resources/read for lens://openapi returns the OpenAPI document
  • Freshness value is stable across reads when content is unchanged, and changes when content changes
  • Unknown URI returns a resource-not-found error
  • Existing tool tests still pass

Notes

  • Out of scope: resources/subscribe and listChanged notifications. Freshness metadata covers the cache-validity need for now, and subscriptions can follow if wanted.
  • URIs are now part of the public interface, so they should be treated as stable; renames would be a breaking change.
  • Shares the size-cap behaviour from Tool results are unbounded #416; if that PR lands first, this one should rebase onto it.

Type of change

  • Bug fix
  • New feature
  • Refactor
  • Docs
  • Tests
  • CI / tooling

Checklist

  • I have read CONTRIBUTING.md
  • npx tsc --noEmit passes
  • npm run build passes
  • I added / updated tests where relevant
  • I updated docs where relevant

bazaar_search passed any caller-supplied URL straight to fetch after a
trailing-slash trim, letting the agent reach metadata endpoints or private
services from the operator's machine.

Parse the URL and reject non-http(s); resolve the host and refuse if any
address is private, loopback, link-local or metadata; follow redirects
manually and re-validate each hop. Hosts can be narrowed further via a
shared OUTBOUND_ALLOWED_HOSTS list that never re-opens a blocked address.
Blocked requests return ERR_URL_BLOCKED.
A tool result is written straight into the agent's context window, so an
unbounded one is self-inflicted denial of service whose failure gets blamed
on the model. bazaar_search and paid_call now serialise compactly (no
indentation), page collections with a total and a notice explaining how to
ask for more, and read the response body through a capped stream reader that
cancels the stream instead of buffering a hostile body in full. Truncation is
always stated in the result; an oversized discovery body returns
ERR_RESPONSE_TOO_LARGE.

Bounds are operator-configurable via MCP_MAX_RESULT_BYTES,
MCP_MAX_UPSTREAM_BYTES, MCP_DEFAULT_PAGE_LIMIT and MCP_MAX_PAGE_LIMIT.
…bs#418)

Lens facts that barely change — watched pairs, venues, the OpenAPI
document, service status — forced an agent to call a tool every turn to
keep them in context. Expose them as MCP resources instead, each with a
stable URI, an accurate MIME type, and honest freshness: a content-hash
version plus a lastModified per source, so a client can tell whether what
it cached is still current.

Status is regenerated per read and so is deliberately never cacheable.
Default sources read only config and the on-disk OpenAPI document, keeping
the stdio server free of a database dependency; a host with richer runtime
state can inject its own descriptors.
server.ts now imports ./limits.js, so the resources change set cannot be
based on main on its own. Note the merge order and the retarget so a
reviewer knows why the branch starts where it does.
@drips-wave

drips-wave Bot commented Oct 18, 2026

Copy link
Copy Markdown

@Jonathan-ai9 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

Copy link
Copy Markdown
Collaborator

#487 has landed — the paid_call host allow-list. You are next in the MCP queue; please rebase on current main.

I resolved #487's conflicts myself because nine PRs were stuck behind it. Three things on main now that will shape your rebase:

1. Two refusal codes, deliberately separate.

ERR_URL_BLOCKED       // the target resolves somewhere we will never fetch (#488)
ERR_HOST_NOT_ALLOWED  // reachable, but not on the paid_call allow-list (#487)

If your PR adds a refusal, extend this vocabulary rather than starting a third. "I cannot reach there" and "I will not pay there" are different answers; whatever you add should be a third distinct answer, not a synonym for one of these.

2. The constructor takes config_, not config — renamed to stop it shadowing the imported config. Both gates initialise there: paidCallRules and bazaarFetchOptions.

3. src/mcp/ is exempt from the console.* guard, and must stay exempt. The server speaks its protocol over stdout via StdioServerTransport, and pino writes to stdout by default — routing MCP diagnostics through the logger would interleave log lines with protocol frames and break every client parsing them. Use console.error there. Everywhere else in the repo, use the logger; the guard will tell you.

Push the rebase and I will review it. If it conflicts in a way that is not obvious, say so and I will resolve it rather than leave you stuck.

Jonathan-ai9 and others added 3 commits October 20, 2026 16:28
Both merge commits on this branch ("Merge branch 'main' into
feat/mcp-resources", 4a553a6 and its first parent baf94a8) were resolved in
GitHub's browser conflict editor, which kept both sides' lines in place of
choosing one: `src/mcp/server.ts` imported the same symbols twice, left
`export interface McpLimits {` unclosed in front of main's
`readPackageVersion` import block, and put class fields inside the
constructor body. Nine TS1xxx syntax errors, and no conflict markers left to
point at the seam.

The tip tree is now the merge git's own three-way machinery produces: main
plus Divinity-Labs#416's result bounds plus this PR's resources, each side's gates intact.
History is untouched — no rebase, no force-push.
@Jonathan-ai9

Copy link
Copy Markdown
Contributor Author

CI on this branch was red from npx tsc --noEmit with 9 syntax errors in
src/mcp/server.ts. Not a code problem — a conflict-resolution problem, and it
predates the merge you'd reach for.

Both merges on this branch were resolved in GitHub's browser editor, and each
kept both sides' lines rather than choosing one: duplicate imports of
CallToolRequestSchema/MCP_ERROR_CODES, export interface McpLimits { left
unclosed directly in front of main's readPackageVersion import block, and
main's paidCallRules/bazaarFetchOptions/budget assignments landing inside
the constructor body. baf94a8 (Oct 20) is already broken this way, so the
branch has been red since before 4a553a6.

Fixed in 6920034 by redoing the merge with git rather than hand-patching: main +
#416's result bounds + this PR's resources, following your three constraints —
config_ kept, both gates initialised in the constructor, resources handlers
added without a third refusal code in MCP_ERROR_CODES. Fast-forward commit on
top; I did not rewrite either GitHub-authored merge. If you'd rather the history
read cleanly, 4cae791 is the same content as a single replay of #418 onto
#416 (25a7dbe) and your call on whether to take it.

Local: tsc clean, build clean, 964 passed / 1 skipped, dropped-tests guard
877 -> 907 with nothing dropped.

One decision this surfaced: #418 now exists on two branches. feat/mcp-resources
is #416+#418 stacked (this PR, now green). feat/mcp-resources-418 @ 7a4a48a is
#418 alone on current main — 1 commit, 7 files, able to merge, and it needs
neither #416 nor either of these merges, because the resources code never touches
./limits.js. Say which you want reviewed and I'll close the other; I'd take the
unstacked one, since it keeps #418 out of your MCP queue behind #416.

@Miracle656
Miracle656 merged commit c0dd32f into Divinity-Labs:main Oct 22, 2026
2 checks passed
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
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.

The server advertises only tools, not resources

2 participants