rust-mcp-server 0.3.8

An MCP server for Rust development
use std::collections::HashMap;
use std::process::Command;

use crate::{Tool, command::execute_command, serde_utils::deserialize_string};
use rmcp::{
    ErrorData,
    model::{AnnotateAble, Annotations, RawContent, Role},
};
use serde::Deserialize;

#[derive(Debug, ::serde::Deserialize, ::schemars::JsonSchema)]
#[schemars(title = "CargoWorkspaceInfoRequest")]
pub struct CargoWorkspaceInfoRequest {
    /// The toolchain to use, e.g., "stable" or "nightly".
    #[serde(default, deserialize_with = "deserialize_string")]
    toolchain: Option<String>,

    /// Path to Cargo.toml
    #[serde(default, deserialize_with = "deserialize_string")]
    manifest_path: Option<String>,

    /// Include direct dependencies (name and version) for each package
    #[serde(default)]
    include_dependencies: Option<bool>,
}

impl CargoWorkspaceInfoRequest {
    pub fn build_cmd(&self) -> Result<Command, ErrorData> {
        let mut cmd = Command::new("cargo");
        if let Some(toolchain) = &self.toolchain {
            cmd.arg(format!("+{toolchain}"));
        }
        cmd.arg("metadata");
        cmd.arg("--format-version").arg("1");
        cmd.arg("--no-deps");

        if let Some(manifest_path) = &self.manifest_path {
            cmd.arg("--manifest-path").arg(manifest_path);
        }

        Ok(cmd)
    }
}

pub struct CargoWorkspaceInfoRmcpTool;

impl Tool for CargoWorkspaceInfoRmcpTool {
    const NAME: &'static str = "workspace-info";
    const TITLE: &'static str = "workspace info";
    const DESCRIPTION: &'static str = "Get information about crates in the current workspace, including package names, target \
         types, manifest paths, descriptions, features, and optionally dependencies. This is a cut down version of cargo metadata with the goal of saving tokens.";
    type RequestArgs = CargoWorkspaceInfoRequest;

    fn call_rmcp_tool(&self, request: Self::RequestArgs) -> Result<crate::Response, ErrorData> {
        let cmd = request.build_cmd()?;
        let mut output = execute_command(cmd, Self::NAME)?;

        if !output.success() {
            return Ok(output.into());
        }

        let Some(stdout) = output.stdout.take() else {
            return Err(ErrorData::internal_error(
                "cargo metadata command produced no output".to_owned(),
                None,
            ));
        };

        let metadata: CargoMetadata = serde_json::from_str(&stdout.0).map_err(|e| {
            ErrorData::internal_error(format!("failed to parse cargo metadata JSON: {e}"), None)
        })?;

        let include_deps = request.include_dependencies.unwrap_or(false);
        let mut packages: Vec<PackageInfo> = vec![];

        for package in metadata.packages {
            let mut target_types = package
                .targets
                .iter()
                .flat_map(|t| &t.kind)
                .collect::<std::collections::HashSet<_>>()
                .into_iter()
                .map(String::from)
                .collect::<Vec<_>>();
            target_types.sort();

            let dependencies = include_deps.then_some(package.dependencies);

            packages.push(PackageInfo {
                name: package.name,
                description: package.description,
                manifest_path: package.manifest_path,
                target_types,
                features: package.features,
                dependencies,
            });
        }

        let mut response: crate::Response = output.into();
        let workspace_info = WorkspaceInfo { packages };
        let mut annotations = Annotations::default();
        annotations.audience = Some(vec![Role::User, Role::Assistant]);
        annotations.priority = Some(1.);

        let workspace_info = RawContent::json(workspace_info)?.annotate(annotations);

        response.add_content(workspace_info);
        Ok(response)
    }
}

// Structures for parsing cargo metadata output
#[derive(Debug, Deserialize)]
struct CargoMetadata {
    packages: Vec<Package>,
}

#[derive(Debug, Deserialize)]
struct Package {
    name: String,
    #[serde(default)]
    description: Option<String>,
    manifest_path: String,
    targets: Vec<Target>,
    #[serde(default)]
    features: HashMap<String, Vec<String>>,
    #[serde(default)]
    dependencies: Vec<Dependency>,
}

#[derive(Debug, Deserialize)]
struct Target {
    kind: Vec<String>,
}

#[derive(Debug, Deserialize, ::serde::Serialize)]
struct Dependency {
    name: String,
    req: String,
}

// Simplified output structures
#[derive(Debug, ::serde::Serialize)]
struct WorkspaceInfo {
    packages: Vec<PackageInfo>,
}

#[derive(Debug, ::serde::Serialize)]
struct PackageInfo {
    name: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    description: Option<String>,
    manifest_path: String,
    target_types: Vec<String>,
    #[serde(skip_serializing_if = "HashMap::is_empty")]
    features: HashMap<String, Vec<String>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    dependencies: Option<Vec<Dependency>>,
}