Skip to content

feat(mcp): bound cumulative spend, audit payments, serve over HTTP, test all tools - #522

Closed
AGWAM001 wants to merge 4 commits into
Divinity-Labs:mainfrom
AGWAM001:mcp-spend-controls-and-tool-coverage
Closed

AGWAM001 wants to merge 4 commits into
Divinity-Labs:mainfrom
AGWAM001:mcp-spend-controls-and-tool-coverage

Conversation

@AGWAM001

Copy link
Copy Markdown

Summary

  • Cumulative spend ceiling (src/mcp/budget.ts): paid_call reserves each call's per-call maximum before the payment runs and settles or releases it after, against a UTC daily ceiling. State survives restarts; an unreadable state file is treated as fully spent, not as a clean slate.
  • Payment audit ledger (src/mcp/ledger.ts): every paid_call attempt is appended to a JSONL trail with outcome, error code, settlement hash and spend class. Signed envelopes and secret material are never recorded; the server refuses to spend when a durable ledger cannot be written. Prometheus counters use a bounded label set.
  • Streamable HTTP transport: the same tools served over HTTP behind a mandatory bearer token (refuses to start unauthenticated), one protocol server per stateless request, sharing the budget and ledger.
  • Metrics endpoint (GET /metrics on the HTTP transport, same bearer token), with the network label defaulted to the configured signers' networks.
  • Tool test coverage for Only the error paths of one tool are tested #421: bazaar_search (with/without query, empty, non-2xx, malformed body, transport failure), ListTools (advertised set, SDK schema parse, per-tool inputSchema validation), and the unknown-tool path. Simulated upstream errors now use the shapes the real libraries throw - the @x402/fetch Failed to create payment payload: ... wrapper around the @x402/stellar simulation guard, undici's TypeError: fetch failed with cause, and a Horizon BadResponse whose op_no_trust signal lives in extras.result_codes (pinned as a characterization test of the substring classifier's blind spot, not an endorsement).

Related issue

Closes #405
Closes #406
Closes #419
Closes #421
Addresses #425

Supersedes the closed-unmerged #491. Note for maintainers: the spend-ceiling commits duplicate open #492 (same author) and the HTTP transport overlaps the #419 scope of open #450 - merging this can make both redundant.

Type of change

  • New feature
  • Tests
  • Docs

Checklist

  • I have read CONTRIBUTING.md
  • npx tsc --noEmit passes
  • npm run build passes
  • I added / updated tests where relevant - vitest run tests/mcp.test.ts: 40/40
  • I updated docs where relevant - README MCP section, .env.example, three changesets

paid_call signs with a real mainnet key, and maxPrice only bounds one mistake —
a caller in a retry loop could spend without limit. Add a cumulative USD ceiling
that reserves each call's maximum before the payment runs and settles it after,
record every attempt in an audit ledger, and refuse to spend at all when a
durable ledger was asked for and cannot be written. The same tools are also
served over Streamable HTTP, which refuses to start without a bearer token since
it exposes paid_call to the network.

Cover Divinity-Labs#421: bazaar_search, ListTools and the unknown-tool path had no tests, and
the simulated upstream errors were plain strings that hid how handlePaidCallError
classifies on lowercased message substrings. Fixtures now use the shapes that
@x402/fetch, @x402/core and @x402/stellar really throw, including one Horizon
error whose op_no_trust signal is invisible to that matcher.
@AGWAM001 AGWAM001 closed this Oct 18, 2026
@AGWAM001
AGWAM001 deleted the mcp-spend-controls-and-tool-coverage branch October 18, 2026 21:00
AGWAM001 and others added 3 commits October 18, 2026 20:57
The payment ledger registered mcp_payment_* counters on the shared registry,
but the MCP process serves no listener that reads it, so the spend of an agent
was still only recoverable from the audit file. Serve GET /metrics from the MCP
HTTP transport, on the same bearer token as paid_call, and refuse to start when
MCP_HTTP_PATH would put the MCP handshake on that path.

The network label was always unknown in the shipped server because run.ts only
passed the host allow-list. Default the network domain to the two networks the
signer is configured for, overridable with MCP_METRIC_NETWORKS, so a series per
network exists without opening the label to whatever a seller echoes.
…ibraries throw

The original four fixtures were duck-typed stubs: bare Error strings and
plain-object responses. Rewire them on the actual chain: the @x402/stellar
simulation guard wrapped by @x402/fetch's payment-payload frame, and real
Response objects for the 402 and success paths. Drop the hand-rolled
handler captures for the file's createHarness.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant