Skip to main content

ai_agent/plugin/
mcp.rs

1// Source: /data/home/swei/claudecode/openclaudecode/src/entrypoints/mcp.ts
2//! Plugin MCP server implementation - ported from ~/claudecode/openclaudecode/src/utils/plugins/mcpPluginIntegration.ts
3//!
4//! This module provides MCP server management for plugins, including:
5//! - Loading MCP server configs from plugin manifests
6//! - Server lifecycle management (start, stop, status)
7//! - Support for stdio and SSE transport types
8
9use crate::error::AgentError;
10use crate::mcp::McpConnection;
11use crate::types::{McpConnectionStatus, McpServerConfig, McpSseConfig, McpStdioConfig};
12use serde::{Deserialize, Serialize};
13use std::collections::HashMap;
14use std::path::Path;
15use std::process::Stdio;
16use std::sync::Arc;
17use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
18use tokio::process::Command;
19use tokio::sync::RwLock;
20
21/// Transport type for MCP server
22#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
23#[serde(rename_all = "lowercase")]
24pub enum PluginMcpTransport {
25    Stdio,
26    Sse,
27    Http,
28    #[serde(other)]
29    Unknown,
30}
31
32impl Default for PluginMcpTransport {
33    fn default() -> Self {
34        PluginMcpTransport::Stdio
35    }
36}
37
38/// Status of a plugin MCP server
39#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
40#[serde(rename_all = "lowercase")]
41pub enum PluginMcpServerStatus {
42    /// Server is not started
43    Stopped,
44    /// Server is starting up
45    Starting,
46    /// Server is running and connected
47    Running,
48    /// Server encountered an error
49    Error,
50    /// Server is disabled
51    Disabled,
52}
53
54/// Plugin MCP server configuration
55#[derive(Debug, Clone, Serialize, Deserialize)]
56#[serde(rename_all = "camelCase")]
57pub struct PluginMcpServerConfig {
58    /// Transport type (stdio, sse, http)
59    pub transport_type: Option<PluginMcpTransport>,
60    /// Server command (for stdio)
61    pub command: Option<String>,
62    /// Server arguments (for stdio)
63    pub args: Option<Vec<String>>,
64    /// Environment variables (for stdio)
65    pub env: Option<HashMap<String, String>>,
66    /// Server URL (for sse/http)
67    pub url: Option<String>,
68    /// HTTP headers (for sse/http)
69    pub headers: Option<HashMap<String, String>>,
70    /// Scope: local, user, project, dynamic, enterprise
71    pub scope: Option<String>,
72    /// Plugin source identifier
73    pub plugin_source: Option<String>,
74}
75
76impl PluginMcpServerConfig {
77    /// Convert to standard McpServerConfig
78    pub fn to_mcp_config(&self) -> Option<McpServerConfig> {
79        let transport = self
80            .transport_type
81            .as_ref()
82            .unwrap_or(&PluginMcpTransport::Stdio);
83
84        match transport {
85            PluginMcpTransport::Stdio => {
86                let command = self.command.as_ref()?;
87                Some(McpServerConfig::Stdio(McpStdioConfig {
88                    transport_type: Some("stdio".to_string()),
89                    command: command.clone(),
90                    args: self.args.clone(),
91                    env: self.env.clone(),
92                }))
93            }
94            PluginMcpTransport::Sse => {
95                let url = self.url.as_ref()?;
96                Some(McpServerConfig::Sse(McpSseConfig {
97                    transport_type: "sse".to_string(),
98                    url: url.clone(),
99                    headers: self.headers.clone(),
100                }))
101            }
102            PluginMcpTransport::Http => {
103                let url = self.url.as_ref()?;
104                Some(McpServerConfig::Http(crate::types::McpHttpConfig {
105                    transport_type: "http".to_string(),
106                    url: url.clone(),
107                    headers: self.headers.clone(),
108                }))
109            }
110            PluginMcpTransport::Unknown => None,
111        }
112    }
113}
114
115/// Plugin MCP server instance
116#[derive(Debug)]
117pub struct PluginMcpServer {
118    /// Server name
119    pub name: String,
120    /// Server configuration
121    pub config: PluginMcpServerConfig,
122    /// Current status
123    pub status: PluginMcpServerStatus,
124    /// Child process handle (for stdio servers)
125    child: Option<tokio::process::Child>,
126    /// MCP connection if running
127    connection: Option<McpConnection>,
128    /// Plugin path for resolving relative paths
129    plugin_path: String,
130    /// Plugin source identifier
131    _plugin_source: String,
132}
133
134impl PluginMcpServer {
135    /// Create a new plugin MCP server
136    pub fn new(
137        name: String,
138        config: PluginMcpServerConfig,
139        plugin_path: String,
140        plugin_source: String,
141    ) -> Self {
142        Self {
143            name,
144            config,
145            status: PluginMcpServerStatus::Stopped,
146            child: None,
147            connection: None,
148            plugin_path,
149            _plugin_source: plugin_source,
150        }
151    }
152
153    /// Start the MCP server
154    pub async fn start(&mut self) -> Result<(), AgentError> {
155        if self.status == PluginMcpServerStatus::Running {
156            return Ok(());
157        }
158
159        self.status = PluginMcpServerStatus::Starting;
160
161        let mcp_config = self.config.to_mcp_config().ok_or_else(|| {
162            AgentError::Mcp(format!("Invalid MCP config for server {}", self.name))
163        })?;
164
165        // Resolve environment variables including plugin-specific ones
166        let resolved_config = self.resolve_environment(&mcp_config);
167
168        match resolved_config {
169            McpServerConfig::Stdio(stdio_config) => {
170                self.start_stdio(stdio_config).await?;
171            }
172            McpServerConfig::Sse(sse_config) => {
173                self.start_sse(sse_config).await?;
174            }
175            McpServerConfig::Http(http_config) => {
176                self.start_http(http_config).await?;
177            }
178        }
179
180        self.status = PluginMcpServerStatus::Running;
181        Ok(())
182    }
183
184    /// Start stdio-based MCP server
185    async fn start_stdio(&mut self, config: McpStdioConfig) -> Result<(), AgentError> {
186        let mut env_vars: HashMap<String, String> = std::env::vars().collect();
187
188        // Add plugin-specific environment variables
189        env_vars.insert("AI_PLUGIN_ROOT".to_string(), self.plugin_path.clone());
190
191        // Add custom env vars
192        if let Some(custom_env) = &config.env {
193            for (key, value) in custom_env {
194                env_vars.insert(key.clone(), value.clone());
195            }
196        }
197
198        let command = config.command.clone();
199        let args = config.args.unwrap_or_default();
200
201        let mut child = Command::new(&command)
202            .args(&args)
203            .envs(&env_vars)
204            .kill_on_drop(true)
205            .stdout(Stdio::piped())
206            .stderr(Stdio::piped())
207            .stdin(Stdio::piped())
208            .spawn()
209            .map_err(|e| {
210                AgentError::Mcp(format!("Failed to spawn MCP server '{}': {}", command, e))
211            })?;
212
213        let stdout = child
214            .stdout
215            .take()
216            .ok_or_else(|| AgentError::Mcp("Failed to take stdout from MCP server".to_string()))?;
217
218        let mut stdin = child
219            .stdin
220            .take()
221            .ok_or_else(|| AgentError::Mcp("Failed to take stdin from MCP server".to_string()))?;
222
223        let mut stdout_reader = BufReader::new(stdout).lines();
224
225        // Send initialize request
226        let initialize_request = serde_json::json!({
227            "jsonrpc": "2.0",
228            "id": 1,
229            "method": "initialize",
230            "params": {
231                "protocolVersion": "2024-11-05",
232                "capabilities": {},
233                "clientInfo": {
234                    "name": format!("agent-sdk-plugin-{}", self.name),
235                    "version": "1.0.0"
236                }
237            }
238        });
239
240        stdin
241            .write_all(format!("{initialize_request}\n").as_bytes())
242            .await
243            .map_err(|e| {
244                AgentError::Io(std::io::Error::new(
245                    std::io::ErrorKind::Other,
246                    e.to_string(),
247                ))
248            })?;
249        stdin.flush().await.map_err(|e| {
250            AgentError::Io(std::io::Error::new(
251                std::io::ErrorKind::Other,
252                e.to_string(),
253            ))
254        })?;
255
256        // Read initialize response
257        let _ = stdout_reader.next_line().await;
258
259        // Send tools/list request
260        let list_tools_request = serde_json::json!({
261            "jsonrpc": "2.0",
262            "id": 2,
263            "method": "tools/list"
264        });
265
266        stdin
267            .write_all(format!("{list_tools_request}\n").as_bytes())
268            .await
269            .map_err(|e| {
270                AgentError::Io(std::io::Error::new(
271                    std::io::ErrorKind::Other,
272                    e.to_string(),
273                ))
274            })?;
275        stdin.flush().await.map_err(|e| {
276            AgentError::Io(std::io::Error::new(
277                std::io::ErrorKind::Other,
278                e.to_string(),
279            ))
280        })?;
281
282        // Read tools/list response
283        let mut tools = vec![];
284        if let Ok(Some(response)) = stdout_reader.next_line().await {
285            if let Ok(resp) = serde_json::from_str::<serde_json::Value>(&response) {
286                if let Some(result) = resp.get("result") {
287                    if let Some(tools_array) = result.get("tools").and_then(|t| t.as_array()) {
288                        for tool_val in tools_array {
289                            if let Ok(mcp_tool) =
290                                serde_json::from_value::<crate::types::McpTool>(tool_val.clone())
291                            {
292                                let tool_def = create_mcp_tool_definition(&self.name, &mcp_tool);
293                                tools.push(tool_def);
294                            }
295                        }
296                    }
297                }
298            }
299        }
300
301        // Drop stdin to signal EOF, but keep process running
302        drop(stdin);
303
304        self.child = Some(child);
305        self.connection = Some(McpConnection {
306            name: self.name.clone(),
307            status: McpConnectionStatus::Connected,
308            tools,
309        });
310
311        Ok(())
312    }
313
314    /// Start SSE-based MCP server
315    async fn start_sse(&mut self, _config: McpSseConfig) -> Result<(), AgentError> {
316        // SSE support would require the SSE client implementation
317        // For now, mark as running with placeholder connection
318        self.connection = Some(McpConnection {
319            name: self.name.clone(),
320            status: McpConnectionStatus::Connected,
321            tools: vec![],
322        });
323        Ok(())
324    }
325
326    /// Start HTTP-based MCP server
327    async fn start_http(&mut self, _config: crate::types::McpHttpConfig) -> Result<(), AgentError> {
328        // HTTP support would require the HTTP client implementation
329        // For now, mark as running with placeholder connection
330        self.connection = Some(McpConnection {
331            name: self.name.clone(),
332            status: McpConnectionStatus::Connected,
333            tools: vec![],
334        });
335        Ok(())
336    }
337
338    /// Stop the MCP server
339    pub async fn stop(&mut self) -> Result<(), AgentError> {
340        if self.status == PluginMcpServerStatus::Stopped {
341            return Ok(());
342        }
343
344        // Drop the connection
345        if let Some(mut conn) = self.connection.take() {
346            conn.close().await;
347        }
348
349        // Kill the child process if any
350        if let Some(mut child) = self.child.take() {
351            let _ = child.kill().await;
352        }
353
354        self.status = PluginMcpServerStatus::Stopped;
355        Ok(())
356    }
357
358    /// Check if the server is running
359    pub fn is_running(&self) -> bool {
360        self.status == PluginMcpServerStatus::Running
361    }
362
363    /// Get the server status
364    pub fn get_status(&self) -> &PluginMcpServerStatus {
365        &self.status
366    }
367
368    /// Get the MCP connection
369    pub fn get_connection(&self) -> Option<&McpConnection> {
370        self.connection.as_ref()
371    }
372
373    /// Resolve environment variables in config
374    fn resolve_environment(&self, config: &McpServerConfig) -> McpServerConfig {
375        match config {
376            McpServerConfig::Stdio(stdio_config) => {
377                let mut resolved_env = std::env::vars().collect::<HashMap<_, _>>();
378
379                // Add plugin-specific env vars
380                resolved_env.insert("AI_PLUGIN_ROOT".to_string(), self.plugin_path.clone());
381
382                if let Some(custom_env) = &stdio_config.env {
383                    for (key, value) in custom_env {
384                        let resolved = self.substitute_variables(value);
385                        resolved_env.insert(key.clone(), resolved);
386                    }
387                }
388
389                McpServerConfig::Stdio(McpStdioConfig {
390                    transport_type: stdio_config.transport_type.clone(),
391                    command: self.substitute_variables(&stdio_config.command),
392                    args: stdio_config
393                        .args
394                        .as_ref()
395                        .map(|args| args.iter().map(|a| self.substitute_variables(a)).collect()),
396                    env: Some(resolved_env),
397                })
398            }
399            McpServerConfig::Sse(sse_config) => {
400                let resolved_url = self.substitute_variables(&sse_config.url);
401                let resolved_headers = sse_config.headers.as_ref().map(|headers| {
402                    headers
403                        .iter()
404                        .map(|(k, v)| (k.clone(), self.substitute_variables(v)))
405                        .collect()
406                });
407
408                McpServerConfig::Sse(McpSseConfig {
409                    transport_type: sse_config.transport_type.clone(),
410                    url: resolved_url,
411                    headers: resolved_headers,
412                })
413            }
414            McpServerConfig::Http(http_config) => {
415                let resolved_url = self.substitute_variables(&http_config.url);
416                let resolved_headers = http_config.headers.as_ref().map(|headers| {
417                    headers
418                        .iter()
419                        .map(|(k, v)| (k.clone(), self.substitute_variables(v)))
420                        .collect()
421                });
422
423                McpServerConfig::Http(crate::types::McpHttpConfig {
424                    transport_type: http_config.transport_type.clone(),
425                    url: resolved_url,
426                    headers: resolved_headers,
427                })
428            }
429        }
430    }
431
432    /// Substitute variables in a string
433    fn substitute_variables(&self, value: &str) -> String {
434        let mut result = value.to_string();
435
436        // Substitute AI_PLUGIN_ROOT
437        result = result.replace("${AI_PLUGIN_ROOT}", &self.plugin_path);
438        result = result.replace("$AI_PLUGIN_ROOT", &self.plugin_path);
439
440        // Substitute environment variables
441        for (key, val) in std::env::vars() {
442            let pattern = format!("${{{}}}", key);
443            let pattern_dollar = format!("${}", key);
444            result = result.replace(&pattern, &val);
445            result = result.replace(&pattern_dollar, &val);
446        }
447
448        result
449    }
450}
451
452/// Create a ToolDefinition from an MCP tool
453fn create_mcp_tool_definition(
454    server_name: &str,
455    mcp_tool: &crate::types::McpTool,
456) -> crate::types::ToolDefinition {
457    let tool_name = format!("mcp__{}__{}", server_name, mcp_tool.name);
458
459    let input_schema = mcp_tool.input_schema.clone().unwrap_or_else(|| {
460        serde_json::json!({
461            "type": "object",
462            "properties": {}
463        })
464    });
465
466    crate::types::ToolDefinition {
467        name: tool_name,
468        description: mcp_tool
469            .description
470            .clone()
471            .unwrap_or_else(|| format!("MCP tool: {}", mcp_tool.name)),
472        input_schema: crate::types::ToolInputSchema {
473            schema_type: input_schema
474                .get("type")
475                .and_then(|t| t.as_str())
476                .unwrap_or("object")
477                .to_string(),
478            properties: input_schema
479                .get("properties")
480                .cloned()
481                .unwrap_or(serde_json::json!({})),
482            required: input_schema
483                .get("required")
484                .and_then(|r| r.as_array())
485                .map(|arr| {
486                    arr.iter()
487                        .filter_map(|s| s.as_str().map(String::from))
488                        .collect()
489                }),
490        },
491        annotations: None,
492    }
493}
494
495/// Plugin MCP server manager
496pub struct PluginMcpServerManager {
497    /// Active servers
498    servers: RwLock<HashMap<String, Arc<RwLock<PluginMcpServer>>>>,
499}
500
501impl Default for PluginMcpServerManager {
502    fn default() -> Self {
503        Self::new()
504    }
505}
506
507impl PluginMcpServerManager {
508    /// Create a new manager
509    pub fn new() -> Self {
510        Self {
511            servers: RwLock::new(HashMap::new()),
512        }
513    }
514
515    /// Add a server to the manager
516    pub async fn add_server(&self, server: PluginMcpServer) {
517        let name = server.name.clone();
518        let server = Arc::new(RwLock::new(server));
519        self.servers.write().await.insert(name, server);
520    }
521
522    /// Get a server by name
523    pub async fn get_server(&self, name: &str) -> Option<Arc<RwLock<PluginMcpServer>>> {
524        self.servers.read().await.get(name).cloned()
525    }
526
527    /// Remove a server by name
528    pub async fn remove_server(&self, name: &str) {
529        if let Some(server) = self.servers.write().await.remove(name) {
530            let mut server = server.write().await;
531            let _ = server.stop().await;
532        }
533    }
534
535    /// Start a server by name
536    pub async fn start_server(&self, name: &str) -> Result<(), AgentError> {
537        if let Some(server) = self.servers.read().await.get(name) {
538            let mut server = server.write().await;
539            server.start().await
540        } else {
541            Err(AgentError::Mcp(format!("Server '{}' not found", name)))
542        }
543    }
544
545    /// Stop a server by name
546    pub async fn stop_server(&self, name: &str) -> Result<(), AgentError> {
547        if let Some(server) = self.servers.read().await.get(name) {
548            let mut server = server.write().await;
549            server.stop().await
550        } else {
551            Err(AgentError::Mcp(format!("Server '{}' not found", name)))
552        }
553    }
554
555    /// Start all servers
556    pub async fn start_all(&self) -> Vec<(String, Result<(), AgentError>)> {
557        let mut results = Vec::new();
558        let servers = self.servers.read().await;
559
560        for (name, server) in servers.iter() {
561            let mut server = server.write().await;
562            results.push((name.clone(), server.start().await));
563        }
564
565        results
566    }
567
568    /// Stop all servers
569    pub async fn stop_all(&self) {
570        let mut servers = self.servers.write().await;
571
572        for (_, server) in servers.iter() {
573            let mut server = server.write().await;
574            let _ = server.stop().await;
575        }
576
577        servers.clear();
578    }
579
580    /// Get all server names
581    pub async fn list_servers(&self) -> Vec<String> {
582        self.servers.read().await.keys().cloned().collect()
583    }
584
585    /// Get status of all servers
586    pub async fn get_all_status(&self) -> HashMap<String, PluginMcpServerStatus> {
587        let servers = self.servers.read().await;
588        let mut result = HashMap::new();
589
590        for (name, server) in servers.iter() {
591            let status = server.read().await.status.clone();
592            result.insert(name.clone(), status);
593        }
594
595        result
596    }
597}
598
599/// Load MCP server configs from a JSON file in the plugin directory
600pub async fn load_mcp_servers_from_file(
601    plugin_path: &str,
602    filename: &str,
603) -> Result<HashMap<String, PluginMcpServerConfig>, AgentError> {
604    let path = Path::new(plugin_path).join(filename);
605
606    if !path.exists() {
607        return Ok(HashMap::new());
608    }
609
610    let content = tokio::fs::read_to_string(&path).await.map_err(|e| {
611        AgentError::Io(std::io::Error::new(
612            std::io::ErrorKind::Other,
613            format!("Failed to read MCP config from {}: {}", path.display(), e),
614        ))
615    })?;
616
617    let parsed: serde_json::Value = serde_json::from_str(&content)
618        .map_err(|e| AgentError::Mcp(format!("Failed to parse MCP config: {}", e)))?;
619
620    // Support both { mcpServers: {...} } and direct {...} formats
621    let mcp_servers = if let Some(servers) = parsed.get("mcpServers") {
622        servers.clone()
623    } else {
624        parsed
625    };
626
627    let mut configs = HashMap::new();
628
629    if let Some(obj) = mcp_servers.as_object() {
630        for (name, config_val) in obj {
631            let config = parse_mcp_server_config(config_val);
632            if config.is_some() {
633                configs.insert(name.clone(), config.unwrap());
634            }
635        }
636    }
637
638    Ok(configs)
639}
640
641/// Parse a single MCP server config from JSON value
642fn parse_mcp_server_config(value: &serde_json::Value) -> Option<PluginMcpServerConfig> {
643    let obj = value.as_object()?;
644
645    // Determine transport type
646    let transport_type = obj
647        .get("type")
648        .and_then(|t| t.as_str())
649        .map(|t| match t {
650            "stdio" => PluginMcpTransport::Stdio,
651            "sse" => PluginMcpTransport::Sse,
652            "http" => PluginMcpTransport::Http,
653            _ => PluginMcpTransport::Unknown,
654        })
655        .unwrap_or(PluginMcpTransport::Stdio);
656
657    // Extract stdio fields
658    let command = obj
659        .get("command")
660        .and_then(|v| v.as_str())
661        .map(String::from);
662    let args = obj.get("args").and_then(|v| v.as_array()).map(|arr| {
663        arr.iter()
664            .filter_map(|s| s.as_str().map(String::from))
665            .collect()
666    });
667
668    let env = obj.get("env").and_then(|v| v.as_object()).map(|obj| {
669        obj.iter()
670            .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
671            .collect()
672    });
673
674    // Extract SSE/HTTP fields
675    let url = obj.get("url").and_then(|v| v.as_str()).map(String::from);
676    let headers = obj.get("headers").and_then(|v| v.as_object()).map(|obj| {
677        obj.iter()
678            .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
679            .collect()
680    });
681
682    Some(PluginMcpServerConfig {
683        transport_type: Some(transport_type),
684        command,
685        args,
686        env,
687        url,
688        headers,
689        scope: None,
690        plugin_source: None,
691    })
692}
693
694/// Load MCP servers from plugin manifest mcpServers field
695pub async fn load_plugin_mcp_servers(
696    plugin_path: &str,
697    mcp_servers_spec: &serde_json::Value,
698) -> Result<HashMap<String, PluginMcpServerConfig>, AgentError> {
699    let mut servers = HashMap::new();
700
701    match mcp_servers_spec {
702        // Single string - path to JSON file or MCPB file
703        serde_json::Value::String(path) => {
704            if path.ends_with(".mcpb") {
705                // MCPB files would need special handling - download and extract
706                // For now, skip MCPB files
707                eprintln!("MCPB file loading not yet implemented: {}", path);
708            } else {
709                // Path to JSON file
710                let loaded = load_mcp_servers_from_file(plugin_path, path).await?;
711                servers.extend(loaded);
712            }
713        }
714        // Array of paths or inline configs
715        serde_json::Value::Array(arr) => {
716            for spec in arr {
717                match spec {
718                    serde_json::Value::String(path) => {
719                        if path.ends_with(".mcpb") {
720                            eprintln!("MCPB file loading not yet implemented: {}", path);
721                        } else {
722                            let loaded = load_mcp_servers_from_file(plugin_path, path).await?;
723                            servers.extend(loaded);
724                        }
725                    }
726                    _ => {
727                        // Inline config
728                        if let Some(config) = parse_mcp_server_config(spec) {
729                            // Generate a name if not provided
730                            let name = format!("inline_{}", servers.len());
731                            servers.insert(name, config);
732                        }
733                    }
734                }
735            }
736        }
737        // Inline object config
738        serde_json::Value::Object(_) => {
739            if let Some(config) = parse_mcp_server_config(mcp_servers_spec) {
740                let name = format!("inline_{}", servers.len());
741                servers.insert(name, config);
742            }
743        }
744        _ => {}
745    }
746
747    Ok(servers)
748}
749
750/// Add plugin scope to MCP server configs (prefix server names)
751pub fn add_plugin_scope_to_servers(
752    servers: HashMap<String, PluginMcpServerConfig>,
753    plugin_name: &str,
754    plugin_source: &str,
755) -> HashMap<String, PluginMcpServerConfig> {
756    servers
757        .into_iter()
758        .map(|(name, mut config)| {
759            let scoped_name = format!("plugin:{}:{}", plugin_name, name);
760            config.plugin_source = Some(plugin_source.to_string());
761            (scoped_name, config)
762        })
763        .collect()
764}
765
766#[cfg(test)]
767mod tests {
768    use super::*;
769
770    #[test]
771    fn test_transport_type_parsing() {
772        let json = serde_json::json!({
773            "type": "stdio",
774            "command": "npx",
775            "args": ["-y", "some-server"]
776        });
777
778        let config = parse_mcp_server_config(&json).unwrap();
779        assert_eq!(config.transport_type, Some(PluginMcpTransport::Stdio));
780        assert_eq!(config.command, Some("npx".to_string()));
781    }
782
783    #[test]
784    fn test_sse_config_parsing() {
785        let json = serde_json::json!({
786            "type": "sse",
787            "url": "http://localhost:3000/sse"
788        });
789
790        let config = parse_mcp_server_config(&json).unwrap();
791        assert_eq!(config.transport_type, Some(PluginMcpTransport::Sse));
792        assert_eq!(config.url, Some("http://localhost:3000/sse".to_string()));
793    }
794
795    #[test]
796    fn test_server_status() {
797        let server = PluginMcpServer::new(
798            "test".to_string(),
799            PluginMcpServerConfig {
800                transport_type: Some(PluginMcpTransport::Stdio),
801                command: Some("echo".to_string()),
802                args: None,
803                env: None,
804                url: None,
805                headers: None,
806                scope: None,
807                plugin_source: None,
808            },
809            "/tmp/plugin".to_string(),
810            "test-plugin".to_string(),
811        );
812
813        assert_eq!(server.get_status(), &PluginMcpServerStatus::Stopped);
814        assert!(!server.is_running());
815    }
816
817    #[test]
818    fn test_manager() {
819        let manager = PluginMcpServerManager::new();
820
821        let server = PluginMcpServer::new(
822            "test".to_string(),
823            PluginMcpServerConfig {
824                transport_type: Some(PluginMcpTransport::Stdio),
825                command: Some("echo".to_string()),
826                args: None,
827                env: None,
828                url: None,
829                headers: None,
830                scope: None,
831                plugin_source: None,
832            },
833            "/tmp/plugin".to_string(),
834            "test-plugin".to_string(),
835        );
836
837        let runtime = tokio::runtime::Runtime::new().unwrap();
838        runtime.block_on(async {
839            manager.add_server(server).await;
840            let servers = manager.list_servers().await;
841            assert_eq!(servers.len(), 1);
842            assert!(servers.contains(&"test".to_string()));
843        });
844    }
845}