Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/pages.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,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' }
Expand Down
95 changes: 95 additions & 0 deletions src/pages/partner-sdks/cloudflare-agents.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
---
description: "Use mppx to let a Cloudflare Agent pay for MPP-enabled MCP tools."
imageDescription: "Connect Cloudflare Agents to paid MPP MCP tools"
---

# Cloudflare Agents [Pay for MCP tools from your agent]

Use [`McpClient.wrap`](/sdk/typescript/client/McpClient.wrap) to let a [Cloudflare Agent](https://developers.cloudflare.com/agents/) call free and paid MCP tools through the same MCP client. Free tools pass through untouched. When a paid tool returns an MPP Challenge, the wrapped client creates a Credential, retries the call, and returns the result—the Agent never needs to know which tools are paid.

## Install

:::code-group
```bash [npm]
$ npm install agents mppx accounts
```
```bash [pnpm]
$ pnpm add agents mppx accounts
```
```bash [bun]
$ bun add agents mppx accounts
```
:::

## Wrap the MCP client

Connect an MCP server in `onStart`, then inject payment handling into the connected client. Payments are authorized inside your wallet through the `wallet_authorizeChallenge` RPC using your [Tempo Accounts](https://accounts.tempo.xyz/docs) access keys—mppx discovers wallet support via `wallet_getCapabilities` and falls back to local signing for accounts that aren't wallet-backed.

```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<Env> {
async onStart() {
const { id } = await this.mcp.connect(this.env.MCP_SERVER_URL)
const { account, getClient } = await connectWallet(this.env)
McpClient.wrap(this.mcp.mcpConnections[id].client, {
methods: [tempo({ account, getClient })],
onPaymentRequired: (challenge) => Number(challenge.request.amount) < 1_000_000, // approve based on amount/intent
})
}
}
```

`McpClient.wrap` mutates the client in place, so there is nothing to store: every surface built on the connection—including `this.mcp.getAITools(...)` and `this.mcp.callTool(...)`—stays payment-aware. Paid calls expose the MPP Receipt on `result.receipt`. `onPaymentRequired` is the spend-policy / human-in-the-loop hook: it runs before each Credential is created, and returning (or resolving) `false` declines the payment—`callTool` rejects with `Payment declined.`

:::note
If you also use Cloudflare's `withX402Client`, apply `McpClient.wrap` first—each wrapper then handles only its own protocol's challenges (the reverse order breaks tool calls).
:::

## Accounts access keys

`connectWallet` runs a headless [Tempo Accounts](https://accounts.tempo.xyz/docs) wallet next to the Agent: the `secp256k1` adapter pins a server-side key, `wallet_connect` authorizes an access key, and wrapping the provider in a viem client gives `tempo(...)` a `json-rpc` account whose challenges route to the wallet:

```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}` }),
authorizeAccessKey: { 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) }) }
}
```

Access keys are chain-scoped—the authorized chain must match the chain ID in the MPP Challenge. `Storage.memory()` re-authorizes a key on each cold start; wrap KV or a Durable Object with `Storage.from(...)` to persist keys. The provider also exposes pre-wired method clients at `provider.mpp` for direct use. For provisioning accounts and access-key policies (spend limits, scopes, expiry), see the [Accounts docs](https://accounts.tempo.xyz/docs).

## Local development with a viem account

For local development or a simple server wallet, skip Accounts and pass a viem account directly (store the key with `npx wrangler secret put MPP_PRIVATE_KEY`):

```ts
import { tempo } from 'mppx/client'
import { McpClient } from 'mppx/mcp/client'
import { privateKeyToAccount } from 'viem/accounts'

McpClient.wrap(mcpClient, {
methods: [tempo({ account: privateKeyToAccount(env.MPP_PRIVATE_KEY as `0x${string}`) })],
})
```

## Related

- [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)
8 changes: 8 additions & 0 deletions src/pages/quickstart/agent.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand Down Expand Up @@ -223,4 +225,10 @@ $ mppx https://mpp.dev/api/ping/paid
title="Wallets"
to="/tools/wallet"
/>
<Card
description="Pay for MPP-enabled services from a Cloudflare Agent"
icon="lucide:cloud"
title="Cloudflare Agents"
to="/partner-sdks/cloudflare-agents"
/>
</Cards>
47 changes: 39 additions & 8 deletions src/pages/sdk/typescript/client/McpClient.wrap.mdx
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
# `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

```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' })
Expand All @@ -20,20 +23,29 @@ const result = await mcp.callTool({ name: 'premium_tool', arguments: {} })
// @log: { content: [...], receipt: { ... } }
```

`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<client, methods> = Omit<client, 'callTool'> & {
Expand All @@ -43,6 +55,7 @@ type McpClient<client, methods> = Omit<client, 'callTool'> & {
name: string
_meta?: Record<string, unknown>
},
resultSchema?: CallToolResultSchema,
options?: CallToolOptions<methods>,
) => Promise<CallToolResult>
}
Expand All @@ -62,10 +75,28 @@ type CallToolResult = Awaited<ReturnType<Client['callTool']>> & {

- **Type:** `Pick<Client, 'callTool'>`

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<boolean>`

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<ChallengeCandidate[]>`

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.
9 changes: 9 additions & 0 deletions vocs.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down