Skip to content

Commit e74afec

Browse files
committed
Bump version to 0.10.3 and add r402-mcp crate for Model Context Protocol payment integration
* Bump workspace version from 0.10.2 to 0.10.3 in Cargo.toml * Add r402-mcp to workspace members and publish workflow package list * Add r402-mcp workspace dependency and rmcp dependency for MCP protocol support * Create r402-mcp crate with client-side auto-payment and server-side payment wrapper for MCP tools * Implement X402McpClient with automatic 402 payment handling, scheme client integration, and lifecycle
1 parent b0d2045 commit e74afec

File tree

19 files changed

+1741
-37
lines changed

19 files changed

+1741
-37
lines changed

.github/workflows/rust-publish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,6 @@ jobs:
1212
call-publish:
1313
uses: qntx/workflows/.github/workflows/rust-publish.yml@main
1414
with:
15-
package: "r402 r402-evm r402-svm r402-http"
15+
package: "r402 r402-evm r402-svm r402-http r402-mcp"
1616
secrets:
1717
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}

Cargo.lock

Lines changed: 70 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
[workspace]
2-
members = ["r402", "r402-evm", "r402-http", "r402-svm"]
2+
members = ["r402", "r402-evm", "r402-http", "r402-mcp", "r402-svm"]
33
default-members = ["r402"]
44
resolver = "3"
55

66
[workspace.package]
7-
version = "0.10.2"
7+
version = "0.10.3"
88
edition = "2024"
99
license = "MIT OR Apache-2.0"
1010
repository = "https://github.com/qntx/r402"
@@ -15,6 +15,7 @@ description = "x402 Payment Protocol SDK for Rust."
1515
r402 = { version = "0.10", path = "r402" }
1616
r402-evm = { version = "0.10", path = "r402-evm" }
1717
r402-http = { version = "0.10", path = "r402-http" }
18+
r402-mcp = { version = "0.10", path = "r402-mcp" }
1819
r402-svm = { version = "0.10", path = "r402-svm" }
1920

2021
# Core
@@ -73,6 +74,9 @@ solana-transaction = "3"
7374
spl-token = { version = "9", features = ["no-entrypoint"] }
7475
spl-token-2022 = { version = "10", features = ["no-entrypoint"] }
7576

77+
# MCP
78+
rmcp = { version = "0.15", default-features = false, features = ["client", "server", "schemars"] }
79+
7680
# Telemetry
7781
tracing = "0.1"
7882
tracing-core = "0.1"

r402-evm/src/exact/facilitator/mod.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -252,9 +252,7 @@ where
252252
})
253253
}
254254

