mcp.rs•11.7 kB
use crate::ethereum::EthereumService;
use crate::types::*;
use anyhow::Result;
use rmcp::handler::server::ServerHandler;
use rmcp::service::{RequestContext, RoleServer};
use rmcp::{ErrorData, ServiceExt};
use rmcp::schemars;
use std::str::FromStr;
use std::sync::Arc;
#[derive(Clone)]
pub struct MCPServer {
ethereum: Arc<EthereumService>,
}
#[derive(Debug, serde::Serialize,serde::Deserialize, schemars::JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct QueryBalanceParams {
#[schemars(description = "Ethereum address to query")]
pub address: String,
#[schemars(description = "Optional ERC-20 token address. If not provided, returns ETH balance")]
pub token_address: Option<String>,
}
#[derive(Debug, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct BuildSwapTransactionParams {
#[schemars(description = "Address of the token to swap from (default: USDT)")]
pub from_token: Option<String>,
#[schemars(description = "Address of the token to swap to (default: WETH)")]
pub to_token: Option<String>,
#[schemars(description = "Amount of from_token to swap (default: 0.0001)")]
pub amount_in: Option<String>,
#[schemars(description = "Maximum acceptable slippage percentage")]
pub slippage_tolerance: Option<f64>,
#[schemars(description = "Address to receive the swapped tokens")]
pub recipient: Option<String>,
}
#[derive(Debug, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
pub struct GetTokenPriceParams {
#[schemars(description = "Token address or symbol (e.g., 'ETH', 'USDC', or '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48')")]
pub token: String,
#[schemars(description = "Currency to return price in (e.g., 'USD', 'ETH'). Defaults to 'USD'")]
pub currency: Option<String>,
}
impl ServerHandler for MCPServer {
async fn ping(&self, _context: RequestContext<RoleServer>) -> Result<(), ErrorData> {
tracing::info!("Received ping request");
Ok(())
}
async fn initialize(
&self,
_request: rmcp::model::InitializeRequestParam,
_context: RequestContext<RoleServer>,
) -> Result<rmcp::model::InitializeResult, ErrorData> {
tracing::info!("Initializing MCP server");
Ok(rmcp::model::InitializeResult {
protocol_version: rmcp::model::ProtocolVersion::V_2024_11_05,
capabilities: rmcp::model::ServerCapabilities {
experimental: None,
logging: None,
prompts: None,
resources: None,
tools: Some(rmcp::model::ToolsCapability {
list_changed: Some(true),
}),
completions: None,
},
server_info: rmcp::model::Implementation {
name: "test1".to_string(),
title: None,
version: "abcde".to_string(),
icons: None,
website_url: None,
},
instructions: None,
})
}
async fn list_tools(
&self,
_request: Option<rmcp::model::PaginatedRequestParam>,
_context: RequestContext<RoleServer>,
) -> Result<rmcp::model::ListToolsResult, ErrorData> {
tracing::info!("Listing available tools");
let query_balance_schema = Arc::new(serde_json::json!({
"type": "object",
"properties": {
"address": {
"type": "string",
"description": "Ethereum address to query"
},
"tokenAddress": {
"type": "string",
"description": "Optional ERC-20 token address. If not provided, returns ETH balance"
}
},
"required": ["address"]
}).as_object().unwrap().clone());
let swap_tx_schema = Arc::new(serde_json::json!({
"type": "object",
"properties": {
"fromToken": {
"type": "string",
"description": "Address of the token to swap from (default: USDT)"
},
"toToken": {
"type": "string",
"description": "Address of the token to swap to (default: WETH)"
},
"amountIn": {
"type": "string",
"description": "Amount of fromToken to swap (default: 1.0)"
},
"slippageTolerance": {
"type": "number",
"description": "Maximum acceptable slippage percentage (default: 0.5%)"
},
"recipient": {
"type": "string",
"description": "Address to receive the swapped tokens (default: signer)"
}
},
"required": []
}).as_object().unwrap().clone());
let token_price_schema = Arc::new(serde_json::json!({
"type": "object",
"properties": {
"token": {
"type": "string",
"description": "Token address or symbol (e.g., 'ETH', 'USDC', or '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48')"
},
"currency": {
"type": "string",
"description": "Currency to return price in (e.g., 'USD', 'ETH'). Defaults to 'USD'"
}
},
"required": ["token"]
}).as_object().unwrap().clone());
let tools = vec![
rmcp::model::Tool {
name: "query_balance".into(),
title: None,
description: Some("Query ETH or ERC-20 token balance for an Ethereum address".into()),
input_schema: query_balance_schema,
output_schema: None,
annotations: None,
icons: None,
},
rmcp::model::Tool {
name: "build_swap_transaction".into(),
title: None,
description: Some("Build a transaction for executing a token swap".into()),
input_schema: swap_tx_schema,
output_schema: None,
annotations: None,
icons: None,
},
rmcp::model::Tool {
name: "get_token_price".into(),
title: None,
description: Some("Get current token price in USD or ETH".into()),
input_schema: token_price_schema,
output_schema: None,
annotations: None,
icons: None,
},
];
Ok(rmcp::model::ListToolsResult {
tools,
next_cursor: None,
})
}
async fn call_tool(
&self,
request: rmcp::model::CallToolRequestParam,
_context: RequestContext<RoleServer>,
) -> Result<rmcp::model::CallToolResult, ErrorData> {
tracing::info!("Calling tool: {}", request.name);
match request.name.as_ref() {
"query_balance" => {
// Convert JsonObject to serde_json::Value
let params: QueryBalanceParams = serde_json::from_value(serde_json::Value::Object(request.arguments.unwrap()))
.map_err(|e| ErrorData::invalid_params(e.to_string(), None))?;
let balance_params = BalanceQuery {
address: params.address,
token_address: params.token_address,
};
let balance = self.ethereum.query_balance(&balance_params).await
.map_err(|e| ErrorData::internal_error(e.to_string(), None))?;
Ok(rmcp::model::CallToolResult {
content: vec![rmcp::model::Content {
raw: rmcp::model::RawContent::text(balance),
annotations: None,
}],
structured_content: None,
is_error: Some(false),
meta: None,
})
}
"build_swap_transaction" => {
// Convert JsonObject to serde_json::Value
let params: BuildSwapTransactionParams = serde_json::from_value(serde_json::Value::Object(request.arguments.unwrap()))
.map_err(|e| ErrorData::invalid_params(e.to_string(), None))?;
// Apply default values
let from_token = params.from_token.unwrap_or_else(|| "0xdAC17F958D2ee523a2206206994597C13D831ec7".to_string());
let to_token = params.to_token.unwrap_or_else(|| "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2".to_string());
let amount_in = params.amount_in.unwrap_or_else(|| "1".to_string());
let swap_params = SwapParams {
from_token,
to_token,
amount_in,
slippage_tolerance: params.slippage_tolerance.map(|s| rust_decimal::Decimal::from_str(&s.to_string()).unwrap_or(rust_decimal::Decimal::ZERO)),
recipient: params.recipient,
};
self.ethereum.validate_swap_params(&swap_params)
.map_err(|e| ErrorData::invalid_params(e.to_string(), None))?;
let tx = self.ethereum.build_swap_transaction(&swap_params).await
.map_err(|e| ErrorData::internal_error(e.to_string(), None))?;
Ok(rmcp::model::CallToolResult {
content: vec![rmcp::model::Content {
raw: rmcp::model::RawContent::text(tx),
annotations: None,
}],
structured_content: None,
is_error: Some(false),
meta: None,
})
}
"get_token_price" => {
let params: GetTokenPriceParams = serde_json::from_value(serde_json::Value::Object(request.arguments.unwrap()))
.map_err(|e| ErrorData::invalid_params(e.to_string(), None))?;
let price = self.ethereum.get_token_price(¶ms.token, params.currency.as_deref()).await
.map_err(|e| ErrorData::internal_error(e.to_string(), None))?;
Ok(rmcp::model::CallToolResult {
content: vec![rmcp::model::Content {
raw: rmcp::model::RawContent::text(price),
annotations: None,
}],
structured_content: None,
is_error: Some(false),
meta: None,
})
}
_ => Ok(rmcp::model::CallToolResult {
content: vec![rmcp::model::Content {
raw: rmcp::model::RawContent::text("None"),
annotations: None,
}],
structured_content: None,
is_error: Some(true),
meta: None,
}),
}
}
}
impl MCPServer {
pub async fn new(rpc_url: Option<String>) -> Result<Self> {
Ok(Self {
ethereum: Arc::new(EthereumService::init(rpc_url).await?),
})
}
pub async fn run(self) -> Result<()> {
let stdin = tokio::io::stdin();
let stdout = tokio::io::stdout();
let transport = (stdin, stdout);
let services = self.serve(transport).await.expect("Serve failed");
services.waiting().await?;
Ok(())
}
}