use rmcp::{
ErrorData as McpError,
handler::server::wrapper::Parameters,
model::{CallToolResult, ContentBlock},
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use super::helpers::*;
use crate::service::mcp_service::RudofMcpService;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct ManagePrefixesRequest {
pub action: Option<String>,
pub alias: Option<String>,
pub iri: Option<String>,
pub new_alias: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct PrefixEntry {
pub alias: String,
pub iri: String,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct ManagePrefixesResponse {
pub prefixes: Vec<PrefixEntry>,
}
pub async fn manage_prefixes_impl(
service: &RudofMcpService,
params: Parameters<ManagePrefixesRequest>,
) -> Result<CallToolResult, McpError> {
let Parameters(ManagePrefixesRequest {
action,
alias,
iri,
new_alias,
}) = params;
let action = action.unwrap_or_else(|| "list".to_string());
let mut rudof = service.rudof.lock().await;
match action.as_str() {
"list" => {},
"add" => {
let alias = match require(&alias, "alias", "add") {
Ok(v) => v,
Err(result) => return Ok(result),
};
let iri = match require(&iri, "iri", "add") {
Ok(v) => v,
Err(result) => return Ok(result),
};
if let Err(e) = rudof.add_prefix(alias, iri).execute() {
return Ok(ToolExecutionError::with_hint(
format!("Failed to add prefix '{alias}': {e}"),
"Provide a valid absolute IRI for 'iri'",
)
.into_call_tool_result());
}
},
"remove" => {
let alias = match require(&alias, "alias", "remove") {
Ok(v) => v,
Err(result) => return Ok(result),
};
if let Err(e) = rudof.remove_prefix(alias).execute() {
return Ok(ToolExecutionError::with_hint(
format!("Failed to remove prefix '{alias}': {e}"),
"List the current prefixes with action \"list\" to see valid aliases",
)
.into_call_tool_result());
}
},
"rename" => {
let old_alias = match require(&alias, "alias", "rename") {
Ok(v) => v,
Err(result) => return Ok(result),
};
let new_alias = match require(&new_alias, "new_alias", "rename") {
Ok(v) => v,
Err(result) => return Ok(result),
};
if let Err(e) = rudof.rename_prefix(old_alias, new_alias).execute() {
return Ok(ToolExecutionError::with_hint(
format!("Failed to rename prefix '{old_alias}' to '{new_alias}': {e}"),
"List the current prefixes with action \"list\" to see valid aliases",
)
.into_call_tool_result());
}
},
"copy" => {
let old_alias = match require(&alias, "alias", "copy") {
Ok(v) => v,
Err(result) => return Ok(result),
};
let new_alias = match require(&new_alias, "new_alias", "copy") {
Ok(v) => v,
Err(result) => return Ok(result),
};
if let Err(e) = rudof.copy_prefix(old_alias, new_alias).execute() {
return Ok(ToolExecutionError::with_hint(
format!("Failed to copy prefix '{old_alias}' to '{new_alias}': {e}"),
"List the current prefixes with action \"list\" to see valid aliases",
)
.into_call_tool_result());
}
},
other => {
return Ok(ToolExecutionError::with_hint(
format!("Unknown action '{other}'"),
"Valid actions: list, add, remove, rename, copy",
)
.into_call_tool_result());
},
}
let prefix_map = rudof.prefixes().execute();
drop(rudof);
let mut prefixes: Vec<PrefixEntry> = prefix_map
.iter()
.map(|(alias, iri)| PrefixEntry {
alias: alias.clone(),
iri: iri.as_str().to_string(),
})
.collect();
prefixes.sort_by(|a, b| a.alias.cmp(&b.alias));
let response = ManagePrefixesResponse {
prefixes: prefixes.clone(),
};
let structured = serialize_structured(&response, "manage_prefixes_impl")?;
let summary = if prefixes.is_empty() {
"No default prefixes are defined.".to_string()
} else {
prefixes
.iter()
.map(|p| format!("{}: <{}>", p.alias, p.iri))
.collect::<Vec<_>>()
.join("\n")
};
let mut result = CallToolResult::success(vec![ContentBlock::text(summary)]);
result.structured_content = Some(structured);
Ok(result)
}
fn require<'a>(value: &'a Option<String>, field: &str, action: &str) -> Result<&'a str, CallToolResult> {
value.as_deref().ok_or_else(|| {
ToolExecutionError::with_hint(
format!("Missing required field '{field}' for action '{action}'"),
format!("Provide '{field}' when action is '{action}'"),
)
.into_call_tool_result()
})
}
#[cfg(test)]
mod tests {
use super::*;
fn request(
action: &str,
alias: Option<&str>,
iri: Option<&str>,
new_alias: Option<&str>,
) -> Parameters<ManagePrefixesRequest> {
Parameters(ManagePrefixesRequest {
action: Some(action.to_string()),
alias: alias.map(String::from),
iri: iri.map(String::from),
new_alias: new_alias.map(String::from),
})
}
#[tokio::test]
async fn list_on_fresh_session_is_empty() {
let service = RudofMcpService::new();
let result = manage_prefixes_impl(&service, request("list", None, None, None))
.await
.expect("list should not be a protocol error");
assert_ne!(result.is_error, Some(true));
let structured = result
.structured_content
.expect("response should have structured content");
let prefixes = structured.get("prefixes").and_then(|v| v.as_array()).unwrap();
assert!(prefixes.is_empty());
}
#[tokio::test]
async fn add_then_list_shows_the_new_prefix() {
let service = RudofMcpService::new();
let add_result = manage_prefixes_impl(
&service,
request("add", Some("wd"), Some("http://www.wikidata.org/entity/"), None),
)
.await
.expect("add should not be a protocol error");
assert_ne!(add_result.is_error, Some(true));
let structured = add_result
.structured_content
.expect("response should have structured content");
let prefixes = structured.get("prefixes").and_then(|v| v.as_array()).unwrap();
assert_eq!(prefixes.len(), 1);
assert_eq!(prefixes[0]["alias"], "wd");
assert_eq!(prefixes[0]["iri"], "http://www.wikidata.org/entity/");
}
#[tokio::test]
async fn add_missing_iri_is_a_tool_error() {
let service = RudofMcpService::new();
let result = manage_prefixes_impl(&service, request("add", Some("wd"), None, None))
.await
.expect("missing field should be a tool error, not a protocol error");
assert_eq!(result.is_error, Some(true));
}
#[tokio::test]
async fn add_invalid_iri_is_a_tool_error() {
let service = RudofMcpService::new();
let result = manage_prefixes_impl(&service, request("add", Some("wd"), Some("not an iri"), None))
.await
.expect("invalid IRI should be a tool error, not a protocol error");
assert_eq!(result.is_error, Some(true));
}
#[tokio::test]
async fn remove_unknown_alias_is_a_tool_error() {
let service = RudofMcpService::new();
let result = manage_prefixes_impl(&service, request("remove", Some("nope"), None, None))
.await
.expect("removing an unknown alias should be a tool error, not a protocol error");
assert_eq!(result.is_error, Some(true));
}
#[tokio::test]
async fn rename_then_list_reflects_the_new_alias() {
let service = RudofMcpService::new();
manage_prefixes_impl(
&service,
request("add", Some("wd"), Some("http://www.wikidata.org/entity/"), None),
)
.await
.unwrap();
let result = manage_prefixes_impl(&service, request("rename", Some("wd"), None, Some("wikidata")))
.await
.expect("rename should not be a protocol error");
assert_ne!(result.is_error, Some(true));
let structured = result
.structured_content
.expect("response should have structured content");
let prefixes = structured.get("prefixes").and_then(|v| v.as_array()).unwrap();
assert_eq!(prefixes.len(), 1);
assert_eq!(prefixes[0]["alias"], "wikidata");
}
#[tokio::test]
async fn copy_then_list_has_both_aliases() {
let service = RudofMcpService::new();
manage_prefixes_impl(
&service,
request("add", Some("wd"), Some("http://www.wikidata.org/entity/"), None),
)
.await
.unwrap();
let result = manage_prefixes_impl(&service, request("copy", Some("wd"), None, Some("wikidata")))
.await
.expect("copy should not be a protocol error");
assert_ne!(result.is_error, Some(true));
let structured = result
.structured_content
.expect("response should have structured content");
let prefixes = structured.get("prefixes").and_then(|v| v.as_array()).unwrap();
assert_eq!(prefixes.len(), 2);
}
#[tokio::test]
async fn unknown_action_is_a_tool_error() {
let service = RudofMcpService::new();
let result = manage_prefixes_impl(&service, request("frobnicate", None, None, None))
.await
.expect("unknown action should be a tool error, not a protocol error");
assert_eq!(result.is_error, Some(true));
}
}