Skip to content
Merged
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
6 changes: 6 additions & 0 deletions .changeset/tired-birds-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"hono-agents": patch
"agents": patch
---

update dependencies
2 changes: 1 addition & 1 deletion biome.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.3.4/schema.json",
"$schema": "https://biomejs.dev/schemas/2.3.7/schema.json",
"assist": {
"actions": {
"source": {
Expand Down
40 changes: 26 additions & 14 deletions docs/mcp-servers.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@ export class TinyMcp extends McpAgent {
server = new McpServer({ name: "", version: "v1.0.0" });

async init() {
this.server.tool(
this.server.registerTool(
"square",
"Squares a number",
{ number: z.number() },
{
description: "Squares a number",
inputSchema: { number: z.number() }
},
async ({ number }) => ({
content: [{ type: "text", text: String(number ** 2) }]
})
}
);
}
}
Expand Down Expand Up @@ -91,12 +93,14 @@ export class StorageMcp extends McpAgent {
content: [{ type: "text" as const, text }]
});

this.server.tool(
this.server.registerTool(
"writeFile",
"Store text as a file with the given path",
{
path: z.string().describe("Absolute path of the file"),
content: z.string().describe("The content to store")
description: "Store text as a file with the given path",
inputSchema: {
path: z.string().describe("Absolute path of the file"),
content: z.string().describe("The content to store")
}
},
async ({ path, content }) => {
try {
Expand All @@ -108,11 +112,13 @@ export class StorageMcp extends McpAgent {
}
);

this.server.tool(
this.server.registerTool(
"readFile",
"Read the contents of a file",
{
path: z.string().describe("Absolute path of the file to read")
description: "Read the contents of a file",
inputSchema: {
path: z.string().describe("Absolute path of the file to read")
}
},
async ({ path }) => {
const obj = await env.BUCKET.get(path);
Expand All @@ -126,9 +132,15 @@ export class StorageMcp extends McpAgent {
}
);

this.server.tool("whoami", "Check who the user is", async () => {
return textRes(`${this.props?.userId}`);
});
this.server.registerTool(
"whoami",
{
description: "Check who the user is"
},
async () => {
return textRes(`${this.props?.userId}`);
}
);
}
}

Expand Down
2 changes: 1 addition & 1 deletion examples/a2a/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"author": "Matt Carey <[email protected]>",
"dependencies": {
"@a2a-js/sdk": "^0.2.2",
"hono": "^4.10.4"
"hono": "^4.10.6"
},
"keywords": [],
"scripts": {
Expand Down
3 changes: 1 addition & 2 deletions examples/mcp-elicitation/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,7 @@ export class MyAgent extends Agent<Env, State> {
amount: {
type: "number",
title: "Amount",
description: "The amount to increase the counter by",
minLength: 1
description: "The amount to increase the counter by"
}
},
required: ["amount"]
Expand Down
2 changes: 1 addition & 1 deletion examples/mcp-worker-authenticated/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@
"deploy": "wrangler deploy"
},
"dependencies": {
"hono": "^4.10.4"
"hono": "^4.10.6"
}
}
19 changes: 11 additions & 8 deletions examples/mcp-worker-authenticated/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ const server = new McpServer({
version: "1.0.0"
});

server.tool(
server.registerTool(
"hello",
"Returns a greeting message",
{ name: z.string().optional() },
{
description: "Returns a greeting message",
inputSchema: { name: z.string().optional() }
},
async ({ name }) => {
const auth = getMcpAuthContext();
const username = auth?.props?.username as string | undefined;
Expand All @@ -28,10 +30,11 @@ server.tool(
}
);

server.tool(
server.registerTool(
"whoami",
"Returns information about the authenticated user",
{},
{
description: "Returns information about the authenticated user"
},
async () => {
const auth = getMcpAuthContext();

Expand All @@ -40,7 +43,7 @@ server.tool(
content: [
{
text: "No authentication context available",
type: "text"
type: "text" as const
}
]
};
Expand All @@ -58,7 +61,7 @@ server.tool(
null,
2
),
type: "text"
type: "text" as const
}
]
};
Expand Down
8 changes: 5 additions & 3 deletions examples/mcp-worker/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ const server = new McpServer({
version: "1.0.0"
});

server.tool(
server.registerTool(
"hello",
"Returns a greeting message",
{ name: z.string().optional() },
{
description: "Returns a greeting message",
inputSchema: { name: z.string().optional() }
},
async ({ name }) => {
return {
content: [
Expand Down
10 changes: 6 additions & 4 deletions examples/mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,14 @@ export class MyMCP extends McpAgent<Env> {
// ...
});

this.server.tool(
this.server.registerTool(
"add",
"Add to the counter, stored in the MCP",
{ a: z.number() },
{
description: "Add to the counter, stored in the MCP",
inputSchema: { a: z.number() }
},
async ({ a }) => {
// ...
// add your logic here
}
);
}
Expand Down
2 changes: 1 addition & 1 deletion examples/mcp/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"author": "",
"dependencies": {
"mcp-remote": "^0.1.30"
"mcp-remote": "^0.1.31"
},
"keywords": [],
"name": "@cloudflare/agents-mcp-example",
Expand Down
8 changes: 5 additions & 3 deletions examples/mcp/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,12 @@ export class MyMCP extends McpAgent<Env, State, {}> {

// Register tool - Note: Current MCP SDK doesn't support icons in tool method yet
// Icons are supported at the server implementation level
this.server.tool(
this.server.registerTool(
"add",
"Add to the counter, stored in the MCP",
{ a: z.number() },
{
description: "Add to the counter, stored in the MCP",
inputSchema: { a: z.number() }
},
async ({ a }) => {
this.setState({ ...this.state, counter: this.state.counter + a });

Expand Down
103 changes: 103 additions & 0 deletions examples/x402-mcp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# x402 MCP Example

This example demonstrates how to create paid MCP tools using the [x402 payment protocol](https://x402.org) with Cloudflare Agents.

## Overview

The example includes:

- **PayMCP**: An MCP server that exposes paid and free tools
- **PayAgent**: A client agent that calls these tools and handles payment confirmation flows

## x402 MCP Integration

This implementation follows the [x402 MCP transport specification](https://github.com/coinbase/x402/blob/main/specs/transports/mcp.md#payment-payload-transmission), which defines:

1. **Payment Required Signaling**: Server returns JSON-RPC error with `code: 402` and `PaymentRequirementsResponse`
2. **Payment Payload Transmission**: Client sends payment in `_meta["x402/payment"]`
3. **Settlement Response**: Server confirms payment in `_meta["x402/payment-response"]`

### Price Discovery Extension

In addition to the core x402 MCP spec, this implementation includes a **Agents extension** for price discovery:

```typescript
_meta: {
"agents-x402/paymentRequired": true, // Indicates tool requires payment
"agents-x402/priceUSD": 0.01 // Pre-advertises price in USD
}
```

**Note**: The `agents-x402/` namespace is used (not `x402/`) because price pre-advertising is an extension beyond the official x402 MCP specification to allow for a nice user experience. The core spec only defines the reactive payment flow (call → 402 error → retry with payment).

## Usage

### Server Side: Creating Paid Tools

```typescript
import { withX402 } from "agents/x402";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

const server = withX402(new McpServer({ name: "PayMCP", version: "1.0.0" }), {
network: "base-sepolia",
recipient: "0x...",
facilitator: { url: "https://x402.org/facilitator" }
});

// Create a paid tool
server.paidTool(
"square",
"Squares a number",
0.01, // Price in USD
{ number: z.number() },
{}, // MCP annotations (readOnlyHint, etc.)
async ({ number }) => {
return { content: [{ type: "text", text: String(number ** 2) }] };
}
);
```

### Client Side: Calling Paid Tools

```typescript
import { withX402Client } from "agents/x402";
import { privateKeyToAccount } from "viem/accounts";

const account = privateKeyToAccount(env.PRIVATE_KEY);

const x402Client = withX402Client(mcpClient, {
network: "base-sepolia",
account
});

// Call tool with payment confirmation callback
const result = await x402Client.callTool(
async (requirements) => {
// Show payment prompt to user
return await userConfirmsPayment(requirements);
},
{
name: "square",
arguments: { number: 5 }
}
);
```

## Environment Variables

```bash
# Server: Address to receive payments
MCP_ADDRESS=0x...

# Client: Private key for signing payments
CLIENT_TEST_PK=0x...
```

## Running the Example

```bash
npm install
npx wrangler dev
```

Then open http://localhost:8787 in your browser.
18 changes: 11 additions & 7 deletions examples/x402-mcp/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { z } from "zod";
import type { PaymentRequirements } from "x402/types";
import { privateKeyToAccount } from "viem/accounts";
import ui from "./ui";
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";

export class PayAgent extends Agent<Env> {
confirmations: Record<string, (res: boolean) => void> = {};
Expand Down Expand Up @@ -65,19 +64,22 @@ export class PayAgent extends Agent<Env> {

// The first parameter becomes the confirmation callback.
// We can set it to `null` if we want the agent to pay automatically.
const res = (await this.x402Client!.callTool(
const res = await this.x402Client!.callTool(
this.onPaymentRequired.bind(this),
{
name: parsed.type,
arguments: input
}
)) as CallToolResult;
);

conn.send(
JSON.stringify({
event: res.isError ? "tool_error" : "tool_result",
tool: parsed.type,
output: res.content[0]?.text ?? ""
output:
res.content[0]?.type === "text"
? (res.content[0]?.text ?? "")
: ""
})
);
}
Expand Down Expand Up @@ -117,10 +119,12 @@ export class PayMCP extends McpAgent<Env> {
);

// Free tool
this.server.tool(
this.server.registerTool(
"echo",
"Echo a message",
{ message: z.string() },
{
description: "Echo a message",
inputSchema: { message: z.string() }
},
async ({ message }) => {
return { content: [{ type: "text", text: message }] };
}
Expand Down
6 changes: 3 additions & 3 deletions examples/x402/package.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"author": "",
"dependencies": {
"hono": "^4.10.4",
"x402-fetch": "^0.7.0",
"x402-hono": "^0.7.1"
"hono": "^4.10.6",
"x402-fetch": "^0.7.3",
"x402-hono": "^0.7.3"
},
"keywords": [],
"name": "@cloudflare/agents-x402-example",
Expand Down
Loading
Loading