scim_server/mcp_integration/
protocol.rs

1//! MCP protocol layer for tool discovery and dispatch
2//!
3//! This module handles the core MCP protocol functionality including tool discovery,
4//! execution dispatch, and protocol communication. It serves as the interface between
5//! AI agents and the SCIM server operations.
6
7use super::core::{ScimMcpServer, ScimToolResult};
8use super::handlers::{group_crud, group_queries, system_info, user_crud, user_queries};
9use super::tools::{group_schemas, system_schemas, user_schemas};
10use crate::ResourceProvider;
11use log::{debug, error, info, warn};
12use serde::{Deserialize, Serialize};
13use serde_json::{Value, json};
14use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
15
16/// MCP JSON-RPC request structure
17#[derive(Debug, Deserialize)]
18struct McpRequest {
19    #[allow(dead_code)]
20    jsonrpc: String,
21    id: Option<Value>,
22    method: String,
23    params: Option<Value>,
24}
25
26/// MCP JSON-RPC response structure
27#[derive(Debug, Serialize)]
28pub struct McpResponse {
29    pub jsonrpc: String,
30    pub id: Option<Value>,
31    pub result: Option<Value>,
32    pub error: Option<Value>,
33}
34
35/// MCP JSON-RPC error structure
36#[derive(Debug, Serialize)]
37struct McpError {
38    code: i32,
39    message: String,
40    data: Option<Value>,
41}
42
43impl McpResponse {
44    /// Create a successful response
45    fn success(id: Option<Value>, result: Value) -> Self {
46        Self {
47            jsonrpc: "2.0".to_string(),
48            id,
49            result: Some(result),
50            error: None,
51        }
52    }
53
54    /// Create an error response
55    fn error(id: Option<Value>, code: i32, message: String) -> Self {
56        Self {
57            jsonrpc: "2.0".to_string(),
58            id,
59            result: None,
60            error: Some(json!(McpError {
61                code,
62                message,
63                data: None
64            })),
65        }
66    }
67}
68
69impl<P: ResourceProvider + Send + Sync + 'static> ScimMcpServer<P> {
70    /// Get the list of available MCP tools as JSON
71    ///
72    /// Returns all tool definitions that AI agents can discover and execute.
73    /// Each tool includes its schema, parameters, and documentation.
74    ///
75    /// # Examples
76    ///
77    /// ```rust
78    /// # #[cfg(feature = "mcp")]
79    /// use scim_server::mcp_integration::ScimMcpServer;
80    /// use scim_server::providers::StandardResourceProvider;
81    /// use scim_server::storage::InMemoryStorage;
82    /// # async fn example(mcp_server: ScimMcpServer<StandardResourceProvider<InMemoryStorage>>) {
83    /// let tools = mcp_server.get_tools();
84    /// println!("Available tools: {}", tools.len());
85    /// # }
86    /// ```
87    pub fn get_tools(&self) -> Vec<Value> {
88        vec![
89            // User operations
90            user_schemas::create_user_tool(),
91            user_schemas::get_user_tool(),
92            user_schemas::update_user_tool(),
93            user_schemas::delete_user_tool(),
94            user_schemas::list_users_tool(),
95            user_schemas::search_users_tool(),
96            user_schemas::user_exists_tool(),
97            // Group operations
98            group_schemas::create_group_tool(),
99            group_schemas::get_group_tool(),
100            group_schemas::update_group_tool(),
101            group_schemas::delete_group_tool(),
102            group_schemas::list_groups_tool(),
103            group_schemas::search_groups_tool(),
104            group_schemas::group_exists_tool(),
105            // System operations
106            system_schemas::get_schemas_tool(),
107            system_schemas::get_server_info_tool(),
108        ]
109    }
110
111    /// Execute a tool by name with arguments
112    ///
113    /// This is the main dispatch function that routes tool execution requests
114    /// to the appropriate handler based on the tool name.
115    ///
116    /// # Arguments
117    /// * `tool_name` - The name of the tool to execute
118    /// * `arguments` - JSON arguments for the tool execution
119    ///
120    /// # Returns
121    /// A `ScimToolResult` containing the execution outcome
122    pub async fn execute_tool(&self, tool_name: &str, arguments: Value) -> ScimToolResult {
123        debug!("Executing MCP tool: {} with args: {}", tool_name, arguments);
124
125        match tool_name {
126            // User CRUD operations
127            "scim_create_user" => user_crud::handle_create_user(self, arguments).await,
128            "scim_get_user" => user_crud::handle_get_user(self, arguments).await,
129            "scim_update_user" => user_crud::handle_update_user(self, arguments).await,
130            "scim_delete_user" => user_crud::handle_delete_user(self, arguments).await,
131
132            // User query operations
133            "scim_list_users" => user_queries::handle_list_users(self, arguments).await,
134            "scim_search_users" => user_queries::handle_search_users(self, arguments).await,
135            "scim_user_exists" => user_queries::handle_user_exists(self, arguments).await,
136
137            // Group CRUD operations
138            "scim_create_group" => group_crud::handle_create_group(self, arguments).await,
139            "scim_get_group" => group_crud::handle_get_group(self, arguments).await,
140            "scim_update_group" => group_crud::handle_update_group(self, arguments).await,
141            "scim_delete_group" => group_crud::handle_delete_group(self, arguments).await,
142
143            // Group query operations
144            "scim_list_groups" => group_queries::handle_list_groups(self, arguments).await,
145            "scim_search_groups" => group_queries::handle_search_groups(self, arguments).await,
146            "scim_group_exists" => group_queries::handle_group_exists(self, arguments).await,
147
148            // System information operations
149            "scim_get_schemas" => system_info::handle_get_schemas(self, arguments).await,
150            "scim_server_info" => system_info::handle_server_info(self, arguments).await,
151
152            // Unknown tool
153            _ => ScimToolResult {
154                success: false,
155                content: json!({
156                    "error": "Unknown tool",
157                    "tool_name": tool_name
158                }),
159                metadata: None,
160            },
161        }
162    }
163
164    /// Run the MCP server using stdio communication
165    ///
166    /// Starts the MCP server and begins listening for tool execution requests
167    /// over standard input/output. This is the standard MCP communication method.
168    ///
169    /// # Examples
170    ///
171    /// ```rust,no_run
172    /// # #[cfg(feature = "mcp")]
173    /// use scim_server::mcp_integration::ScimMcpServer;
174    /// use scim_server::providers::StandardResourceProvider;
175    /// use scim_server::storage::InMemoryStorage;
176    /// # async fn example(mcp_server: ScimMcpServer<StandardResourceProvider<InMemoryStorage>>) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
177    /// // Run MCP server
178    /// mcp_server.run_stdio().await?;
179    /// # Ok(())
180    /// # }
181    /// ```
182    pub async fn run_stdio(self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
183        info!("SCIM MCP server starting stdio communication");
184        info!(
185            "Available tools: {:?}",
186            self.get_tools()
187                .iter()
188                .map(|t| t.get("name"))
189                .collect::<Vec<_>>()
190        );
191
192        let stdin = tokio::io::stdin();
193        let mut stdout = tokio::io::stdout();
194        let mut reader = BufReader::new(stdin);
195        let mut line = String::new();
196
197        info!("SCIM MCP server ready - listening on stdio");
198
199        loop {
200            line.clear();
201            match reader.read_line(&mut line).await {
202                Ok(0) => {
203                    debug!("EOF received, shutting down");
204                    break; // EOF
205                }
206                Ok(_) => {
207                    let line_content = line.trim();
208                    if line_content.is_empty() {
209                        continue;
210                    }
211
212                    debug!("Received request: {}", line_content);
213
214                    if let Some(response) = self.handle_mcp_request(line_content).await {
215                        let response_json = match serde_json::to_string(&response) {
216                            Ok(json) => json,
217                            Err(e) => {
218                                error!("Failed to serialize response: {}", e);
219                                continue;
220                            }
221                        };
222
223                        debug!("Sending response: {}", response_json);
224
225                        if let Err(e) = stdout.write_all(response_json.as_bytes()).await {
226                            error!("Failed to write response: {}", e);
227                            break;
228                        }
229                        if let Err(e) = stdout.write_all(b"\n").await {
230                            error!("Failed to write newline: {}", e);
231                            break;
232                        }
233                        if let Err(e) = stdout.flush().await {
234                            error!("Failed to flush stdout: {}", e);
235                            break;
236                        }
237                    }
238                }
239                Err(e) => {
240                    error!("Error reading from stdin: {}", e);
241                    break;
242                }
243            }
244        }
245
246        info!("SCIM MCP server shutting down");
247        Ok(())
248    }
249
250    /// Handle a single MCP request and return the appropriate response
251    pub async fn handle_mcp_request(&self, line: &str) -> Option<McpResponse> {
252        let request: McpRequest = match serde_json::from_str(line) {
253            Ok(req) => req,
254            Err(e) => {
255                warn!("Failed to parse JSON request: {} - Input: {}", e, line);
256                return Some(McpResponse::error(None, -32700, "Parse error".to_string()));
257            }
258        };
259
260        debug!(
261            "Processing method: {} with id: {:?}",
262            request.method, request.id
263        );
264
265        match request.method.as_str() {
266            "initialize" => Some(self.handle_initialize(request.id)),
267            "notifications/initialized" => {
268                debug!("Received initialized notification - handshake complete");
269                None // Notifications don't require responses
270            }
271            "tools/list" => Some(self.handle_tools_list(request.id)),
272            "tools/call" => Some(self.handle_tools_call(request.id, request.params).await),
273            "ping" => Some(self.handle_ping(request.id)),
274            _ => {
275                warn!("Unknown method: {}", request.method);
276                Some(McpResponse::error(
277                    request.id,
278                    -32601,
279                    "Method not found".to_string(),
280                ))
281            }
282        }
283    }
284
285    /// Handle initialize request
286    fn handle_initialize(&self, id: Option<Value>) -> McpResponse {
287        debug!("Handling initialize request");
288
289        let result = json!({
290            "protocolVersion": "2024-11-05",
291            "capabilities": {
292                "tools": {}
293            },
294            "serverInfo": {
295                "name": self.server_info.name,
296                "version": self.server_info.version,
297                "description": self.server_info.description
298            }
299        });
300
301        McpResponse::success(id, result)
302    }
303
304    /// Handle tools/list request
305    fn handle_tools_list(&self, id: Option<Value>) -> McpResponse {
306        debug!("Handling tools/list request");
307
308        let tools = self.get_tools();
309        let result = json!({
310            "tools": tools
311        });
312
313        McpResponse::success(id, result)
314    }
315
316    /// Handle tools/call request
317    async fn handle_tools_call(&self, id: Option<Value>, params: Option<Value>) -> McpResponse {
318        debug!("Handling tools/call request");
319
320        let params = match params {
321            Some(p) => p,
322            None => {
323                return McpResponse::error(
324                    id,
325                    -32602,
326                    "Invalid params: missing parameters".to_string(),
327                );
328            }
329        };
330
331        let tool_name = match params.get("name").and_then(|n| n.as_str()) {
332            Some(name) => name,
333            None => {
334                return McpResponse::error(
335                    id,
336                    -32602,
337                    "Invalid params: missing tool name".to_string(),
338                );
339            }
340        };
341
342        let arguments = params.get("arguments").cloned().unwrap_or(json!({}));
343
344        debug!(
345            "Executing tool: {} with arguments: {}",
346            tool_name, arguments
347        );
348
349        let tool_result = self.execute_tool(tool_name, arguments).await;
350
351        if tool_result.success {
352            let result = json!({
353                "content": [
354                    {
355                        "type": "text",
356                        "text": serde_json::to_string_pretty(&tool_result.content)
357                            .unwrap_or_else(|_| "Error serializing result".to_string())
358                    }
359                ],
360                "_meta": tool_result.metadata
361            });
362
363            McpResponse::success(id, result)
364        } else {
365            McpResponse::error(
366                id,
367                -32000,
368                format!(
369                    "Tool execution failed: {}",
370                    tool_result
371                        .content
372                        .get("error")
373                        .and_then(|e| e.as_str())
374                        .unwrap_or("Unknown error")
375                ),
376            )
377        }
378    }
379
380    /// Handle ping request
381    fn handle_ping(&self, id: Option<Value>) -> McpResponse {
382        debug!("Handling ping request");
383        McpResponse::success(id, json!({}))
384    }
385}