use rust_mcp_sdk::{
macros::{mcp_tool, JsonSchema},
schema::{schema_utils::CallToolError, CallToolResult, TextContent},
};
use serde::{Deserialize, Serialize};
use std::path::Path;
use tokio::fs;
#[mcp_tool(
name = "update_blocked_by",
description = "Update the blocked_by relationship for a document",
idempotent_hint = false,
destructive_hint = false,
open_world_hint = false,
read_only_hint = false
)]
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct UpdateBlockedByTool {
pub project_path: String,
pub document_path: String,
pub blocked_by: Vec<String>,
}
impl UpdateBlockedByTool {
pub async fn call_tool(&self) -> std::result::Result<CallToolResult, CallToolError> {
let metis_dir = Path::new(&self.project_path);
if !metis_dir.exists() || !metis_dir.is_dir() {
return Err(CallToolError::new(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!(
"Metis workspace not found at {}. Run initialize_project first.",
metis_dir.display()
),
)));
}
let full_document_path = metis_dir.join(&self.document_path);
if !full_document_path.exists() {
return Err(CallToolError::new(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("Document not found at {}", full_document_path.display()),
)));
}
let content = fs::read_to_string(&full_document_path)
.await
.map_err(|e| CallToolError::new(e))?;
let updated_content = self.update_blocked_by_frontmatter(&content)?;
fs::write(&full_document_path, updated_content)
.await
.map_err(|e| CallToolError::new(e))?;
let response = serde_json::json!({
"success": true,
"document_path": self.document_path,
"blocked_by": self.blocked_by,
"message": format!(
"Successfully updated blocked_by relationships. Document is {} blocked by {} documents",
if self.blocked_by.is_empty() { "not" } else { "now" },
self.blocked_by.len()
)
});
Ok(CallToolResult::text_content(vec![TextContent::from(
serde_json::to_string_pretty(&response).map_err(CallToolError::new)?,
)]))
}
fn update_blocked_by_frontmatter(&self, content: &str) -> Result<String, CallToolError> {
let lines: Vec<&str> = content.lines().collect();
let mut result_lines = Vec::new();
let mut in_frontmatter = false;
let mut found_blocked_by = false;
let mut frontmatter_ended = false;
for (i, line) in lines.iter().enumerate() {
if i == 0 && line.trim() == "---" {
in_frontmatter = true;
result_lines.push(line.to_string());
continue;
}
if in_frontmatter && line.trim() == "---" {
if !found_blocked_by {
self.add_blocked_by_field(&mut result_lines);
}
result_lines.push(line.to_string());
in_frontmatter = false;
frontmatter_ended = true;
continue;
}
if in_frontmatter {
let trimmed = line.trim();
if trimmed.starts_with("blocked_by:") {
found_blocked_by = true;
self.add_blocked_by_field(&mut result_lines);
continue;
} else {
result_lines.push(line.to_string());
}
} else {
result_lines.push(line.to_string());
}
}
if !frontmatter_ended {
return Err(CallToolError::new(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"Document does not have valid YAML frontmatter",
)));
}
Ok(result_lines.join("\n"))
}
fn add_blocked_by_field(&self, result_lines: &mut Vec<String>) {
if self.blocked_by.is_empty() {
result_lines.push("blocked_by: []".to_string());
} else {
result_lines.push("blocked_by:".to_string());
for blocked_document in &self.blocked_by {
result_lines.push(format!(" - \"{}\"", blocked_document));
}
}
}
}