255-
fn supported(
256-
&self,
257-
) -> BoxFuture<'_, Result<proto::SupportedResponse, FacilitatorError>> {
255+
fn supported(&self) -> BoxFuture<'_, Result<proto::SupportedResponse, FacilitatorError>> {
258256
Box::pin(async move {
259257
let chain_id = self.provider.chain_id();
260258
let kinds = vec![proto::SupportedPaymentKind {

r402-http/src/server/facilitator.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,9 +154,7 @@ impl Facilitator for FacilitatorClient {
154154
})
155155
}
156156

157-
fn supported(
158-
&self,
159-
) -> BoxFuture<'_, Result<SupportedResponse, FacilitatorError>> {
157+
fn supported(&self) -> BoxFuture<'_, Result<SupportedResponse, FacilitatorError>> {
160158
Box::pin(async move {
161159
Self::supported(self)
162160
.await

r402-mcp/Cargo.toml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
[package]
2+
name = "r402-mcp"
3+
version.workspace = true
4+
edition.workspace = true
5+
license.workspace = true
6+
repository.workspace = true
7+
description = "MCP (Model Context Protocol) integration for the x402 payment protocol."
8+
9+
[dependencies]
10+
r402 = { workspace = true }
11+
serde = { workspace = true }
12+
serde_json = { workspace = true }
13+
thiserror = { workspace = true }
14+
15+
# Optional
16+
rmcp = { workspace = true, optional = true }
17+
tracing = { workspace = true, optional = true }
18+
19+
[features]
20+
default = []
21+
rmcp = ["dep:rmcp"]
22+
telemetry = ["dep:tracing"]
23+
24+
[lints]
25+
workspace = true

r402-mcp/README.md

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# r402-mcp
2+
3+
MCP (Model Context Protocol) integration for the x402 payment protocol.
4+
5+
This crate enables paid tool calls in MCP servers and automatic payment handling in MCP clients, following the [x402 payment protocol](https://github.com/coinbase/x402) specification.
6+
7+
## Features
8+
9+
- **Client-side auto-payment**`X402McpClient` wraps any MCP caller with automatic 402 payment handling
10+
- **Server-side payment wrapper**`PaymentWrapper` wraps tool handlers with verify → execute → settle lifecycle
11+
- **Framework-agnostic** — Works with any MCP SDK via the `McpCaller` trait and `serde_json::Value`-based types
12+
- **Lifecycle hooks** — Trait-based hooks for both client and server sides
13+
- **Payment extraction utilities** — Extract/attach payment data from/to MCP `_meta` fields
14+
15+
## Architecture
16+
17+
The crate follows the x402 MCP specification where payment data flows through JSON-RPC `_meta` fields:
18+
19+
- `_meta["x402/payment"]` — Client → Server: payment payload
20+
- `_meta["x402/payment-response"]` — Server → Client: settlement response
21+
22+
### Client Flow
23+
24+
1. Call tool without payment
25+
2. If 402: extract `PaymentRequired` from `structuredContent` or `content[0].text`
26+
3. Create payment via registered `SchemeClient`s
27+
4. Retry with payment in `_meta["x402/payment"]`
28+
5. Extract settlement response from result `_meta`
29+
30+
### Server Flow
31+
32+
1. Extract `x402/payment` from request `_meta`
33+
2. If missing → return 402 error with `PaymentRequired`
34+
3. Verify payment via `Facilitator`
35+
4. Execute original handler
36+
5. Settle payment via `Facilitator`
37+
6. Attach `SettleResponse` to result `_meta`
38+
39+
## Usage
40+
41+
### Client
42+
43+
```rust,ignore
44+
use r402_mcp::client::{X402McpClient, McpCaller};
45+
use r402_mcp::types::ClientOptions;
46+
47+
let client = X402McpClient::builder(my_mcp_session)
48+
.scheme_client(Box::new(evm_scheme_client))
49+
.auto_payment(true)
50+
.build();
51+
52+
let result = client.call_tool("get_weather", args).await?;
53+
if let Some(settle) = &result.payment_response {
54+
println!("Paid! tx: {:?}", settle);
55+
}
56+
```
57+
58+
### Server
59+
60+
```rust,ignore
61+
use r402_mcp::server::PaymentWrapper;
62+
use r402_mcp::types::PaymentWrapperConfig;
63+
64+
let wrapper = PaymentWrapper::new(facilitator.into(), PaymentWrapperConfig {
65+
accepts: vec![payment_requirements],
66+
resource: Some(resource_info),
67+
..Default::default()
68+
});
69+
70+
let result = wrapper.process(request, |req| async {
71+
Ok(CallToolResult {
72+
content: vec![ContentItem::text("Weather: sunny")],
73+
..Default::default()
74+
})
75+
}).await;
76+
```
77+
78+
### Low-level Utilities
79+
80+
```rust
81+
use r402_mcp::extract;
82+
83+
// Extract payment from meta
84+
let payment = extract::extract_payment_from_meta(&meta);
85+
86+
// Attach payment to meta
87+
extract::attach_payment_to_meta(&mut meta, payment_value);
88+
89+
// Extract payment required from error result
90+
let pr = extract::extract_payment_required_from_result(&result);
91+
```
92+
93+
## Feature Flags
94+
95+
| Feature | Description |
96+
|---------|-------------|
97+
| `telemetry` | Enables `tracing` instrumentation |
98+
99+
## License
100+
101+
Licensed under either of [Apache License, Version 2.0](../LICENSE-APACHE) or [MIT license](../LICENSE-MIT) at your option.

0 commit comments

Comments
 (0)