use r402::proto;
use serde_json::Value;
use crate::types::{CallToolResult, ContentItem};
use crate::{
PAYMENT_ERROR_KEY, PAYMENT_META_KEY, PAYMENT_REQUIRED_CODE, PAYMENT_RESPONSE_META_KEY,
};
#[must_use]
pub fn extract_payment_from_meta(meta: &serde_json::Map<String, Value>) -> Option<Value> {
let payment = meta.get(PAYMENT_META_KEY)?;
let obj = payment.as_object()?;
let version = obj.get("x402Version")?.as_u64()?;
if version == 0 {
return None;
}
obj.get("payload")?;
Some(payment.clone())
}
pub fn attach_payment_to_meta(meta: &mut serde_json::Map<String, Value>, payment: Value) {
meta.insert(PAYMENT_META_KEY.to_owned(), payment);
}
#[must_use]
pub fn extract_payment_response_from_meta(
meta: &serde_json::Map<String, Value>,
) -> Option<proto::SettleResponse> {
let response_data = meta.get(PAYMENT_RESPONSE_META_KEY)?;
serde_json::from_value(response_data.clone()).ok()
}
pub fn attach_payment_response_to_meta(
meta: &mut serde_json::Map<String, Value>,
response: &proto::SettleResponse,
) -> Result<(), serde_json::Error> {
let value = serde_json::to_value(response)?;
meta.insert(PAYMENT_RESPONSE_META_KEY.to_owned(), value);
Ok(())
}
#[must_use]
pub fn extract_payment_required_from_result(
result: &CallToolResult,
) -> Option<proto::PaymentRequired> {
if !result.is_error {
return None;
}
if let Some(sc) = &result.structured_content {
if let Some(pr) = unwrap_x402_error_envelope(sc) {
return Some(pr);
}
if let Some(pr) = try_parse_payment_required_from_value(sc) {
return Some(pr);
}
}
for item in &result.content {
let ContentItem::Text { text } = item;
if let Some(pr) = try_parse_payment_required_from_text(text) {
return Some(pr);
}
}
None
}
#[must_use]
pub fn create_tool_resource_url(tool_name: &str, custom_url: Option<&str>) -> String {
custom_url.map_or_else(|| format!("mcp://tool/{tool_name}"), str::to_owned)
}
fn try_parse_payment_required_from_value(value: &Value) -> Option<proto::PaymentRequired> {
let obj = value.as_object()?;
let version = obj.get("x402Version")?;
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let version_num = version.as_u64().or_else(|| {
version.as_f64().and_then(|f| {
if f >= 1.0 && f <= f64::from(u32::MAX) {
Some(f as u64)
} else {
None
}
})
})?;
if version_num < 1 {
return None;
}
let accepts = obj.get("accepts")?.as_array()?;
if accepts.is_empty() {
return None;
}
serde_json::from_value(value.clone()).ok()
}
fn try_parse_payment_required_from_text(text: &str) -> Option<proto::PaymentRequired> {
let value: Value = serde_json::from_str(text).ok()?;
unwrap_x402_error_envelope(&value).or_else(|| try_parse_payment_required_from_value(&value))
}
fn unwrap_x402_error_envelope(value: &Value) -> Option<proto::PaymentRequired> {
let obj = value.as_object()?;
let error_envelope = obj.get(PAYMENT_ERROR_KEY)?.as_object()?;
let code = error_envelope.get("code")?.as_i64()?;
if code != i64::from(PAYMENT_REQUIRED_CODE) {
return None;
}
let data = error_envelope.get("data")?;
try_parse_payment_required_from_value(data)
}
#[must_use]
pub fn wrap_x402_error_envelope(pr: &proto::PaymentRequired) -> Option<Value> {
let data = serde_json::to_value(pr).ok()?;
Some(serde_json::json!({
PAYMENT_ERROR_KEY: {
"code": PAYMENT_REQUIRED_CODE,
"data": data
}
}))
}