aether-mcp-servers 0.6.2

Built-in MCP tool servers for the Aether AI agent framework
Documentation
use crate::workspace_paths::resolve_path;
use mcp_utils::server::mrtr::{ELICITATION_UNSUPPORTED, input_requests_supported, parse_response};
use rmcp::{
    ErrorData as McpError, RoleServer,
    handler::server::{
        tool::{InputResponses, IntoCallToolResult},
        wrapper::Json,
    },
    model::{
        CallToolResponse, CallToolResult, ContentBlock, ElicitRequest, ElicitRequestParams, ElicitResult,
        ElicitationAction, ElicitationSchema, EnumSchema, InputRequest, InputRequests, InputRequiredResult,
        RequestMetaObject,
    },
    service::RequestContext,
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
pub use utils::artifact_review::ArtifactFormat;
use utils::artifact_review::{ArtifactReviewDecision, ArtifactReviewElicitationMeta, ArtifactReviewSubmission};

const REVIEW: &str = "review";

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct ReviewArtifactInput {
    pub source: ReviewArtifactSource,
    pub title: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "type", rename_all = "lowercase", deny_unknown_fields)]
pub enum ReviewArtifactSource {
    File { path: PathBuf },
    Content { content: String },
}

#[derive(Debug, Clone, Serialize, JsonSchema, PartialEq, Eq)]
#[serde(tag = "status", rename_all = "lowercase")]
pub enum ReviewArtifactOutput {
    Approved,
    Feedback { feedback: String },
    Cancelled,
    Declined,
}

impl TryFrom<ElicitResult> for ReviewArtifactOutput {
    type Error = McpError;

    fn try_from(result: ElicitResult) -> Result<Self, Self::Error> {
        match result.action {
            ElicitationAction::Cancel => Ok(Self::Cancelled),
            ElicitationAction::Decline => Ok(Self::Declined),
            ElicitationAction::Accept => {
                let content = result
                    .content
                    .ok_or_else(|| McpError::invalid_params("accepted review response is missing content", None))?;
                let submission: ArtifactReviewSubmission = serde_json::from_value(content).map_err(|error| {
                    McpError::invalid_params(format!("invalid accepted review response: {error}"), None)
                })?;

                Ok(match submission {
                    ArtifactReviewSubmission::Approved => Self::Approved,
                    ArtifactReviewSubmission::Feedback { feedback } => Self::Feedback { feedback },
                })
            }
            _ => Err(McpError::invalid_params("unknown elicitation action", None)),
        }
    }
}

pub async fn execute_review_artifact(
    root_dir: &Path,
    input: ReviewArtifactInput,
    responses: InputResponses,
    context: &RequestContext<RoleServer>,
) -> Result<CallToolResponse, McpError> {
    if let Some(responses) = responses.0 {
        let result: ElicitResult = parse_response(&responses, REVIEW)?;
        return Json(ReviewArtifactOutput::try_from(result)?).into_call_tool_result();
    }

    let (meta, subject) = match input.source {
        ReviewArtifactSource::File { path } => {
            let path = resolve_path(root_dir, path);
            let markdown = match read_file(&path).await {
                Ok(markdown) => markdown,
                Err(error) => return Ok(CallToolResult::error(vec![ContentBlock::text(error)]).into()),
            };

            let subject = input.title.clone().unwrap_or_else(|| path.display().to_string());
            let meta = ArtifactReviewElicitationMeta::new(&path, &markdown, ArtifactFormat::Markdown)
                .with_title(input.title.unwrap_or_else(|| format!("Review {}", path.display())));
            (meta, subject)
        }

        ReviewArtifactSource::Content { content: inline_markdown } => {
            let (title, subject) = input
                .title
                .map_or_else(|| ("Review".to_string(), "this content".to_string()), |title| (title.clone(), title));
            (ArtifactReviewElicitationMeta::inline(&title, &inline_markdown, ArtifactFormat::Markdown), subject)
        }
    };

    let params = build_elicitation_form(&meta, &subject)?;
    let requests = InputRequests::from([(REVIEW.to_string(), InputRequest::Elicitation(ElicitRequest::new(params)))]);

    if !input_requests_supported(context.client_capabilities().as_ref(), &requests) {
        return Ok(CallToolResult::error(vec![ContentBlock::text(ELICITATION_UNSUPPORTED)]).into());
    }

    Ok(InputRequiredResult::from_input_requests(requests).into())
}

async fn read_file(path: &Path) -> Result<String, String> {
    let metadata = tokio::fs::metadata(path)
        .await
        .map_err(|error| format!("Failed to read artifact {}: {error}", path.display()))?;

    if !metadata.is_file() {
        return Err(format!("Artifact is not a regular file: {}", path.display()));
    }

    tokio::fs::read_to_string(path)
        .await
        .map_err(|error| format!("Failed to read UTF-8 artifact {}: {error}", path.display()))
}

fn build_elicitation_form(
    artifact: &ArtifactReviewElicitationMeta,
    subject: &str,
) -> Result<ElicitRequestParams, McpError> {
    let meta = artifact.to_json().map(RequestMetaObject::from).map_err(|error| {
        McpError::internal_error(format!("failed to serialize artifact review metadata: {error}"), None)
    })?;

    let requested_schema = ElicitationSchema::builder()
        .required_enum_schema(
            "decision",
            EnumSchema::builder(ArtifactReviewDecision::ALL.map(|decision| decision.to_string()).to_vec()).build(),
        )
        .optional_string("feedback")
        .build()
        .map_err(|error| McpError::internal_error(format!("failed to build schema: {error}"), None))?;

    Ok(ElicitRequestParams::FormElicitationParams {
        meta: Some(meta),
        message: format!("Review {subject} and approve or submit feedback."),
        requested_schema,
    })
}