use rmcp::model::{CallToolResult, Content};
use serde_json::{Map, Value};
use crate::operations::{OperationError, OperationOutcome};
use crate::validation::ValidationFailure;
pub fn operation_result(
output: Result<OperationOutcome, OperationError>,
) -> Result<CallToolResult, rmcp::ErrorData> {
match output {
Ok(outcome) => Ok(outcome_to_result(outcome, false)),
Err(error) => Ok(tool_error(error.to_string())),
}
}
pub fn validation_result(
output: Result<OperationOutcome, OperationError>,
) -> Result<CallToolResult, rmcp::ErrorData> {
match output {
Ok(outcome) => Ok(outcome_to_result(outcome, false)),
Err(OperationError::Invalid(failure)) => Ok(validation_failure_result(failure)),
Err(error) => Ok(tool_error(error.to_string())),
}
}
fn outcome_to_result(outcome: OperationOutcome, is_error: bool) -> CallToolResult {
text_and_structured(outcome.text, ensure_object(outcome.structured), is_error)
}
fn validation_failure_result(failure: ValidationFailure) -> CallToolResult {
let diagnostics: Vec<Value> = failure
.diagnostics
.iter()
.map(serde_json::to_value)
.collect::<serde_json::Result<Vec<_>>>()
.expect("Diagnostic derives Serialize; to_value cannot fail");
let mut structured = Map::new();
structured.insert("valid".to_string(), Value::Bool(false));
structured.insert(
"change_id".to_string(),
Value::String(failure.change_id.clone()),
);
structured.insert("diagnostics".to_string(), Value::Array(diagnostics));
text_and_structured(failure.to_string(), structured, true)
}
fn text_and_structured(
text: String,
structured: Map<String, Value>,
is_error: bool,
) -> CallToolResult {
let mut result = CallToolResult::success(vec![Content::text(text)]);
result.structured_content = Some(Value::Object(structured));
if is_error {
result.is_error = Some(true);
}
result
}
fn tool_error(message: String) -> CallToolResult {
CallToolResult::error(vec![Content::text(message)])
}
fn ensure_object(value: Value) -> Map<String, Value> {
match value {
Value::Object(map) => map,
other => {
let mut map = Map::new();
map.insert("value".to_string(), other);
map
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::validation::Diagnostic;
use serde_json::json;
#[test]
fn outcome_with_object_payload_carries_structured_content() {
let outcome = OperationOutcome::new(
"created change add-foo",
json!({
"change_id": "add-foo",
"schema": "nbspec-default",
"folder": "proposals/add-foo",
"notebook": "nbspec",
}),
);
let result = outcome_to_result(outcome, false);
assert_eq!(result.is_error, Some(false));
let structured = result.structured_content.expect("structured payload");
assert_eq!(structured["change_id"], json!("add-foo"));
assert_eq!(structured["schema"], json!("nbspec-default"));
}
#[test]
fn outcome_with_non_object_payload_is_wrapped() {
let outcome = OperationOutcome::new("count: 3", json!(3));
let result = outcome_to_result(outcome, false);
let structured = result.structured_content.expect("structured payload");
assert_eq!(structured, json!({ "value": 3 }));
}
#[test]
fn validation_failure_carries_diagnostics_serialized_from_diagnostic() {
let failure = ValidationFailure {
change_id: "add-foo".to_string(),
diagnostics: vec![
Diagnostic {
note: "proposals/add-foo/proposal.md".to_string(),
artifact_id: "proposal".to_string(),
line: Some(3),
message: "missing H1".to_string(),
},
Diagnostic {
note: "proposals/add-foo/specifications/x.md".to_string(),
artifact_id: "specifications".to_string(),
line: None,
message: "required artifact has no authored content".to_string(),
},
],
};
let result = validation_failure_result(failure);
assert_eq!(result.is_error, Some(true));
let structured = result.structured_content.expect("structured payload");
assert_eq!(structured.get("valid"), Some(&json!(false)));
assert_eq!(structured.get("change_id"), Some(&json!("add-foo")));
let diagnostics = structured
.get("diagnostics")
.and_then(|v| v.as_array())
.expect("diagnostics array");
assert_eq!(diagnostics.len(), 2);
assert_eq!(
diagnostics[0],
json!({
"note": "proposals/add-foo/proposal.md",
"artifact_id": "proposal",
"line": 3,
"message": "missing H1",
})
);
assert_eq!(
diagnostics[1],
json!({
"note": "proposals/add-foo/specifications/x.md",
"artifact_id": "specifications",
"line": null,
"message": "required artifact has no authored content",
})
);
}
#[test]
fn validation_failure_text_matches_failure_display() {
let failure = ValidationFailure {
change_id: "add-foo".to_string(),
diagnostics: vec![Diagnostic {
note: "proposals/add-foo/proposal.md".to_string(),
artifact_id: "proposal".to_string(),
line: Some(3),
message: "missing H1".to_string(),
}],
};
let result = validation_failure_result(failure);
let text = result
.content
.first()
.and_then(|c| c.as_text())
.map(|t| t.text.as_str())
.expect("text content");
assert!(text.starts_with("change add-foo is invalid: 1 violation"));
assert!(text.contains("proposals/add-foo/proposal.md:3: [proposal] missing H1"));
}
}