rust-mcp-server 0.3.8

An MCP server for Rust development
use rmcp::model::Annotated;

use crate::command::{AgentRecommendation, Output};

pub(crate) struct Response {
    output: Output,
    additional_content: Vec<Annotated<rmcp::model::RawContent>>,
    recommendations: Vec<AgentRecommendation>,
}

impl Response {
    pub(crate) fn add_content(&mut self, content: Annotated<rmcp::model::RawContent>) {
        self.additional_content.push(content);
    }

    pub(crate) fn add_recommendation(&mut self, recommendation: impl Into<String>) {
        self.recommendations
            .push(AgentRecommendation(recommendation.into()));
    }

    pub(crate) fn into_rmcp_result(
        self,
        ignore_recommendations: bool,
    ) -> rmcp::model::CallToolResult {
        let mut result: rmcp::model::CallToolResult = self.output.into();
        result.content.extend(self.additional_content);
        if !ignore_recommendations {
            result
                .content
                .extend(self.recommendations.into_iter().map(Into::into));
        }
        result
    }
}

impl From<Output> for Response {
    fn from(val: Output) -> Self {
        Response {
            output: val,
            additional_content: Vec::new(),
            recommendations: Vec::new(),
        }
    }
}

#[cfg(test)]
mod tests {
    use rmcp::model::{AnnotateAble, Annotations, RawContent};

    use crate::command::{CommandLine, ExitStatus, Stdout};

    use super::*;

    #[test]
    fn only_output() {
        let output = Output {
            tool_name: "test_tool".into(),
            stdout: Some(Stdout("This is a test output".into())),
            stderr: None,
            cmd_line: CommandLine("test_command --option".into()),
            exit_status: ExitStatus(std::process::ExitStatus::default()),
        };
        let response: Response = output.into();
        let rmcp_result = response.into_rmcp_result(false);

        let [cmd_line, stdout, exit_status] = &rmcp_result.content[..] else {
            panic!("expected 3 content items: {rmcp_result:?}");
        };

        assert_eq!(
            cmd_line.as_text().unwrap().text,
            "Executed command: `test_command --option`"
        );
        assert_eq!(stdout.as_text().unwrap().text, "This is a test output");
        assert_eq!(exit_status.as_text().unwrap().text, "✅ test_tool: Success");
    }

    #[test]
    fn with_additional_content_and_recommendations() {
        let output = Output {
            tool_name: "test_tool".into(),
            stdout: Some(Stdout("This is a test output".into())),
            stderr: None,
            cmd_line: CommandLine("test_command --option".into()),
            exit_status: ExitStatus(std::process::ExitStatus::default()),
        };
        let mut response: Response = output.into();
        response
            .add_content(RawContent::text("additional content").annotate(Annotations::default()));
        response.add_recommendation("Consider checking the logs.");

        let rmcp_result = response.into_rmcp_result(false);

        let [
            cmd_line,
            stdout,
            exit_status,
            additional_content,
            recommendation,
        ] = &rmcp_result.content[..]
        else {
            panic!("expected 5 content items: {rmcp_result:?}");
        };

        assert_eq!(
            cmd_line.as_text().unwrap().text,
            "Executed command: `test_command --option`"
        );
        assert_eq!(stdout.as_text().unwrap().text, "This is a test output");
        assert_eq!(exit_status.as_text().unwrap().text, "✅ test_tool: Success");
        assert_eq!(
            additional_content.as_text().unwrap().text,
            "additional content"
        );
        assert_eq!(
            recommendation.as_text().unwrap().text,
            "RECOMMENDATION: Consider checking the logs."
        );
    }

    #[test]
    fn ignore_recommendations() {
        let output = Output {
            tool_name: "test_tool".into(),
            stdout: Some(Stdout("This is a test output".into())),
            stderr: None,
            cmd_line: CommandLine("test_command --option".into()),
            exit_status: ExitStatus(std::process::ExitStatus::default()),
        };
        let mut response: Response = output.into();
        response.add_recommendation("Consider checking the logs.");

        let rmcp_result = response.into_rmcp_result(true);

        let [cmd_line, stdout, exit_status] = &rmcp_result.content[..] else {
            panic!("expected 3 content items: {rmcp_result:?}");
        };

        assert_eq!(
            cmd_line.as_text().unwrap().text,
            "Executed command: `test_command --option`"
        );
        assert_eq!(stdout.as_text().unwrap().text, "This is a test output");
        assert_eq!(exit_status.as_text().unwrap().text, "✅ test_tool: Success");
    }
}