Skip to content

Implement MCPServer and MCPConfig models #95

@doomspork

Description

@doomspork

Context

MCP servers have two transport types: stdio (command + args + env) and HTTP (type + url + headers). MCPConfig wraps a map of named servers, representing .mcp.json.

Ported from: Fig/Sources/Models/MCPServer.swift, Fig/Sources/Models/MCPConfig.swift

What to implement

File paths

  • fig-core/src/models/mcp_server.rs
  • fig-core/src/models/mcp_config.rs

Rust struct definitions

// mcp_server.rs
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct MCPServer {
    // Stdio properties
    #[serde(skip_serializing_if = "Option::is_none")]
    pub command: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub args: Option<Vec<String>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub env: Option<HashMap<String, String>>,
    // HTTP properties
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(rename = "type")]
    pub server_type: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub url: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub headers: Option<HashMap<String, String>>,
    #[serde(flatten)]
    pub additional_properties: HashMap<String, Value>,
}

impl MCPServer {
    pub fn is_stdio(&self) -> bool {
        self.command.is_some() && self.server_type.as_deref() != Some("http")
    }
    pub fn is_http(&self) -> bool {
        self.server_type.as_deref() == Some("http") && self.url.is_some()
    }
    pub fn stdio(command: String, args: Option<Vec<String>>, env: Option<HashMap<String, String>>) -> Self {
        Self { command: Some(command), args, env, ..Default::default() }
    }
    pub fn http(url: String, headers: Option<HashMap<String, String>>) -> Self {
        Self { server_type: Some("http".to_string()), url: Some(url), headers, ..Default::default() }
    }
    /// Returns true if server has environment variables with potentially sensitive values
    pub fn has_sensitive_env(&self) -> bool {
        self.env.as_ref().map(|e| e.keys().any(|k| {
            let lower = k.to_lowercase();
            lower.contains("token") || lower.contains("key") || lower.contains("secret") || lower.contains("password")
        })).unwrap_or(false)
    }
}

// mcp_config.rs
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct MCPConfig {
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(rename = "mcpServers")]
    pub mcp_servers: Option<HashMap<String, MCPServer>>,
    #[serde(flatten)]
    pub additional_properties: HashMap<String, Value>,
}

impl MCPConfig {
    pub fn server_names(&self) -> Vec<String> {
        self.mcp_servers.as_ref()
            .map(|s| s.keys().cloned().collect())
            .unwrap_or_default()
    }
    pub fn server(&self, name: &str) -> Option<&MCPServer> {
        self.mcp_servers.as_ref()?.get(name)
    }
    pub fn server_count(&self) -> usize {
        self.mcp_servers.as_ref().map(|s| s.len()).unwrap_or(0)
    }
}

Acceptance criteria

  • Both stdio and HTTP server configs parse correctly
  • Factory methods MCPServer::stdio() and MCPServer::http() work
  • Round-trip tests pass with unknown fields preserved
  • is_stdio() and is_http() detect transport type correctly

Test requirements

  • test_stdio_server_round_trip — with command, args, env
  • test_http_server_round_trip — with type, url, headers
  • test_mcp_config_multiple_servers
  • test_mcp_server_type_detectionis_stdio(), is_http()
  • test_mcp_server_factory_methods
  • test_has_sensitive_env

Dependencies

Requires: #92

Blocks

#96, #99, #112, #121

Metadata

Metadata

Assignees

No one assigned

    Labels

    epic:config-parsingConfig file discovery, parsing, and data modelsepic:rust-migrationRust + Iced migration infrastructurephase:1Phase 1 — Read-only explorerpriority:highMust-have for MVPtype:migrationDirect port of existing Swift functionality

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions