Skip to content

Implement MCPServerFormData and validation for server editor #99

@doomspork

Description

@doomspork

Context

Form data types and validation logic for the MCP server add/edit form. Validates server name, transport-specific fields, and detects sensitive environment variables.

Ported from: Fig/Sources/Models/MCPServerFormData.swift

What to implement

File path

  • fig-core/src/models/mcp_form_data.rs

Rust definitions

use std::collections::HashMap;
use crate::models::mcp_server::MCPServer;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MCPServerType {
    Stdio,
    Http,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MCPServerScope {
    Global,
    Project,
}

#[derive(Debug, Clone, PartialEq)]
pub struct KeyValuePair {
    pub id: uuid::Uuid,
    pub key: String,
    pub value: String,
}

impl KeyValuePair {
    pub fn new(key: String, value: String) -> Self {
        Self { id: uuid::Uuid::new_v4(), key, value }
    }
}

#[derive(Debug, Clone, PartialEq)]
pub struct MCPValidationError {
    pub field: String,
    pub message: String,
}

#[derive(Debug, Clone, PartialEq)]
pub struct MCPServerFormData {
    pub name: String,
    pub server_type: MCPServerType,
    pub scope: MCPServerScope,
    // Stdio fields
    pub command: String,
    pub args: Vec<String>,
    pub env_vars: Vec<KeyValuePair>,
    // HTTP fields
    pub url: String,
    pub headers: Vec<KeyValuePair>,
    // Editing state
    pub original_name: Option<String>,  // None when adding, Some when editing
}

impl MCPServerFormData {
    pub fn new_for_adding(scope: MCPServerScope) -> Self { ... }
    pub fn from_server(name: &str, server: &MCPServer, scope: MCPServerScope) -> Self { ... }

    /// Validate the form data. Returns empty vec if valid.
    pub fn validate(&self, existing_names: &[String]) -> Vec<MCPValidationError> {
        let mut errors = Vec::new();

        // Name: required, alphanumeric + hyphens + underscores
        if self.name.trim().is_empty() {
            errors.push(MCPValidationError { field: "name".into(), message: "Server name is required".into() });
        } else if !self.name.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_') {
            errors.push(MCPValidationError { field: "name".into(), message: "Name must contain only letters, numbers, hyphens, and underscores".into() });
        }

        // Duplicate name check (skip if editing same name)
        if self.original_name.as_deref() != Some(&self.name) && existing_names.contains(&self.name) {
            errors.push(MCPValidationError { field: "name".into(), message: "A server with this name already exists".into() });
        }

        match self.server_type {
            MCPServerType::Stdio => {
                if self.command.trim().is_empty() {
                    errors.push(MCPValidationError { field: "command".into(), message: "Command is required for stdio servers".into() });
                }
            }
            MCPServerType::Http => {
                if self.url.trim().is_empty() {
                    errors.push(MCPValidationError { field: "url".into(), message: "URL is required for HTTP servers".into() });
                } else if !self.url.starts_with("http://") && !self.url.starts_with("https://") {
                    errors.push(MCPValidationError { field: "url".into(), message: "URL must start with http:// or https://".into() });
                }
            }
        }

        errors
    }

    /// Convert form data to MCPServer model.
    pub fn to_mcp_server(&self) -> MCPServer { ... }

    /// Detect sensitive environment variables.
    pub fn sensitive_env_warnings(&self) -> Vec<String> { ... }
}

Sensitive patterns to detect in env var keys: token, key, secret, password, credential, auth.

Acceptance criteria

  • Validation catches empty names, invalid characters, duplicate names
  • Validation catches missing command (stdio) or invalid URL (HTTP)
  • to_mcp_server() correctly converts for both transport types
  • from_server() populates form from existing server
  • Sensitive env var detection works

Test requirements

  • test_validate_empty_name
  • test_validate_invalid_name_chars
  • test_validate_duplicate_name
  • test_validate_stdio_missing_command
  • test_validate_http_invalid_url
  • test_validate_http_missing_url
  • test_to_mcp_server_stdio
  • test_to_mcp_server_http
  • test_from_server_round_trip
  • test_sensitive_env_warnings
  • test_editing_same_name_no_duplicate_error

Dependencies

Requires: #89

Blocks

#107

Metadata

Metadata

Assignees

No one assigned

    Labels

    epic:mcp-managementMCP server viewing, editing, copyingepic:rust-migrationRust + Iced migration infrastructurephase:4Phase 4 — Advanced featurespriority: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