Skip to main content
Glama
substrate.rs36.5 kB
//! Substrate integration tools for the MCP server. //! //! This module provides dynamic access to Substrate blockchain operations (balances, pallets, storage, events, transactions, etc.) //! via the Model Context Protocol (MCP). Configuration is via environment variables and runtime metadata. use dotenv::dotenv; use hex; use rmcp::{ ErrorData as McpError, RoleServer, handler::server::{ServerHandler, wrapper::Parameters}, model::*, schemars::JsonSchema, serde_json::json, service::RequestContext, tool, tool_router, }; use serde::{Deserialize, Serialize}; use serde_json; use std::env; use std::{str::FromStr, sync::Arc}; use subxt::backend::{legacy::LegacyRpcMethods, rpc::RpcClient}; use subxt::config::polkadot::PolkadotExtrinsicParamsBuilder as Params; use subxt::config::substrate::AccountId32; use subxt::dynamic::Value; use subxt::ext::subxt_rpcs::client::RpcParams; use subxt::tx::TxStatus; use subxt::utils::H256; use subxt::{OnlineClient, PolkadotConfig}; use subxt_signer::sr25519::Keypair; use tokio::sync::Mutex; use tracing; // Generate an interface that we can use from the node's metadata. #[subxt::subxt(runtime_metadata_path = "artifacts/metadata.scale")] pub mod substrate {} type SubstrateConfig = PolkadotConfig; // Parameter structs for tool methods #[derive(Serialize, Deserialize, JsonSchema)] pub struct QueryBalanceParams { pub account: String, } #[derive(Serialize, Deserialize, JsonSchema)] pub struct ListPalletEntriesParams { pub pallet_name: String, } #[derive(Serialize, Deserialize, JsonSchema)] pub struct DynamicRuntimeCallParams { pub trait_name: String, pub method_name: String, pub args_data: Vec<String>, } #[derive(Serialize, Deserialize, JsonSchema)] pub struct SendDynamicSignedTransactionParams { pub pallet_name: String, pub call_name: String, pub call_parameters: String, pub mortality: Option<u64>, pub nonce: Option<u64>, pub tip_of_asset_id: Option<u32>, pub tip: Option<u128>, pub tip_of: Option<u128>, } #[derive(Serialize, Deserialize, JsonSchema)] pub struct QueryStorageParams { pub pallet_name: String, pub entry_name: String, pub storage_keys: Option<Vec<String>>, } #[derive(Serialize, Deserialize, JsonSchema)] pub struct FindEventsParams { pub pallet_name: String, pub event_name: String, } #[derive(Serialize, Deserialize, JsonSchema)] pub struct GetConstantParams { pub pallet_name: String, pub constant_name: String, } #[derive(Serialize, Deserialize, JsonSchema)] pub struct GetBlockByHashParams { pub block_hash: String, } #[derive(Serialize, Deserialize, JsonSchema)] pub struct FindExtrinsicsParams { pub pallet_name: String, pub call_name: String, } #[derive(Serialize, Deserialize, JsonSchema)] pub struct CustomRpcParams { pub method: String, pub params: Option<String>, } /// Main tool for interacting with a Substrate blockchain via MCP. /// /// Provides dynamic querying, transaction submission, and runtime API access. #[derive(Clone)] pub struct SubstrateTool { api: Arc<Mutex<OnlineClient<SubstrateConfig>>>, rpc_client: Arc<Mutex<RpcClient>>, rpc_methods: Arc<Mutex<LegacyRpcMethods<SubstrateConfig>>>, signing_keypair: Option<Keypair>, } #[tool_router] impl SubstrateTool { /// Create a new SubstrateTool, loading configuration from environment variables. /// /// - `RPC_URL`: WebSocket endpoint for the Substrate node /// - `SIGNING_KEYPAIR_HEX`: Signing keypair as hex (32 bytes, optional) /// /// If the signing keypair is missing or invalid, transaction-signing tools will return an error, but the server will still start. /// /// # Logging /// Logs a warning if the signing keypair is missing or invalid, and info if loaded successfully. pub async fn new() -> Self { dotenv().ok(); let rpc_url = env::var("RPC_URL").expect("RPC_URL must be set in .env"); let rpc = RpcClient::from_url(&rpc_url) .await .expect("Failed to create rpc client"); let client = OnlineClient::<SubstrateConfig>::from_rpc_client(rpc.clone()) .await .expect("Failed to create API client"); let signing_keypair = env::var("SIGNING_KEYPAIR_HEX") .inspect_err(|_| { tracing::warn!("SIGNING_KEYPAIR_HEX not set; signing will be disabled"); }) .ok() .and_then(|signing_keypair_hex| { hex::decode(signing_keypair_hex.trim()) .map_err(|e| { tracing::warn!( "Failed to decode SIGNING_KEYPAIR_HEX as hex: {e}; signing will be disabled" ); }) .ok() }) .and_then(|bytes| { if bytes.len() == 32 { let secret_key_array: [u8; 32] = bytes.as_slice().try_into().expect( "SIGNING_KEYPAIR_HEX: Slice of 32 bytes should convert to array [u8; 32]", ); Keypair::from_secret_key(secret_key_array) .map_err(|e| { tracing::warn!( "Invalid SIGNING_KEYPAIR_HEX: {e}; signing will be disabled" ); }) .ok() } else { tracing::warn!("SIGNING_KEYPAIR_HEX is not 32 bytes; signing will be disabled"); None } }) .inspect(|_| { tracing::info!("Loaded signing keypair from SIGNING_KEYPAIR_HEX"); }); Self { api: Arc::new(Mutex::new(client)), rpc_client: Arc::new(Mutex::new(rpc.clone())), rpc_methods: Arc::new(Mutex::new(LegacyRpcMethods::<SubstrateConfig>::new(rpc))), signing_keypair, } } /// Fetch the free balance of an account. /// /// # Parameters /// - `account`: SS58-encoded account address /// /// # Returns /// The free balance as a string, or an error if not found. #[tool(description = "Fetch the balance of an account")] pub async fn query_balance( &self, params: Parameters<QueryBalanceParams>, ) -> Result<CallToolResult, McpError> { let client = self.api.lock().await; let account_id = AccountId32::from_str(&params.0.account).map_err(|e| { McpError::invalid_params( "Invalid account address", Some(serde_json::json!({ "error": e.to_string() })), ) })?; let storage = client.storage().at_latest().await.map_err(|e| { McpError::internal_error(format!("Failed to access storage: {}", e), None) })?; let account_balance = storage .fetch(&substrate::storage().balances().account(account_id)) .await .map_err(|e| { McpError::resource_not_found( format!( "Failed to fetch account balance: {} for account: {}", e, params.0.account ), None, ) })?; match account_balance { Some(balance) => Ok(CallToolResult::success(vec![Content::text( balance.free.to_string(), )])), None => Err(McpError::resource_not_found( format!("Balance was not found for account: {}", params.0.account), None, )), } } /// List all pallets in the runtime metadata. /// /// # Returns /// A list of pallet names. #[tool(description = "List all pallets")] pub async fn list_pallets(&self) -> Result<CallToolResult, McpError> { let client = self.api.lock().await; Ok(CallToolResult::success( client .metadata() .pallets() .map(|p| Content::text(p.name().to_string())) .collect(), )) } /// List all storage entries for a given pallet. /// /// # Parameters /// - `pallet_name`: Name of the pallet /// /// # Returns /// A list of storage entry names. #[tool(description = "List all of a pallet's entries")] pub async fn list_pallet_entries( &self, params: Parameters<ListPalletEntriesParams>, ) -> Result<CallToolResult, McpError> { let client = self.api.lock().await; let client_metadata = client.metadata(); // Find the pallet let pallet = client_metadata .pallets() .find(|p| *p.name() == params.0.pallet_name) .ok_or(McpError::invalid_params("Pallet not found", None))?; // Get storage let storage = pallet .storage() .ok_or(McpError::invalid_params("Pallet has no storage", None))?; // Collect entry names let entries = storage .entries() .iter() .map(|e| Content::text(e.name().to_string())) .collect(); Ok(CallToolResult::success(entries)) } /// Execute a dynamic runtime API call. /// /// # Parameters /// - `trait_name`: Runtime API trait name /// - `method_name`: Method name /// - `args_data`: Arguments as strings /// /// # Returns /// The result of the runtime API call as a string. #[tool(description = "Execute a dynamic runtime API call")] pub async fn dynamic_runtime_call( &self, params: Parameters<DynamicRuntimeCallParams>, ) -> Result<CallToolResult, McpError> { let client = self.api.lock().await; let runtime_api_call = subxt::dynamic::runtime_api_call( params.0.trait_name, params.0.method_name, params.0.args_data, ); let runtime_api = client.runtime_api().at_latest().await.map_err(|e| { McpError::internal_error(format!("Failed to access runtime api: {}", e), None) })?; let call_result = runtime_api.call(runtime_api_call).await.map_err(|e| { McpError::internal_error(format!("Failed to call runtime api: {}", e), None) })?; let result = call_result.to_value().map_err(|e| { McpError::internal_error( format!("Failed to convert runtime api result to value: {}", e), None, ) })?; Ok(CallToolResult::success(vec![Content::text( result.to_string(), )])) } /// Construct, sign, and submit a dynamic transaction. /// /// # Parameters /// - `pallet_name`: Pallet name /// - `call_name`: Call name /// - `call_parameters`: Call parameters as a string /// - `mortality`, `nonce`, `tip_of_asset_id`, `tip`, `tip_of`: Optional transaction params /// /// # Returns /// Transaction hash if successful. #[tool(description = "Constructs, signs and sends a dynamic transaction")] pub async fn send_dynamic_signed_transaction( &self, params: Parameters<SendDynamicSignedTransactionParams>, ) -> Result<CallToolResult, McpError> { let signing_keypair = self .signing_keypair .as_ref() .ok_or(McpError::internal_error("Signing keypair is not set", None))?; let client = self.api.lock().await; let tx = subxt::dynamic::tx( params.0.pallet_name.clone(), params.0.call_name.clone(), vec![Value::from_bytes(params.0.call_parameters.as_bytes())], ); let mut tx_params = Params::new(); if let Some(mortality) = params.0.mortality { tx_params = tx_params.mortal(mortality); } if let Some(nonce) = params.0.nonce { tx_params = tx_params.nonce(nonce); } if let (Some(tip_of_asset_id), Some(tip_of)) = (params.0.tip_of_asset_id, params.0.tip_of) { tx_params = tx_params.tip_of(tip_of, tip_of_asset_id); } if let Some(tip) = params.0.tip { tx_params = tx_params.tip(tip); } let tx_hash = client .tx() .sign_and_submit(&tx, signing_keypair, tx_params.build()) .await .map_err(|e| { McpError::internal_error(format!("Failed to submit transaction: {}", e), None) })?; Ok(CallToolResult::success(vec![Content::text(format!( "Transaction {tx_hash:?} was successfully submitted", ))])) } /// Construct, sign, and submit a dynamic transaction and wait for it to be included in a block. /// /// # Parameters /// - `pallet_name`: Pallet name /// - `call_name`: Call name /// - `call_parameters`: Call parameters as a string /// - `mortality`, `nonce`, `tip_of_asset_id`, `tip`, `tip_of`: Optional transaction params /// /// # Returns /// The transaction hash and the block number it was included in. #[tool( description = "Constructs and sends a transaction and waits for it to be included in a block" )] pub async fn send_dynamic_transaction_and_wait( &self, params: Parameters<SendDynamicSignedTransactionParams>, ) -> Result<CallToolResult, McpError> { let signing_keypair = self .signing_keypair .as_ref() .ok_or(McpError::internal_error("Signing keypair is not set", None))?; let client = self.api.lock().await; let tx = subxt::dynamic::tx( params.0.pallet_name.clone(), params.0.call_name.clone(), vec![Value::from_bytes(params.0.call_parameters.as_bytes())], ); let mut tx_params = Params::new(); if let Some(mortality) = params.0.mortality { tx_params = tx_params.mortal(mortality); } if let Some(nonce) = params.0.nonce { tx_params = tx_params.nonce(nonce); } if let (Some(tip_of_asset_id), Some(tip_of)) = (params.0.tip_of_asset_id, params.0.tip_of) { tx_params = tx_params.tip_of(tip_of, tip_of_asset_id); } if let Some(tip) = params.0.tip { tx_params = tx_params.tip(tip); } let mut tx_progress = client .tx() .sign_and_submit_then_watch(&tx, signing_keypair, tx_params.build()) .await .map_err(|e| { McpError::internal_error(format!("Failed to send transaction: {}", e), None) })?; let mut result = None; while let Some(status) = tx_progress.next().await { if let TxStatus::InFinalizedBlock(in_block) = status.map_err(|e| { McpError::internal_error(format!("Failed to get transaction status: {}", e), None) })? { result = Some(in_block); } } let result = result.ok_or(McpError::internal_error( "Transaction could not be finalized", None, ))?; Ok(CallToolResult::success(vec![Content::text(format!( "Transaction {:?} is finalized in block {:?}", result.extrinsic_hash(), result.block_hash() ))])) } /// Query storage dynamically by providing pallet and storage item names. /// /// # Parameters /// - `pallet_name`: Pallet name /// - `entry_name`: Storage item name /// - `storage_keys`: Optional storage keys as a list of strings /// /// # Returns /// The storage value as a string. #[tool(description = "Query storage dynamically by providing pallet and storage item names")] pub async fn query_storage( &self, params: Parameters<QueryStorageParams>, ) -> Result<CallToolResult, McpError> { let client = self.api.lock().await; // Convert storage keys to Values if provided let keys: Vec<_> = params .0 .storage_keys .map(|keys| { keys.into_iter() .map(|k| Value::from_bytes(k.as_bytes())) .collect() }) .unwrap_or_default(); // Build the dynamic storage query let storage_query = subxt::dynamic::storage(&params.0.pallet_name, &params.0.entry_name, keys); // Execute the storage query let storage = client.storage().at_latest().await.map_err(|e| { McpError::internal_error(format!("Failed to access storage: {}", e), None) })?; let fetch_result = storage.fetch(&storage_query).await.map_err(|e| { McpError::resource_not_found( format!( "Failed to fetch storage for pallet: {}, storage: {}. Error: {}", params.0.pallet_name, params.0.entry_name, e ), None, ) })?; match fetch_result { Some(value) => { let decoded = value.to_value().map_err(|e| { McpError::internal_error(format!("Failed to decode storage value: {}", e), None) })?; Ok(CallToolResult::success(vec![Content::text( decoded.to_string(), )])) } None => Ok(CallToolResult::success(vec![Content::text( "No value found at storage location".to_string(), )])), } } /// Get all events from the latest block. /// /// # Returns /// A list of events with their details. #[tool(description = "Get all events from the latest block")] pub async fn get_latest_events(&self) -> Result<CallToolResult, McpError> { let client = self.api.lock().await; // Get events from the latest block let events = client.events().at_latest().await.map_err(|e| { McpError::internal_error(format!("Failed to fetch events: {}", e), None) })?; // Collect all events with their details let mut event_details = Vec::new(); for event in events.iter() { let event = event.map_err(|e| { McpError::internal_error(format!("Failed to decode event: {}", e), None) })?; event_details.push(Content::text(format!( "{}::{}: {}", event.pallet_name(), event.variant_name(), event.field_values().map_err(|e| { McpError::internal_error(format!("Failed to get field values: {}", e), None) })? ))); } Ok(CallToolResult::success(event_details)) } /// Find specific events by pallet and variant name. /// /// # Parameters /// - `pallet_name`: Pallet name /// - `event_name`: Event name /// /// # Returns /// A list of events with their details. #[tool(description = "Find specific events by pallet and variant name")] pub async fn find_events( &self, params: Parameters<FindEventsParams>, ) -> Result<CallToolResult, McpError> { let client = self.api.lock().await; // Get events from the latest block let events = client.events().at_latest().await.map_err(|e| { McpError::internal_error(format!("Failed to fetch events: {}", e), None) })?; // Find matching events let mut matching_events = Vec::new(); for event in events.iter() { let event = event.map_err(|e| { McpError::internal_error(format!("Failed to decode event: {}", e), None) })?; if event.pallet_name() == params.0.pallet_name && event.variant_name() == params.0.event_name { matching_events.push(Content::text(format!( "{}::{}: {}", params.0.pallet_name, params.0.event_name, event.field_values().map_err(|e| { McpError::internal_error(format!("Failed to get field values: {}", e), None) })? ))); } } if matching_events.is_empty() { matching_events.push(Content::text(format!( "No events found matching {}::{}", params.0.pallet_name, params.0.event_name ))); } Ok(CallToolResult::success(matching_events)) } /// Get a constant value from a specific pallet. /// /// # Parameters /// - `pallet_name`: Pallet name /// - `constant_name`: Constant name /// /// # Returns /// The constant value as a string. #[tool(description = "Get a constant value from a specific pallet")] pub async fn get_constant( &self, params: Parameters<GetConstantParams>, ) -> Result<CallToolResult, McpError> { let client = self.api.lock().await; // Create a dynamic constant query let constant_query = subxt::dynamic::constant(&params.0.pallet_name, &params.0.constant_name); // Get the constant value let value = client.constants().at(&constant_query).map_err(|e| { McpError::resource_not_found( format!( "Failed to get constant {}::{}: {}", params.0.pallet_name, params.0.constant_name, e ), None, ) })?; // Convert to a readable format let constant_value = value.to_value().map_err(|e| { McpError::internal_error(format!("Failed to decode constant value: {}", e), None) })?; Ok(CallToolResult::success(vec![Content::text(format!( "{}::{} = {}", params.0.pallet_name, params.0.constant_name, constant_value ))])) } /// Get details about the latest finalized block. /// /// # Returns /// A list of block details. #[tool(description = "Get details about the latest finalized block")] pub async fn get_latest_block(&self) -> Result<CallToolResult, McpError> { let client = self.api.lock().await; // Get the latest block let block = client.blocks().at_latest().await.map_err(|e| { McpError::internal_error(format!("Failed to get latest block: {}", e), None) })?; let mut details = Vec::new(); // Block header info let block_number = block.header().number; let block_hash = block.hash(); details.push(Content::text(format!("Block #{}", block_number))); details.push(Content::text(format!("Hash: {}", block_hash))); // Get extrinsics let extrinsics = block.extrinsics().await.map_err(|e| { McpError::internal_error(format!("Failed to get extrinsics: {}", e), None) })?; details.push(Content::text("Extrinsics:".to_string())); for ext in extrinsics.iter() { let idx = ext.index(); // Get extrinsic metadata let meta = ext.extrinsic_metadata().map_err(|e| { McpError::internal_error(format!("Failed to get extrinsic metadata: {}", e), None) })?; // Get field values let fields = ext.field_values().map_err(|e| { McpError::internal_error(format!("Failed to get field values: {}", e), None) })?; details.push(Content::text(format!( " #{}: {}/{}", idx, meta.pallet.name(), meta.variant.name ))); details.push(Content::text(format!(" Fields: {}", fields))); // Get associated events if let Ok(events) = ext.events().await { details.push(Content::text(" Events:".to_string())); for evt in events.iter().flatten() { if let Ok(values) = evt.field_values() { details.push(Content::text(format!( " {}::{}: {}", evt.pallet_name(), evt.variant_name(), values ))); } } } // Get transaction extensions if available if let Some(extensions) = ext.transaction_extensions() { details.push(Content::text(" Transaction Extensions:".to_string())); for extension in extensions.iter() { if let Ok(value) = extension.value() { details.push(Content::text(format!( " {}: {}", extension.name(), value ))); } } } } Ok(CallToolResult::success(details)) } /// Get details about a specific block by its hash. /// /// # Parameters /// - `block_hash`: Block hash /// /// # Returns /// A list of block details. #[tool(description = "Get details about a specific block by its hash")] pub async fn get_block_by_hash( &self, params: Parameters<GetBlockByHashParams>, ) -> Result<CallToolResult, McpError> { let client = self.api.lock().await; // Parse the block hash let hash_bytes = hex::decode(params.0.block_hash.trim_start_matches("0x")).map_err(|e| { McpError::invalid_params(format!("Invalid block hash format: {}", e), None) })?; // Get the block let block = client .blocks() .at(H256::from_slice(&hash_bytes)) .await .map_err(|e| { McpError::resource_not_found(format!("Failed to get block: {}", e), None) })?; let mut details = Vec::new(); // Block header info let block_number = block.header().number; details.push(Content::text(format!("Block #{}", block_number))); details.push(Content::text(format!("Hash: {}", params.0.block_hash))); // Get extrinsics let extrinsics = block.extrinsics().await.map_err(|e| { McpError::internal_error(format!("Failed to get extrinsics: {}", e), None) })?; details.push(Content::text("Extrinsics:".to_string())); for ext in extrinsics.iter() { let idx = ext.index(); let meta = ext.extrinsic_metadata().map_err(|e| { McpError::internal_error(format!("Failed to get extrinsic metadata: {}", e), None) })?; let fields = ext.field_values().map_err(|e| { McpError::internal_error(format!("Failed to get field values: {}", e), None) })?; details.push(Content::text(format!( " #{}: {}/{}", idx, meta.pallet.name(), meta.variant.name ))); details.push(Content::text(format!(" Fields: {}", fields))); // Get associated events if let Ok(events) = ext.events().await { details.push(Content::text(" Events:".to_string())); for evt in events.iter().flatten() { if let Ok(values) = evt.field_values() { details.push(Content::text(format!( " {}::{}: {}", evt.pallet_name(), evt.variant_name(), values ))); } } } } Ok(CallToolResult::success(details)) } /// Find specific extrinsics in the latest block by pallet and call names. /// /// # Parameters /// - `pallet_name`: Pallet name /// - `call_name`: Call name /// /// # Returns /// A list of extrinsics with their details. #[tool(description = "Find specific extrinsics in the latest block by pallet and call names")] pub async fn find_extrinsics( &self, params: Parameters<FindExtrinsicsParams>, ) -> Result<CallToolResult, McpError> { let client = self.api.lock().await; // Get the latest block let block = client.blocks().at_latest().await.map_err(|e| { McpError::internal_error(format!("Failed to get latest block: {}", e), None) })?; let mut found_extrinsics = Vec::new(); let extrinsics = block.extrinsics().await.map_err(|e| { McpError::internal_error(format!("Failed to get extrinsics: {}", e), None) })?; for ext in extrinsics.iter() { let meta = ext.extrinsic_metadata().map_err(|e| { McpError::internal_error(format!("Failed to get extrinsic metadata: {}", e), None) })?; if meta.pallet.name() == params.0.pallet_name && meta.variant.name == params.0.call_name { let idx = ext.index(); let fields = ext.field_values().map_err(|e| { McpError::internal_error(format!("Failed to get field values: {}", e), None) })?; found_extrinsics.push(Content::text(format!( "Extrinsic #{}: {}/{}\n Fields: {}", idx, params.0.pallet_name, params.0.call_name, fields ))); // Include associated events if let Ok(events) = ext.events().await { for evt in events.iter().flatten() { if let Ok(values) = evt.field_values() { found_extrinsics.push(Content::text(format!( " Event: {}::{}: {}", evt.pallet_name(), evt.variant_name(), values ))); } } } } } if found_extrinsics.is_empty() { found_extrinsics.push(Content::text(format!( "No extrinsics found matching {}/{}", params.0.pallet_name, params.0.call_name ))); } Ok(CallToolResult::success(found_extrinsics)) } /// Get basic system information via RPC. /// /// # Returns /// A list of system information. #[tool(description = "Get basic system information via RPC")] pub async fn get_system_info(&self) -> Result<CallToolResult, McpError> { let rpc = self.rpc_methods.lock().await; let mut info = Vec::new(); // Get various system information let name = rpc.system_name().await.map_err(|e| { McpError::internal_error(format!("Failed to get system name: {}", e), None) })?; info.push(Content::text(format!("System Name: {}", name))); let health = rpc.system_health().await.map_err(|e| { McpError::internal_error(format!("Failed to get system health: {}", e), None) })?; info.push(Content::text(format!("Health: {:?}", health))); let chain = rpc .system_chain() .await .map_err(|e| McpError::internal_error(format!("Failed to get chain: {}", e), None))?; info.push(Content::text(format!("Chain: {}", chain))); let properties = rpc.system_properties().await.map_err(|e| { McpError::internal_error(format!("Failed to get system properties: {}", e), None) })?; properties.iter().for_each(|(key, value)| { info.push(Content::text(format!("{}: {}", key, value))); }); Ok(CallToolResult::success(info)) } /// Make a custom RPC call. /// /// # Parameters /// - `method`: RPC method name /// - `params`: Optional parameters as a string /// /// # Returns /// The result of the RPC call as a string. #[tool(description = "Make a custom RPC call")] pub async fn custom_rpc( &self, params: Parameters<CustomRpcParams>, ) -> Result<CallToolResult, McpError> { let rpc_client = self.rpc_client.lock().await; let rpc_params_str = params.0.params.unwrap_or_default(); let mut rpc_params = RpcParams::new(); rpc_params.push(rpc_params_str).map_err(|e| { McpError::internal_error(format!("Failed to parse params: {}", e), None) })?; let result: String = rpc_client .request(&params.0.method, rpc_params) .await .map_err(|e| McpError::internal_error(format!("RPC call failed: {}", e), None))?; Ok(CallToolResult::success(vec![Content::text(format!( "{}: {}", params.0.method, result ))])) } } impl ServerHandler for SubstrateTool { fn get_info(&self) -> ServerInfo { ServerInfo { protocol_version: ProtocolVersion::V_2024_11_05, capabilities: ServerCapabilities::builder() .enable_prompts() .enable_resources() .enable_tools() .build(), server_info: Implementation::from_build_env(), instructions: Some("This server provides Substrate integration tools. You can query account balances using the query_balance tool.".to_string()), } } async fn list_resources( &self, _request: Option<PaginatedRequestParam>, _: RequestContext<RoleServer>, ) -> Result<ListResourcesResult, McpError> { Ok(ListResourcesResult { resources: vec![], next_cursor: None, }) } async fn read_resource( &self, ReadResourceRequestParam { uri }: ReadResourceRequestParam, _: RequestContext<RoleServer>, ) -> Result<ReadResourceResult, McpError> { Err(McpError::resource_not_found( "resource_not_found", Some(json!({ "uri": uri })), )) } async fn list_prompts( &self, _request: Option<PaginatedRequestParam>, _: RequestContext<RoleServer>, ) -> Result<ListPromptsResult, McpError> { Ok(ListPromptsResult { next_cursor: None, prompts: vec![Prompt::new( "example_prompt", Some("This is an example prompt that takes one required argument, message"), Some(vec![PromptArgument { name: "message".to_string(), title: Some("Message".to_string()), description: Some("A message to put in the prompt".to_string()), required: Some(true), }]), )], }) } async fn get_prompt( &self, GetPromptRequestParam { name, arguments }: GetPromptRequestParam, _: RequestContext<RoleServer>, ) -> Result<GetPromptResult, McpError> { match name.as_str() { "example_prompt" => { let message = arguments .and_then(|json| json.get("message")?.as_str().map(|s| s.to_string())) .ok_or_else(|| { McpError::invalid_params("No message provided to example_prompt", None) })?; let prompt = format!("This is an example prompt with your message here: '{message}'"); Ok(GetPromptResult { description: None, messages: vec![PromptMessage { role: PromptMessageRole::User, content: PromptMessageContent::text(prompt), }], }) } _ => Err(McpError::invalid_params("prompt not found", None)), } } async fn list_resource_templates( &self, _request: Option<PaginatedRequestParam>, _: RequestContext<RoleServer>, ) -> Result<ListResourceTemplatesResult, McpError> { Ok(ListResourceTemplatesResult { next_cursor: None, resource_templates: Vec::new(), }) } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ThomasMarches/substrate-mcp-rs'

If you have feedback or need assistance with the MCP directory API, please join our Discord server