diff --git a/src/pages.gen.ts b/src/pages.gen.ts index fa64de59..e4f07228 100644 --- a/src/pages.gen.ts +++ b/src/pages.gen.ts @@ -38,6 +38,7 @@ type Page = | { path: '/intents/subscription'; render: 'static' } | { path: '/mpp-vs-x402'; render: 'static' } | { path: '/overview'; render: 'static' } + | { path: '/partner-sdks/cloudflare-agents'; render: 'static' } | { path: '/payment-methods/card/charge'; render: 'static' } | { path: '/payment-methods/card'; render: 'static' } | { path: '/payment-methods/custom'; render: 'static' } diff --git a/src/pages/partner-sdks/cloudflare-agents.mdx b/src/pages/partner-sdks/cloudflare-agents.mdx new file mode 100644 index 00000000..f947cf2c --- /dev/null +++ b/src/pages/partner-sdks/cloudflare-agents.mdx @@ -0,0 +1,117 @@ +--- +description: "Use mppx to let a Cloudflare Agent pay for MPP-enabled MCP tools and HTTP requests." +imageDescription: "Connect Cloudflare Agents to paid MPP MCP tools and HTTP APIs" +--- + +# Cloudflare Agents [Pay for MCP tools and HTTP requests from your agent] + +Use [`McpClient.wrap`](/sdk/typescript/client/McpClient.wrap) and [`Mppx.create`](/sdk/typescript/client/Mppx.create) to let a [Cloudflare Agent](https://developers.cloudflare.com/agents/) pay for MCP tools and HTTP requests through MPP. Free calls pass through untouched; when a paid tool or a 402-protected request returns an MPP Challenge, the wrapped client creates a Credential, retries the call, and returns the result. A [Tempo Accounts](https://accounts.tempo.xyz/docs) wallet settles each payment with a scoped access key. + +## Install + +:::code-group +```bash [npm] +$ npm install agents mppx accounts +``` +```bash [pnpm] +$ pnpm add agents mppx accounts +``` +```bash [bun] +$ bun add agents mppx accounts +``` +::: + +## Connect a wallet + +Both payment paths share one wallet. `connectWallet` runs a headless [Tempo Accounts](https://accounts.tempo.xyz/docs) wallet beside the Agent and returns the `account` and `getClient` you pass to `tempo(...)`: + +```ts [src/accounts.ts] +import { Expiry, Provider, Storage, secp256k1 } from 'accounts' +import { createClient, custom } from 'viem' +import { tempo } from 'viem/tempo/chains' + +export async function connectWallet(env: { ACCOUNTS_PRIVATE_KEY: string }) { + const provider = Provider.create({ + adapter: secp256k1({ privateKey: env.ACCOUNTS_PRIVATE_KEY as `0x${string}` }), + accessKey: { authorize: { expiry: Expiry.days(7) } }, + storage: Storage.memory(), + }) + const { accounts: [{ address: account }] } = await provider.request({ method: 'wallet_connect' }) + return { account, getClient: () => createClient({ account, chain: tempo, transport: custom(provider) }) } +} +``` + +`Storage.memory()` re-authorizes a key on each cold start; wrap KV or a Durable Object with `Storage.from(...)` to persist keys across restarts. + +## Pay for MCP tools + +Connect an MPP-enabled MCP server in `onStart`, then wrap the connected client. `McpClient.wrap` mutates it in place and returns the same client, retyped so `this.mcp.getAITools(...)` and `this.mcp.callTool(...)` become payment-aware—paid calls carry the MPP Receipt on `result.receipt`. + +```ts [src/index.ts] +import { Agent } from 'agents' +import { tempo } from 'mppx/client' +import { McpClient } from 'mppx/mcp/client' +import { connectWallet } from './accounts' + +type Env = { MCP_SERVER_URL: string; ACCOUNTS_PRIVATE_KEY: string } + +export class MyAgent extends Agent { + async onStart() { + const { id } = await this.mcp.connect(this.env.MCP_SERVER_URL) + const { account, getClient } = await connectWallet(this.env) + const client = McpClient.wrap(this.mcp.mcpConnections[id].client, { + methods: [tempo({ account, getClient })], + onPaymentRequired: (challenge) => Number(challenge.request.amount) < 1_000_000, // per-call spend policy + }) + + // Every tool call is now payment-aware; paid calls carry an MPP Receipt. + const result = await client.callTool({ name: 'premium_search', arguments: { query: 'tempo' } }) + console.log(result.receipt) + } +} +``` + +`onPaymentRequired` runs before each payment; return `false` to decline (the call rejects with `Payment declined.`). + +## Pay for HTTP requests + +The same wallet pays for plain HTTP requests. `Mppx.create` returns a payment-aware `fetch` that automatically pays an MPP 402 and retries. Call it from a request context (Workers forbid I/O at module top level): + +```ts [src/pay.ts] +import { Mppx, tempo } from 'mppx/client' +import { connectWallet } from './accounts' + +export async function payFetch(env: { ACCOUNTS_PRIVATE_KEY: string }, url: string) { + const { account, getClient } = await connectWallet(env) + const mppx = Mppx.create({ + methods: [tempo({ account, getClient })], + polyfill: false, // Workers' globalThis.fetch is read-only, so wrap the fetch you call + }) + return mppx.fetch(url) // auto-pays an MPP 402 +} +``` + +Pass `onChallenge` to inspect a challenge (e.g. `challenge.request.amount`) before it pays. + +## Environment setup + +Store the wallet key as a [Workers secret](https://developers.cloudflare.com/workers/configuration/secrets/) and fund it with testnet tokens while developing: + +```bash [terminal] +$ npx wrangler secret put ACCOUNTS_PRIVATE_KEY +``` + +For local development, you can skip Accounts and pass a viem account straight to `tempo({ account })`—see [`McpClient.wrap`](/sdk/typescript/client/McpClient.wrap). + +## Access keys vs. your root key + +`connectWallet` authorizes a scoped, revocable [access key](https://accounts.tempo.xyz/docs) and pays every tool call and HTTP request with it—never your account's root key. Set its expiry once and revoke it any time; inspect or gate each payment with `onPaymentRequired` (MCP) or `onChallenge` (HTTP). If you add on-chain spend limits to the key, size them to include network fees, not just the charge amount. + +## Related + +- [`Mppx.create`](/sdk/typescript/client/Mppx.create) and [`McpClient.wrap`](/sdk/typescript/client/McpClient.wrap) +- [Cloudflare MCP client API](https://developers.cloudflare.com/agents/model-context-protocol/apis/client-api/) +- [Cloudflare MPP payment overview](https://developers.cloudflare.com/agents/tools/payments/mpp/) +- [Charge for HTTP content with MPP](https://developers.cloudflare.com/agents/tools/payments/mpp-charge-for-http-content/) +- [Use with agents](/quickstart/agent) + diff --git a/src/pages/quickstart/agent.mdx b/src/pages/quickstart/agent.mdx index b82dd650..6c2893cc 100644 --- a/src/pages/quickstart/agent.mdx +++ b/src/pages/quickstart/agent.mdx @@ -9,6 +9,8 @@ import { Badge, Card, Cards, Tab, Tabs } from 'vocs' Agents can automatically interact with MPP-enabled services, paying for API calls without human intervention. Learn more about [agentic payments](/use-cases/agentic-payments) or get started below. +If you're building an agent with a partner SDK, see [Cloudflare Agents](/partner-sdks/cloudflare-agents) for a framework-specific integration. + | Tool | Best for | Setup | |------|----------|-------| | [Tempo Wallet](#tempo-wallet) | MPP services with spend controls and service discovery | `tempo wallet login` | @@ -223,4 +225,10 @@ $ mppx https://mpp.dev/api/ping/paid title="Wallets" to="/tools/wallet" /> + diff --git a/src/pages/sdk/typescript/client/McpClient.wrap.mdx b/src/pages/sdk/typescript/client/McpClient.wrap.mdx index 92aa07ca..d93e0bac 100644 --- a/src/pages/sdk/typescript/client/McpClient.wrap.mdx +++ b/src/pages/sdk/typescript/client/McpClient.wrap.mdx @@ -2,7 +2,9 @@ import { Tab, Tabs } from 'vocs' # `McpClient.wrap` [Payment-aware MCP client] -Wraps an MCP SDK client with automatic payment handling. When a tool call returns a `-32042` payment required error, the wrapper creates a Credential and retries the call. +Adds automatic payment handling to an MCP SDK client in place. When a tool call returns a `-32042` payment required error—or a tool result carrying payment-required metadata—the client creates a Credential and retries the call. + +`McpClient.wrap` mutates the provided client and returns the same reference, so there is nothing to store: surfaces built on the original client stay payment-aware. This includes setups where another SDK owns the MCP client reference, for example [Cloudflare Agents](/partner-sdks/cloudflare-agents). Calling `wrap` on the same client again replaces its payment configuration. ## Usage @@ -12,7 +14,8 @@ Wraps an MCP SDK client with automatic payment handling. When a tool call return ```ts import { Provider } from 'accounts' import { Client } from '@modelcontextprotocol/sdk/client' -import { McpClient, tempo } from 'mppx/mcp-sdk/client' +import { tempo } from 'mppx/client' +import { McpClient } from 'mppx/mcp/client' const client = new Client({ name: 'my-client', version: '1.0.0' }) await client.connect(transport) @@ -36,7 +39,8 @@ const result = await mcp.callTool({ name: 'premium_tool', arguments: {} }) ```ts import { Client } from '@modelcontextprotocol/sdk/client' -import { McpClient, tempo } from 'mppx/mcp-sdk/client' +import { tempo } from 'mppx/client' +import { McpClient } from 'mppx/mcp/client' import { privateKeyToAccount } from 'viem/accounts' const client = new Client({ name: 'my-client', version: '1.0.0' }) @@ -53,20 +57,29 @@ const result = await mcp.callTool({ name: 'premium_tool', arguments: {} }) +`mcp` is the same object as `client`, retyped with the payment-aware `callTool`. + ### With call options -Pass `context` and `timeout` through the second argument to `callTool`. +`callTool` keeps the MCP SDK's `(params, resultSchema?, options?)` signature—pass `context`, a per-call `onPaymentRequired` approval hook, and a request `timeout` through the third argument. `context` and `onPaymentRequired` are stripped before the remaining request options are forwarded to the SDK. ```ts const result = await mcp.callTool( { name: 'premium_tool', arguments: { query: 'hello' } }, - { context: { foo: 'bar' }, timeout: 30_000 }, + undefined, + { + context: { foo: 'bar' }, + onPaymentRequired: (challenge) => Number(challenge.request.amount) < 1_000_000, + timeout: 30_000, + }, ) ``` +A per-call `onPaymentRequired` overrides the configured hook; pass `null` to bypass a configured hook for one call. + ## Return type -`McpClient.wrap` returns an object that spreads the original client and overrides `callTool` with a payment-aware version. +`McpClient.wrap` returns the same client reference, with `callTool` retyped to the payment-aware version. The MCP SDK's three-argument `callTool` signature is preserved, so surfaces built on it—such as Cloudflare's `MCPClientManager.callTool(params, resultSchema, options)`—keep working. ```ts type McpClient = Omit & { @@ -76,6 +89,7 @@ type McpClient = Omit & { name: string _meta?: Record }, + resultSchema?: CallToolResultSchema, options?: CallToolOptions, ) => Promise } @@ -95,10 +109,28 @@ type CallToolResult = Awaited> & { - **Type:** `Pick` -The MCP SDK client instance to wrap. Must have a `callTool` method—typically an instance of `Client` from `@modelcontextprotocol/sdk/client`. +The MCP SDK client instance to mutate. Must have a `callTool` method—typically an instance of `Client` from `@modelcontextprotocol/sdk/client`. ### config.methods -- **Type:** `readonly Method.AnyClient[]` +- **Type:** `readonly (Method.AnyClient | readonly Method.AnyClient[])[]` + +Array of payment methods to use when handling payment Challenges. Accepts individual method clients or tuples (e.g. from `tempo()`). The client matches Challenges from the server against installed methods by name and intent. + +### config.onPaymentRequired + +- **Type:** `(challenge: Challenge.Challenge) => boolean | Promise` + +Optional approval hook called before creating a payment credential. Receives the selected Challenge; return `false` to decline—`callTool` then rejects with `Payment declined.` + +### config.orderChallenges + +- **Type:** `(candidates: ChallengeCandidate[]) => ChallengeCandidate[] | Promise` + +Optional. Filters and sorts the supported Challenges before a Credential is created; the first candidate is used. + +### config.paymentPreferences + +- **Type:** `AcceptPayment.Config` -Array of payment methods to use when handling payment Challenges. The wrapper matches Challenges from the server against installed methods by name and intent. +Optional. Client-declared supported payment methods, keyed by typed `method/intent` strings. diff --git a/vocs.config.ts b/vocs.config.ts index 03e702d8..6aefaccc 100644 --- a/vocs.config.ts +++ b/vocs.config.ts @@ -289,6 +289,15 @@ export default defineConfig({ { text: "Use with your app", link: "/quickstart/client" }, ], }, + { + text: "Use with partner SDKs", + items: [ + { + text: "Cloudflare Agents", + link: "/partner-sdks/cloudflare-agents", + }, + ], + }, { text: "Guides", items: [