mycop 0.3.1

AI Code Security Scanner — detect vulnerabilities in AI-generated code
Documentation
pub mod convert;
pub mod tools;
pub mod types;

use rmcp::{
    handler::server::ServerHandler,
    model::*,
    service::{RequestContext, RoleServer},
    Error as McpError, ServiceExt,
};

use crate::config::ScanConfig;
use crate::rules::RuleRegistry;

use self::convert::rule_to_output;

use rmcp::model::AnnotateAble;
use std::future::Future;

#[derive(Clone)]
pub struct MycopMcpServer;

impl ServerHandler for MycopMcpServer {
    fn get_info(&self) -> ServerInfo {
        ServerInfo {
            protocol_version: ProtocolVersion::V_2024_11_05,
            capabilities: ServerCapabilities::builder()
                .enable_tools()
                .enable_resources()
                .build(),
            server_info: Implementation {
                name: "mycop".to_string(),
                version: env!("CARGO_PKG_VERSION").to_string(),
            },
            instructions: Some(
                "mycop is an AI code security scanner with 100 built-in rules \
                 covering OWASP Top 10 and CWE Top 25. Use 'scan' to check files \
                 for vulnerabilities, 'list_rules' to browse security rules, \
                 'explain_finding' for detailed vulnerability info, 'review' for \
                 deep AI security analysis, and 'check_deps' to detect hallucinated \
                 packages. To fix vulnerabilities, read the scan findings and apply \
                 the fixes yourself using the fix_hint provided in each finding."
                    .to_string(),
            ),
        }
    }

    fn list_tools(
        &self,
        _request: PaginatedRequestParam,
        _context: RequestContext<RoleServer>,
    ) -> impl Future<Output = Result<ListToolsResult, McpError>> + Send + '_ {
        std::future::ready(Ok(ListToolsResult {
            tools: tools::tool_box().list(),
            next_cursor: None,
        }))
    }

    #[allow(clippy::manual_async_fn)]
    fn call_tool(
        &self,
        request: CallToolRequestParam,
        context: RequestContext<RoleServer>,
    ) -> impl Future<Output = Result<CallToolResult, McpError>> + Send + '_ {
        async move {
            let tcc = rmcp::handler::server::tool::ToolCallContext::new(self, request, context);
            tools::tool_box().call(tcc).await
        }
    }

    fn list_resources(
        &self,
        _request: PaginatedRequestParam,
        _context: RequestContext<RoleServer>,
    ) -> impl Future<Output = Result<ListResourcesResult, McpError>> + Send + '_ {
        std::future::ready(Ok(ListResourcesResult {
            resources: vec![
                RawResource {
                    uri: "mycop://rules/catalog".to_string(),
                    name: "Security Rules Catalog".to_string(),
                    description: Some(
                        "Complete catalog of all built-in security rules as JSON".to_string(),
                    ),
                    mime_type: Some("application/json".to_string()),
                    size: None,
                }
                .no_annotation(),
                RawResource {
                    uri: "mycop://config/schema".to_string(),
                    name: "Configuration Schema".to_string(),
                    description: Some("Default .scanrc.yml configuration template".to_string()),
                    mime_type: Some("text/yaml".to_string()),
                    size: None,
                }
                .no_annotation(),
            ],
            next_cursor: None,
        }))
    }

    fn read_resource(
        &self,
        request: ReadResourceRequestParam,
        _context: RequestContext<RoleServer>,
    ) -> impl Future<Output = Result<ReadResourceResult, McpError>> + Send + '_ {
        std::future::ready(read_resource_sync(&request.uri))
    }
}

fn read_resource_sync(uri: &str) -> Result<ReadResourceResult, McpError> {
    match uri {
        "mycop://rules/catalog" => {
            let registry = RuleRegistry::load_default().map_err(|e| {
                McpError::internal_error(format!("Failed to load rules: {}", e), None)
            })?;
            let rules: Vec<_> = registry
                .all_rules()
                .iter()
                .map(|r| rule_to_output(r))
                .collect();
            let json = serde_json::to_string_pretty(&rules).map_err(|e| {
                McpError::internal_error(format!("Serialization error: {}", e), None)
            })?;

            Ok(ReadResourceResult {
                contents: vec![ResourceContents::text(json, uri)],
            })
        }
        "mycop://config/schema" => {
            let content = ScanConfig::default_content();
            Ok(ReadResourceResult {
                contents: vec![ResourceContents::text(content, uri)],
            })
        }
        _ => Err(McpError::invalid_params(
            format!("Unknown resource: {}", uri),
            None,
        )),
    }
}

impl MycopMcpServer {
    pub async fn run() -> anyhow::Result<()> {
        eprintln!("mycop MCP server starting...");

        let server = Self;
        let transport = rmcp::transport::io::stdio();
        let service = server.serve(transport).await?;

        eprintln!(
            "mycop MCP server v{} running on stdio",
            env!("CARGO_PKG_VERSION")
        );

        service.waiting().await?;
        Ok(())
    }
}