use r402_core::wire::{
Extensions, PaymentPayload, PaymentRequired, PaymentRequirements, SettleResponse,
};
use rmcp::model::{CallToolRequestParams, CallToolResult, ContentBlock, RequestMetaObject};
use serde_json::Value;
use crate::constants::{MCP_PAYMENT_META_KEY, MCP_PAYMENT_RESPONSE_META_KEY, MCP_TOOL_URL_PREFIX};
pub type McpPaymentPayload = PaymentPayload<PaymentRequirements, Value>;
#[must_use]
pub fn create_tool_resource_url(tool_name: &str, custom_url: Option<&str>) -> String {
match custom_url {
Some(u) if !u.is_empty() => u.to_owned(),
_ => format!("{MCP_TOOL_URL_PREFIX}{tool_name}"),
}
}
#[must_use]
pub fn payment_required_tool_result(required: &PaymentRequired) -> CallToolResult {
let value = serde_json::to_value(required).unwrap_or(Value::Null);
CallToolResult::structured_error(value)
}
#[must_use]
pub fn settlement_failed_tool_result(
accepts: &[PaymentRequirements],
resource: &r402_core::wire::ResourceInfo,
extensions: &Extensions,
error_msg: impl Into<String>,
) -> CallToolResult {
let mut required = PaymentRequired::new(resource.clone())
.with_error(error_msg.into())
.with_accepts(accepts.to_vec());
if !extensions.is_empty() {
required = required.with_extensions(extensions.clone());
}
payment_required_tool_result(&required)
}
#[must_use]
pub fn extract_payment_required(result: &CallToolResult) -> Option<PaymentRequired> {
if !result.is_error.unwrap_or(false) {
return None;
}
if let Some(ref sc) = result.structured_content
&& let Some(pr) = payment_required_from_value(sc)
{
return Some(pr);
}
result
.content
.iter()
.find_map(ContentBlock::as_text)
.and_then(|t| serde_json::from_str::<Value>(&t.text).ok())
.and_then(|v| payment_required_from_value(&v))
}
fn payment_required_from_value(value: &Value) -> Option<PaymentRequired> {
let obj = value.as_object()?;
if !obj.contains_key("x402Version") || !obj.contains_key("accepts") {
return None;
}
let version = obj.get("x402Version").and_then(Value::as_u64)?;
if version != 2 {
return None;
}
let accepts = obj.get("accepts")?.as_array()?;
if accepts.is_empty() {
return None;
}
serde_json::from_value(value.clone()).ok()
}
#[must_use]
pub fn extract_payment_from_params(params: &CallToolRequestParams) -> Option<McpPaymentPayload> {
let meta = params.meta.as_ref()?;
let value = meta.get(MCP_PAYMENT_META_KEY)?.clone();
let payload: McpPaymentPayload = serde_json::from_value(value).ok()?;
if payload.payload.is_null() {
return None;
}
Some(payload)
}
#[must_use]
pub fn attach_payment_to_params(
mut params: CallToolRequestParams,
payload: &McpPaymentPayload,
) -> CallToolRequestParams {
let value = serde_json::to_value(payload).unwrap_or(Value::Null);
let meta = params.meta.get_or_insert_with(RequestMetaObject::new);
meta.insert(MCP_PAYMENT_META_KEY.to_owned(), value);
params
}
#[must_use]
pub fn extract_settle_response(result: &CallToolResult) -> Option<SettleResponse> {
let meta = result.meta.as_ref()?;
let value = meta.get(MCP_PAYMENT_RESPONSE_META_KEY)?.clone();
serde_json::from_value(value).ok()
}
#[must_use]
pub fn attach_settle_response(
mut result: CallToolResult,
settle: &SettleResponse,
) -> CallToolResult {
let value = serde_json::to_value(settle).unwrap_or(Value::Null);
let mut meta = result.meta.take().unwrap_or_default();
meta.insert(MCP_PAYMENT_RESPONSE_META_KEY.to_owned(), value);
result.meta = Some(meta);
result
}
#[must_use]
pub fn is_payment_required_result(result: &CallToolResult) -> bool {
extract_payment_required(result).is_some()
}
#[cfg(test)]
mod tests {
use r402_core::wire::{PaymentRequirements, ResourceInfo};
use super::*;
fn sample_required() -> PaymentRequired {
let resource = ResourceInfo::new("mcp://tool/demo");
let req = PaymentRequirements::new(
"exact".into(),
"eip155:84532".parse().unwrap(),
"10000".into(),
"0xpay".into(),
"0xasset".into(),
60,
);
PaymentRequired::new(resource)
.with_error("Payment required")
.with_accepts(vec![req])
}
#[test]
fn payment_required_sets_structured_and_text() {
let pr = sample_required();
let result = payment_required_tool_result(&pr);
assert_eq!(result.is_error, Some(true));
let sc = result.structured_content.as_ref().unwrap();
let text = result
.content
.first()
.and_then(ContentBlock::as_text)
.unwrap();
let from_text: Value = serde_json::from_str(&text.text).unwrap();
assert_eq!(sc, &from_text);
let again = extract_payment_required(&result).unwrap();
assert_eq!(
again.accepts.first().map(|a| a.amount.as_str()),
Some("10000")
);
}
#[test]
fn settlement_failed_uses_dual_format() {
let pr = sample_required();
let result = settlement_failed_tool_result(
&pr.accepts,
&pr.resource,
&Extensions::new(),
"Settlement failed",
);
assert!(extract_payment_required(&result).is_some());
}
#[test]
fn rejects_v1_shaped_payment_required() {
let result = CallToolResult::structured_error(serde_json::json!({
"x402Version": 1,
"accepts": [{"scheme":"exact","network":"eip155:1","amount":"1","payTo":"0xa","asset":"0xb","maxTimeoutSeconds":60}]
}));
assert!(extract_payment_required(&result).is_none());
}
#[test]
fn tool_url_default_and_custom() {
assert_eq!(
create_tool_resource_url("weather", None),
"mcp://tool/weather"
);
assert_eq!(
create_tool_resource_url("weather", Some("https://x/y")),
"https://x/y"
);
}
#[test]
fn attach_and_extract_payment_meta() {
let payload: McpPaymentPayload = serde_json::from_value(serde_json::json!({
"x402Version": 2,
"accepted": {
"scheme": "exact",
"network": "eip155:1",
"amount": "1",
"payTo": "0xa",
"asset": "0xb",
"maxTimeoutSeconds": 60
},
"payload": {"sig": "0x"}
}))
.unwrap();
let params = CallToolRequestParams::new("demo");
let params = attach_payment_to_params(params, &payload);
let extracted = extract_payment_from_params(¶ms).unwrap();
assert_eq!(extracted.accepted.scheme.as_str(), "exact");
}
#[test]
fn null_payload_body_rejected() {
use rmcp::model::RequestParamsMeta;
let mut params = CallToolRequestParams::new("demo");
let mut meta = RequestMetaObject::new();
meta.insert(
MCP_PAYMENT_META_KEY.to_owned(),
serde_json::json!({
"x402Version": 2,
"accepted": {
"scheme": "exact",
"network": "eip155:1",
"amount": "1",
"payTo": "0xa",
"asset": "0xb",
"maxTimeoutSeconds": 60
},
"payload": null
}),
);
params.set_meta(meta);
assert!(extract_payment_from_params(¶ms).is_none());
}
#[test]
fn attach_and_extract_settle_response() {
let settle = SettleResponse::Success {
payer: "0xpayer".into(),
transaction: "0xtx".into(),
network: "eip155:1".into(),
amount: Some("1".into()),
extensions: Extensions::new(),
};
let result = CallToolResult::success(vec![ContentBlock::text("ok")]);
let result = attach_settle_response(result, &settle);
let extracted = extract_settle_response(&result).unwrap();
assert!(extracted.is_success());
}
}