use std::path::PathBuf;
use rmcp::handler::server::wrapper::Parameters;
use rmcp::transport::stdio;
use rmcp::{ServerHandler, ServiceExt, schemars, tool, tool_handler, tool_router};
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
struct ValidateParams {
file: PathBuf,
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
struct ResolveParams {
id: String,
ontologies_dir: PathBuf,
}
#[derive(Clone)]
struct Mif;
#[allow(clippy::unused_self)]
#[tool_router]
impl Mif {
#[tool(description = "Validate a MIF document against the canonical MIF JSON Schema")]
fn validate_mif_document(
&self,
Parameters(ValidateParams { file }): Parameters<ValidateParams>,
) -> String {
let contents = match std::fs::read_to_string(&file) {
Ok(contents) => contents,
Err(source) => return format!("failed to read {}: {source}", file.display()),
};
let instance: serde_json::Value = match serde_json::from_str(&contents) {
Ok(instance) => instance,
Err(source) => return format!("failed to parse {} as JSON: {source}", file.display()),
};
match mif_schema::validate_document(&instance) {
Ok(()) => format!("{}: valid", file.display()),
Err(error) => {
let messages = error.messages().join("; ");
format!("{}: invalid ({messages})", file.display())
},
}
}
#[tool(description = "Resolve an ontology's three-tier extends chain")]
fn resolve_ontology_reference(
&self,
Parameters(ResolveParams { id, ontologies_dir }): Parameters<ResolveParams>,
) -> String {
let corpus = match mif_ontology::load_corpus_from_dir(&ontologies_dir) {
Ok(corpus) => corpus,
Err(error) => return error.to_string(),
};
match mif_ontology::resolve_chain(&id, &corpus) {
Ok(chain) => chain
.iter()
.map(|ontology| format!("{} ({})", ontology.id, ontology.version))
.collect::<Vec<_>>()
.join(" -> "),
Err(error) => error.to_string(),
}
}
}
#[tool_handler(
name = "mif-mcp",
version = "0.1.0",
instructions = "Validate MIF documents and resolve MIF ontology references"
)]
impl ServerHandler for Mif {}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let service = Mif.serve(stdio()).await?;
service.waiting().await?;
Ok(())
}
#[cfg(test)]
mod tests {
use std::fs;
use super::{Mif, Parameters, ResolveParams, ValidateParams};
fn write_temp_file(contents: &str) -> tempfile::NamedTempFile {
let file = tempfile::NamedTempFile::new().unwrap();
fs::write(file.path(), contents).unwrap();
file
}
#[test]
fn validate_tool_accepts_a_conformant_document() {
let file = write_temp_file(
r#"{
"@context": "https://mif-spec.dev/schema/context.jsonld",
"@type": "Concept",
"@id": "urn:mif:memory:test-001",
"conceptType": "semantic",
"content": "Test content.",
"created": "2026-07-02T00:00:00Z"
}"#,
);
let result = Mif.validate_mif_document(Parameters(ValidateParams {
file: file.path().to_path_buf(),
}));
assert!(result.ends_with(": valid"));
}
#[test]
fn validate_tool_reports_invalid_document() {
let file = write_temp_file(r#"{"content": "missing required fields"}"#);
let result = Mif.validate_mif_document(Parameters(ValidateParams {
file: file.path().to_path_buf(),
}));
assert!(result.contains("invalid"));
}
#[test]
fn validate_tool_reports_missing_file() {
let result = Mif.validate_mif_document(Parameters(ValidateParams {
file: "/nonexistent/mif-mcp-test-fixture.json".into(),
}));
assert!(result.contains("failed to read"));
}
#[test]
fn validate_tool_reports_invalid_json() {
let file = write_temp_file("not json");
let result = Mif.validate_mif_document(Parameters(ValidateParams {
file: file.path().to_path_buf(),
}));
assert!(result.contains("failed to parse"));
}
#[test]
fn resolve_tool_returns_the_extends_chain() {
let dir = tempfile::tempdir().unwrap();
fs::write(
dir.path().join("mif-base.yaml"),
"ontology:\n id: mif-base\n version: 1.0.0\n",
)
.unwrap();
fs::write(
dir.path().join("domain.yaml"),
"ontology:\n id: domain\n version: 1.0.0\n extends: [mif-base]\n",
)
.unwrap();
let result = Mif.resolve_ontology_reference(Parameters(ResolveParams {
id: "domain".to_string(),
ontologies_dir: dir.path().to_path_buf(),
}));
assert_eq!(result, "mif-base (1.0.0) -> domain (1.0.0)");
}
#[test]
fn resolve_tool_reports_unknown_ontology() {
let dir = tempfile::tempdir().unwrap();
let result = Mif.resolve_ontology_reference(Parameters(ResolveParams {
id: "missing".to_string(),
ontologies_dir: dir.path().to_path_buf(),
}));
assert!(result.contains("not found"));
}
}