Skip to content

Commit 7e48094

Browse files
committed
feat: add ledgerflow-mcp crate for x402 verification and settlement tools
- Introduced a new crate `ledgerflow-mcp` with a complete implementation of an MCP server. - Added dependencies for ultrafast-mcp and x402-rs to facilitate the server's functionality. - Implemented tool handlers for `x402_supported`, `x402_verify`, and `x402_settle` methods. - Configured the server to run over stdio or as an HTTP server based on command-line arguments. - Updated the README with usage instructions and tool descriptions. - Refactored existing code in `ledgerflow-facilitator` to accommodate the new structure and dependencies.
1 parent c3ade5a commit 7e48094

9 files changed

Lines changed: 876 additions & 46 deletions

File tree

Cargo.lock

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

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ members = [
1010
"ledgerflow-aptos-cli",
1111
"ledgerflow-sui-cli",
1212
"ledgerflow-facilitator",
13+
"ledgerflow-mcp",
1314
]
1415
resolver = "2"
1516

ledgerflow-facilitator/src/handlers.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
//! Axum handlers wrapping x402-rs facilitator logic.
2+
#![allow(dead_code)]
23

34
use axum::{http::StatusCode, response::IntoResponse, Extension, Json};
45
use tracing::instrument;

ledgerflow-facilitator/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
#[path = "handlers.rs"]
2-
pub mod handlers;
31
#[path = "config.rs"]
42
pub mod config;
3+
#[path = "handlers.rs"]
4+
pub mod handlers;
55
use axum::{
66
http::Method,
77
routing::{get, post},

ledgerflow-facilitator/src/main.rs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,15 @@
99
1010
use std::net::SocketAddr;
1111

12-
use axum::Router;
1312
use clap::Parser;
1413
use color_eyre::Result;
1514
use dotenvy::dotenv;
15+
use ledgerflow_facilitator::config::{load_config, ServerConfig};
1616
use tower_http::trace::TraceLayer;
1717
use tracing::level_filters::LevelFilter;
1818
use tracing_subscriber::EnvFilter;
1919
use x402_rs::{facilitator_local::FacilitatorLocal, provider_cache::ProviderCache};
2020

21-
mod config;
22-
mod handlers;
23-
24-
use crate::config::{load_config, ServerConfig};
25-
2621
#[tokio::main]
2722
async fn main() -> Result<()> {
2823
color_eyre::install()?;

ledgerflow-facilitator/tests/integration.rs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1-
use axum::http::StatusCode;
2-
use axum::Router;
3-
use x402_rs::facilitator_local::FacilitatorLocal;
4-
use x402_rs::provider_cache::ProviderCache;
1+
use axum::{http::StatusCode, Router};
2+
use x402_rs::{facilitator_local::FacilitatorLocal, provider_cache::ProviderCache};
53

64
// Tiny helper to build app with a ProviderCache that may read env; for CI we allow missing RPCs
75
async fn test_app() -> eyre::Result<Router> {
@@ -78,7 +76,10 @@ async fn post_verify_rejects_invalid_payload() -> eyre::Result<()> {
7876
.await;
7977

8078
// Either BAD_REQUEST for parse errors or 200 with invalid schema mapping; both are acceptable minimal checks
81-
assert!(matches!(res.status_code(), StatusCode::OK | StatusCode::BAD_REQUEST));
79+
assert!(matches!(
80+
res.status_code(),
81+
StatusCode::OK | StatusCode::BAD_REQUEST
82+
));
8283
Ok(())
8384
}
8485

@@ -97,6 +98,9 @@ async fn post_settle_rejects_invalid_payload() -> eyre::Result<()> {
9798
.post("/settle")
9899
.json(&serde_json::json!({"foo":"bar"}))
99100
.await;
100-
assert!(matches!(res.status_code(), StatusCode::OK | StatusCode::BAD_REQUEST));
101+
assert!(matches!(
102+
res.status_code(),
103+
StatusCode::OK | StatusCode::BAD_REQUEST
104+
));
101105
Ok(())
102106
}

ledgerflow-mcp/Cargo.toml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
[package]
2+
name = "ledgerflow-mcp"
3+
version = "0.1.0"
4+
edition = "2021"
5+
license = "Apache-2.0 OR MIT"
6+
repository = "https://github.com/longcipher/ledgerflow"
7+
description = "MCP server exposing x402 verify/settle/supported tools for LedgerFlow"
8+
9+
[dependencies]
10+
ultrafast-mcp = { version = "202506018.1.0", features = ["full"] }
11+
serde_json = { workspace = true }
12+
tracing = { workspace = true }
13+
tracing-subscriber = { workspace = true, features = ["env-filter"] }
14+
clap = { workspace = true, features = ["derive"] }
15+
eyre = { workspace = true }
16+
color-eyre = { workspace = true }
17+
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
18+
async-trait = "0.1"
19+
20+
# Use the facilitator crate we just built to reuse ProviderCache + handlers
21+
x402-rs = { git = "https://github.com/x402-rs/x402-rs", package = "x402-rs" }
22+
23+
dotenvy = "0.15"
24+
25+
[features]
26+
default = []
27+
# Enable HTTP transport by enabling ultrafast-mcp's HTTP feature set
28+
http = ["ultrafast-mcp/http-with-auth"]
29+
30+
[package.metadata.docs.rs]
31+
all-features = true

ledgerflow-mcp/README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# ledgerflow-mcp
2+
3+
An MCP (Model Context Protocol) server that exposes x402 verification and settlement tools for LedgerFlow using `ultrafast-mcp`.
4+
5+
## Tools
6+
7+
- x402_supported: list supported networks and schemes
8+
- x402_verify: verify a payment intent (Exact scheme)
9+
- x402_settle: settle a verified intent (Exact scheme)
10+
11+
## Run
12+
13+
By default the server runs over stdio.
14+
15+
```sh
16+
cargo run -p ledgerflow-mcp -- --stdio
17+
```
18+
19+
To run as an HTTP server:
20+
21+
```sh
22+
cargo run -p ledgerflow-mcp -- --http --host 127.0.0.1 --port 8765
23+
```
24+
25+
Configure RPC endpoints and signer via env vars (same as `ledgerflow-facilitator`). See `ledgerflow-facilitator/README.md`.

ledgerflow-mcp/src/main.rs

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
use std::sync::Arc;
2+
3+
use clap::Parser;
4+
use color_eyre::Result;
5+
use tracing::info;
6+
use ultrafast_mcp::{
7+
prelude::*, ListToolsRequest, ListToolsResponse, MCPError, MCPResult, ToolCall, ToolContent,
8+
ToolsCapability,
9+
};
10+
use x402_rs::{
11+
facilitator::Facilitator,
12+
facilitator_local::FacilitatorLocal,
13+
network::Network,
14+
provider_cache::ProviderCache,
15+
types::{
16+
Scheme, SettleRequest, SettleResponse, SupportedPaymentKind, VerifyRequest, VerifyResponse,
17+
X402Version,
18+
},
19+
};
20+
21+
#[derive(Parser, Debug)]
22+
#[command(name = "ledgerflow-mcp")]
23+
#[command(about = "MCP server exposing x402 verify/settle/supported tools", long_about = None)]
24+
pub struct Args {
25+
/// Run over stdio (default)
26+
#[arg(long)]
27+
stdio: bool,
28+
29+
/// Run HTTP server instead of stdio
30+
#[arg(long)]
31+
http: bool,
32+
33+
/// Host for HTTP server
34+
#[arg(long, default_value = "127.0.0.1")]
35+
host: String,
36+
37+
/// Port for HTTP server
38+
#[arg(long, default_value_t = 8765)]
39+
port: u16,
40+
}
41+
42+
#[derive(Clone)]
43+
struct X402ToolHandler {
44+
facilitator: FacilitatorLocal,
45+
}
46+
47+
#[async_trait::async_trait]
48+
impl ToolHandler for X402ToolHandler {
49+
async fn handle_tool_call(&self, call: ToolCall) -> MCPResult<ToolResult> {
50+
match call.name.as_str() {
51+
"x402_supported" => {
52+
let kinds: Vec<SupportedPaymentKind> = Network::variants()
53+
.iter()
54+
.copied()
55+
.map(|n| SupportedPaymentKind {
56+
x402_version: X402Version::V1,
57+
scheme: Scheme::Exact,
58+
network: n,
59+
})
60+
.collect();
61+
let payload = serde_json::json!({ "supported": kinds });
62+
Ok(ToolResult {
63+
content: vec![ToolContent::text(payload.to_string())],
64+
is_error: Some(false),
65+
})
66+
}
67+
"x402_verify" => {
68+
let req_value = call.arguments.unwrap_or_default();
69+
let req: VerifyRequest = serde_json::from_value(req_value)
70+
.map_err(|e| MCPError::invalid_params(format!("invalid arguments: {e}")))?;
71+
let res: VerifyResponse = self
72+
.facilitator
73+
.verify(&req)
74+
.await
75+
.map_err(|e| MCPError::internal_error(e.to_string()))?;
76+
Ok(ToolResult {
77+
content: vec![ToolContent::text(
78+
serde_json::to_string(&res).unwrap_or_else(|_| "{}".into()),
79+
)],
80+
is_error: Some(false),
81+
})
82+
}
83+
"x402_settle" => {
84+
let req_value = call.arguments.unwrap_or_default();
85+
let req: SettleRequest = serde_json::from_value(req_value)
86+
.map_err(|e| MCPError::invalid_params(format!("invalid arguments: {e}")))?;
87+
let res: SettleResponse = self
88+
.facilitator
89+
.settle(&req)
90+
.await
91+
.map_err(|e| MCPError::internal_error(e.to_string()))?;
92+
Ok(ToolResult {
93+
content: vec![ToolContent::text(
94+
serde_json::to_string(&res).unwrap_or_else(|_| "{}".into()),
95+
)],
96+
is_error: Some(false),
97+
})
98+
}
99+
_ => Err(MCPError::method_not_found(format!(
100+
"tool '{}' not found",
101+
call.name
102+
))),
103+
}
104+
}
105+
106+
async fn list_tools(&self, _request: ListToolsRequest) -> MCPResult<ListToolsResponse> {
107+
Ok(ListToolsResponse {
108+
tools: vec![
109+
Tool {
110+
name: "x402_supported".to_string(),
111+
description: "List supported payment kinds (networks + schemes)".to_string(),
112+
input_schema: serde_json::json!({"type":"object","properties":{}}),
113+
output_schema: None,
114+
annotations: None,
115+
},
116+
Tool {
117+
name: "x402_verify".to_string(),
118+
description: "Verify a payment intent using x402 Exact scheme".to_string(),
119+
input_schema: serde_json::json!({"type":"object"}),
120+
output_schema: None,
121+
annotations: None,
122+
},
123+
Tool {
124+
name: "x402_settle".to_string(),
125+
description: "Settle a verified payment intent".to_string(),
126+
input_schema: serde_json::json!({"type":"object"}),
127+
output_schema: None,
128+
annotations: None,
129+
},
130+
],
131+
next_cursor: None,
132+
})
133+
}
134+
}
135+
136+
#[tokio::main]
137+
async fn main() -> Result<()> {
138+
color_eyre::install()?;
139+
tracing_subscriber::fmt()
140+
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
141+
.init();
142+
143+
let args = Args::parse();
144+
145+
// Prepare providers/signers from env (shared with facilitator crate)
146+
dotenvy::dotenv().ok();
147+
let providers = ProviderCache::from_env()
148+
.await
149+
.map_err(|e| eyre::eyre!(format!("{e}")))?;
150+
let facilitator = FacilitatorLocal::new(providers);
151+
152+
let handler = X402ToolHandler { facilitator };
153+
154+
// Server info and capabilities
155+
let info = ServerInfo {
156+
name: "ledgerflow-mcp".to_string(),
157+
version: env!("CARGO_PKG_VERSION").to_string(),
158+
description: Some("MCP server exposing x402 verify/settle/supported".to_string()),
159+
authors: None,
160+
homepage: None,
161+
license: None,
162+
repository: None,
163+
};
164+
165+
let capabilities = ServerCapabilities {
166+
tools: Some(ToolsCapability {
167+
list_changed: Some(true),
168+
}),
169+
..Default::default()
170+
};
171+
let server = UltraFastServer::new(info, capabilities).with_tool_handler(Arc::new(handler));
172+
173+
if args.http {
174+
#[cfg(feature = "http")]
175+
{
176+
let addr = SocketAddr::from_str(&format!("{}:{}", args.host, args.port))?;
177+
info!(%addr, "Starting HTTP MCP server");
178+
server
179+
.run_streamable_http(addr.ip().to_string().as_str(), addr.port())
180+
.await?;
181+
}
182+
#[cfg(not(feature = "http"))]
183+
{
184+
info!("HTTP feature not enabled; falling back to stdio");
185+
server.run_stdio().await?;
186+
}
187+
} else {
188+
info!("Starting stdio MCP server");
189+
server.run_stdio().await?;
190+
}
191+
192+
Ok(())
193+
}

0 commit comments

Comments
 (0)