@@ -6,24 +6,41 @@ use std::collections::HashMap;
66use tokio:: sync:: RwLock ;
77use crate :: transport:: { Transport , JsonRpcMessage , JsonRpcNotification , JsonRpcVersion } ;
88use crate :: { Config , CustomStdioTransport } ;
9+ use crate :: validation:: sanitize_for_logging;
910
11+ /// Server state containing RPC clients and configuration
12+ ///
13+ /// Manages the main Solana RPC client, additional SVM network clients,
14+ /// and server configuration. Thread-safe through Arc<RwLock<>> wrapper.
1015pub struct ServerState {
16+ /// Primary Solana RPC client
1117 pub rpc_client : RpcClient ,
18+ /// Map of enabled SVM network clients by network ID
1219 pub svm_clients : HashMap < String , RpcClient > ,
20+ /// Current server configuration
1321 pub config : Config ,
22+ /// Whether the server has been initialized
1423 pub initialized : bool ,
24+ /// Protocol version being used
1525 pub protocol_version : String ,
1626}
1727
1828impl ServerState {
29+ /// Creates a new ServerState with the given configuration
30+ ///
31+ /// # Arguments
32+ /// * `config` - Validated configuration to use
33+ ///
34+ /// # Returns
35+ /// * `Self` - New ServerState instance
36+ ///
37+ /// # Security
38+ /// - Only creates RPC clients for enabled networks
39+ /// - Uses validated configuration with HTTPS enforcement
1940 pub fn new ( config : Config ) -> Self {
20- let commitment = match config. commitment . as_str ( ) {
21- "processed" => CommitmentConfig :: processed ( ) ,
22- "confirmed" => CommitmentConfig :: confirmed ( ) ,
23- "finalized" => CommitmentConfig :: finalized ( ) ,
24- _ => CommitmentConfig :: default ( ) ,
25- } ;
41+ let commitment = Self :: parse_commitment ( & config. commitment ) ;
2642
43+ log:: info!( "Creating RPC client for: {}" , sanitize_for_logging( & config. rpc_url) ) ;
2744 let rpc_client = RpcClient :: new_with_commitment (
2845 config. rpc_url . clone ( ) ,
2946 commitment. clone ( ) ,
@@ -33,6 +50,8 @@ impl ServerState {
3350 let mut svm_clients = HashMap :: new ( ) ;
3451 for ( network_id, network) in & config. svm_networks {
3552 if network. enabled {
53+ log:: info!( "Creating SVM client for network '{}': {}" ,
54+ network_id, sanitize_for_logging( & network. rpc_url) ) ;
3655 let client = RpcClient :: new_with_commitment (
3756 network. rpc_url . clone ( ) ,
3857 commitment. clone ( ) ,
@@ -50,16 +69,20 @@ impl ServerState {
5069 }
5170 }
5271
72+ /// Updates the server configuration and recreates clients as needed
73+ ///
74+ /// # Arguments
75+ /// * `new_config` - New validated configuration
76+ ///
77+ /// # Security
78+ /// - Validates new configuration before applying
79+ /// - Recreates clients with new URLs securely
5380 pub fn update_config ( & mut self , new_config : Config ) {
54- let commitment = match new_config. commitment . as_str ( ) {
55- "processed" => CommitmentConfig :: processed ( ) ,
56- "confirmed" => CommitmentConfig :: confirmed ( ) ,
57- "finalized" => CommitmentConfig :: finalized ( ) ,
58- _ => CommitmentConfig :: default ( ) ,
59- } ;
81+ let commitment = Self :: parse_commitment ( & new_config. commitment ) ;
6082
6183 // Update main RPC client if URL changed
6284 if self . config . rpc_url != new_config. rpc_url {
85+ log:: info!( "Updating main RPC client to: {}" , sanitize_for_logging( & new_config. rpc_url) ) ;
6386 self . rpc_client = RpcClient :: new_with_commitment (
6487 new_config. rpc_url . clone ( ) ,
6588 commitment. clone ( ) ,
@@ -70,6 +93,8 @@ impl ServerState {
7093 self . svm_clients . clear ( ) ;
7194 for ( network_id, network) in & new_config. svm_networks {
7295 if network. enabled {
96+ log:: info!( "Creating/updating SVM client for network '{}': {}" ,
97+ network_id, sanitize_for_logging( & network. rpc_url) ) ;
7398 let client = RpcClient :: new_with_commitment (
7499 network. rpc_url . clone ( ) ,
75100 commitment. clone ( ) ,
@@ -81,25 +106,69 @@ impl ServerState {
81106 self . config = new_config;
82107 }
83108
109+ /// Gets list of enabled network IDs
110+ ///
111+ /// # Returns
112+ /// * `Vec<&str>` - List of enabled network identifiers
84113 pub fn get_enabled_networks ( & self ) -> Vec < & str > {
85114 self . config . svm_networks
86115 . iter ( )
87116 . filter ( |( _, network) | network. enabled )
88117 . map ( |( id, _) | id. as_str ( ) )
89118 . collect ( )
90119 }
120+
121+ /// Parses commitment string into CommitmentConfig
122+ ///
123+ /// # Arguments
124+ /// * `commitment_str` - String representation of commitment level
125+ ///
126+ /// # Returns
127+ /// * `CommitmentConfig` - Parsed commitment configuration
128+ fn parse_commitment ( commitment_str : & str ) -> CommitmentConfig {
129+ match commitment_str {
130+ "processed" => CommitmentConfig :: processed ( ) ,
131+ "confirmed" => CommitmentConfig :: confirmed ( ) ,
132+ "finalized" => CommitmentConfig :: finalized ( ) ,
133+ _ => {
134+ log:: warn!( "Invalid commitment '{}', using default (finalized)" , commitment_str) ;
135+ CommitmentConfig :: finalized ( )
136+ }
137+ }
138+ }
91139}
92140
141+ /// Starts the Solana MCP server with stdio transport
142+ ///
143+ /// Initializes the server with configuration validation, sets up transport,
144+ /// sends protocol negotiation, and starts the main message loop.
145+ ///
146+ /// # Returns
147+ /// * `Result<()>` - Ok if server shuts down cleanly, Err on critical errors
148+ ///
149+ /// # Security
150+ /// - Validates configuration before starting
151+ /// - Uses secure transport with proper error handling
152+ /// - Implements graceful shutdown on connection close
93153pub async fn start_server ( ) -> Result < ( ) > {
94154 log:: info!( "Starting Solana MCP server..." ) ;
95155
96- let config = Config :: load ( ) ?;
97- log:: info!( "Loaded config: RPC URL: {}, Protocol Version: {}" , config. rpc_url, config. protocol_version) ;
156+ // Load and validate configuration
157+ let config = Config :: load ( ) . map_err ( |e| {
158+ log:: error!( "Failed to load configuration: {}" , e) ;
159+ e
160+ } ) ?;
161+
162+ log:: info!( "Loaded config: RPC URL: {}, Protocol Version: {}" ,
163+ sanitize_for_logging( & config. rpc_url) , config. protocol_version) ;
98164
99165 let state = Arc :: new ( RwLock :: new ( ServerState :: new ( config. clone ( ) ) ) ) ;
100166
101167 let transport = CustomStdioTransport :: new ( ) ;
102- transport. open ( ) ?;
168+ transport. open ( ) . map_err ( |e| {
169+ log:: error!( "Failed to open transport: {}" , e) ;
170+ e
171+ } ) ?;
103172 log:: info!( "Opened stdio transport" ) ;
104173
105174 // Send initial protocol version notification
@@ -110,30 +179,66 @@ pub async fn start_server() -> Result<()> {
110179 params : Some ( serde_json:: json!( {
111180 "version" : config. protocol_version. clone( )
112181 } ) ) ,
113- } ) ) ?;
182+ } ) ) . map_err ( |e| {
183+ log:: error!( "Failed to send protocol notification: {}" , e) ;
184+ e
185+ } ) ?;
114186
115- // Start message loop
187+ // Start message loop with proper error handling
116188 log:: info!( "Starting message loop" ) ;
117189 loop {
118190 match transport. receive ( ) {
119191 Ok ( message) => {
120- let message_str = serde_json:: to_string ( & message) ?;
121- log:: debug!( "Received message: {}" , message_str) ;
122- let response = crate :: tools:: handle_request ( & message_str, state. clone ( ) ) . await ?;
123- log:: debug!( "Sending response: {}" , serde_json:: to_string( & response) ?) ;
124- transport. send ( & response) ?;
192+ // Handle message without logging sensitive content
193+ log:: debug!( "Received message of type: {}" , get_message_type( & message) ) ;
194+
195+ match handle_message ( message, state. clone ( ) ) . await {
196+ Ok ( response) => {
197+ log:: debug!( "Sending response" ) ;
198+ if let Err ( e) = transport. send ( & response) {
199+ log:: error!( "Failed to send response: {}" , e) ;
200+ break ;
201+ }
202+ }
203+ Err ( e) => {
204+ log:: error!( "Error handling message: {}" , e) ;
205+ // Continue processing other messages
206+ }
207+ }
125208 }
126209 Err ( e) => {
127- if e. to_string ( ) . contains ( "Connection closed" ) {
128- log:: info!( "Client disconnected" ) ;
210+ let error_msg = e. to_string ( ) ;
211+ if error_msg. contains ( "Connection closed" ) || error_msg. contains ( "EOF" ) {
212+ log:: info!( "Client disconnected gracefully" ) ;
129213 break ;
214+ } else {
215+ log:: error!( "Error receiving message: {}" , e) ;
216+ // For non-connection errors, continue trying
130217 }
131- log:: error!( "Error receiving message: {}" , e) ;
132218 }
133219 }
134220 }
135221
136222 log:: info!( "Closing transport" ) ;
137- transport. close ( ) ?;
223+ if let Err ( e) = transport. close ( ) {
224+ log:: warn!( "Error closing transport: {}" , e) ;
225+ }
226+
227+ log:: info!( "Solana MCP server stopped" ) ;
138228 Ok ( ( ) )
139229}
230+
231+ /// Handles a received message and returns appropriate response
232+ async fn handle_message ( message : JsonRpcMessage , state : Arc < RwLock < ServerState > > ) -> Result < JsonRpcMessage > {
233+ let message_str = serde_json:: to_string ( & message) ?;
234+ crate :: tools:: handle_request ( & message_str, state) . await
235+ }
236+
237+ /// Gets the message type for safe logging
238+ fn get_message_type ( message : & JsonRpcMessage ) -> & ' static str {
239+ match message {
240+ JsonRpcMessage :: Request ( _) => "request" ,
241+ JsonRpcMessage :: Response ( _) => "response" ,
242+ JsonRpcMessage :: Notification ( _) => "notification" ,
243+ }
244+ }
0 commit comments