use std::path::{Path, PathBuf};
use mif_problem::{ProblemMeta, ToProblem};
use rmcp::handler::server::wrapper::Parameters;
use rmcp::transport::stdio;
use rmcp::{ServerHandler, ServiceExt, schemars, tool, tool_handler, tool_router};
const DEFAULT_DB_PATH: &str = ".mif/vectors.db";
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
struct ValidateParams {
file: PathBuf,
level: Option<u8>,
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
struct ResolveParams {
id: String,
ontologies_dir: PathBuf,
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
struct IngestParams {
file: PathBuf,
db_path: Option<PathBuf>,
}
#[derive(Debug, serde::Serialize)]
struct IngestReport {
lint: &'static str,
validate: &'static str,
roundtrip: &'static str,
embedding_dim: usize,
stored: bool,
id: String,
db: String,
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
struct SearchParams {
query: String,
db_path: Option<PathBuf>,
limit: Option<usize>,
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
struct FindSimilarParams {
id: String,
db_path: Option<PathBuf>,
limit: Option<usize>,
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
struct CorpusStatsParams {
db_path: Option<PathBuf>,
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
struct RoundtripParams {
file: PathBuf,
#[serde(default)]
shape: Option<String>,
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
struct EmitJsonldParams {
file: PathBuf,
#[serde(default)]
out: Option<PathBuf>,
#[serde(default)]
shape: Option<String>,
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
struct EmitMarkdownParams {
file: PathBuf,
#[serde(default)]
out: Option<PathBuf>,
#[serde(default)]
shape: Option<String>,
}
const DEFAULT_LIMIT: usize = 10;
#[derive(Debug, serde::Serialize)]
struct MatchEntry {
id: String,
score: f32,
}
#[derive(Debug, serde::Serialize)]
struct SearchResult {
matches: Vec<MatchEntry>,
}
#[derive(Debug, serde::Serialize)]
struct CorpusStatsResult {
count: u64,
dim: Option<usize>,
db: String,
}
#[derive(Debug, thiserror::Error)]
enum McpError {
#[error("failed to read {path}: {source}")]
Io {
path: String,
#[source]
source: std::io::Error,
},
#[error("failed to parse {path} as JSON: {source}")]
Json {
path: String,
#[source]
source: serde_json::Error,
},
#[error(transparent)]
Schema(#[from] mif_schema::MifSchemaError),
#[error(transparent)]
Ontology(#[from] mif_ontology::OntologyError),
#[error(transparent)]
Frontmatter(#[from] mif_frontmatter::FrontmatterError),
#[error(transparent)]
Embed(#[from] mif_embed::EmbedError),
#[error(transparent)]
Store(#[from] mif_store::StoreError),
#[error("no document with id '{0}' has been ingested into this vector store")]
DocumentNotFound(String),
#[error("failed to serialize JSON-LD: {source}")]
JsonSerialize {
#[source]
source: serde_json::Error,
},
}
impl McpError {
const fn meta(&self) -> ProblemMeta {
match self {
Self::Io { .. } => ProblemMeta {
slug: "mif-mcp-io",
version: "v1",
title: "Failed to read an input file",
status: 500,
exit_code: 1,
},
Self::Json { .. } => ProblemMeta {
slug: "mif-mcp-invalid-json",
version: "v1",
title: "Input file is not valid JSON",
status: 400,
exit_code: 2,
},
Self::DocumentNotFound(_) => ProblemMeta {
slug: "mif-mcp-document-not-found",
version: "v1",
title: "No document with the given id has been ingested",
status: 404,
exit_code: 3,
},
Self::JsonSerialize { .. } => ProblemMeta {
slug: "mif-mcp-json-serialize-failure",
version: "v1",
title: "Failed to serialize JSON-LD to text",
status: 500,
exit_code: 1,
},
Self::Schema(_)
| Self::Ontology(_)
| Self::Frontmatter(_)
| Self::Embed(_)
| Self::Store(_) => ProblemMeta {
slug: "delegated",
version: "v1",
title: "Delegated error",
status: 500,
exit_code: 1,
},
}
}
}
impl ToProblem for McpError {
fn to_problem(&self) -> mif_problem::ProblemDetails {
match self {
Self::Schema(inner) => inner.to_problem(),
Self::Ontology(inner) => inner.to_problem(),
Self::Frontmatter(inner) => inner.to_problem(),
Self::Embed(inner) => inner.to_problem(),
Self::Store(inner) => inner.to_problem(),
Self::Io { source, .. } => {
let (status, fix, action) = mif_problem::classify_io_error(source);
let mut problem = self
.meta()
.into_details(env!("CARGO_PKG_NAME"), self.to_string());
problem.status = status;
problem.with_suggested_fix(fix).with_code_action(action)
},
Self::DocumentNotFound(_) => self
.meta()
.into_details(env!("CARGO_PKG_NAME"), self.to_string())
.with_suggested_fix(mif_problem::SuggestedFix::new(
"Ingest the document first with the `ingest_mif_document` tool, or check \
the id for a typo.",
mif_problem::Applicability::MaybeIncorrect,
))
.with_code_action(mif_problem::CodeAction::new(
"Ingest the document before searching for similar ones",
"quickfix",
mif_problem::Applicability::MaybeIncorrect,
)),
Self::Json { .. } | Self::JsonSerialize { .. } => self
.meta()
.into_details(env!("CARGO_PKG_NAME"), self.to_string()),
}
}
}
fn validate_mif_document_inner(file: &Path, level: Option<u8>) -> Result<String, McpError> {
let contents = std::fs::read_to_string(file).map_err(|source| McpError::Io {
path: file.display().to_string(),
source,
})?;
let jsonld = project_to_jsonld(
file,
&contents,
mif_frontmatter::FrontmatterShape::V1Canonical,
)?;
let level = mif_schema::Level::try_from(level.unwrap_or(1))?;
mif_schema::validate_level(&jsonld, level)?;
Ok(format!("{}: valid", file.display()))
}
fn resolve_ontology_reference_inner(id: &str, ontologies_dir: &Path) -> Result<String, McpError> {
let corpus = mif_ontology::load_corpus_from_dir(ontologies_dir)?;
let chain = mif_ontology::resolve_chain(id, &corpus)?;
Ok(chain
.iter()
.map(|ontology| format!("{} ({})", ontology.id, ontology.version))
.collect::<Vec<_>>()
.join(" -> "))
}
fn project_to_jsonld(
path: &Path,
contents: &str,
shape: mif_frontmatter::FrontmatterShape,
) -> Result<serde_json::Value, McpError> {
let contents = contents.strip_prefix('\u{feff}').unwrap_or(contents);
if contents.trim_start().starts_with("---") {
mif_frontmatter::roundtrip_lossless(contents)?;
let (frontmatter, body) = mif_frontmatter::parse_markdown(contents)?;
Ok(mif_frontmatter::md_to_jsonld(&frontmatter, &body)?)
} else {
let jsonld: serde_json::Value =
serde_json::from_str(contents).map_err(|source| McpError::Json {
path: path.display().to_string(),
source,
})?;
mif_frontmatter::jsonld_roundtrip_lossless(&jsonld, shape)?;
Ok(jsonld)
}
}
fn content_hash(contents: &str) -> String {
const FNV_OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
let mut hash = FNV_OFFSET_BASIS;
for byte in contents.as_bytes() {
hash ^= u64::from(*byte);
hash = hash.wrapping_mul(FNV_PRIME);
}
format!("{hash:016x}")
}
fn ingest_mif_document_inner(
file: &Path,
db_path: Option<&Path>,
) -> Result<IngestReport, McpError> {
let contents = std::fs::read_to_string(file).map_err(|source| McpError::Io {
path: file.display().to_string(),
source,
})?;
let jsonld = project_to_jsonld(
file,
&contents,
mif_frontmatter::FrontmatterShape::V1Canonical,
)?;
mif_schema::validate_document(&jsonld)?;
let id = jsonld
.get("@id")
.and_then(serde_json::Value::as_str)
.map_or_else(|| file.display().to_string(), ToString::to_string);
let content_text = jsonld
.get("content")
.and_then(serde_json::Value::as_str)
.unwrap_or(&contents);
let embedder = mif_embed::Embedder::load()?;
let vector = embedder.embed(content_text)?;
let db_path = db_path.map_or_else(|| PathBuf::from(DEFAULT_DB_PATH), Path::to_path_buf);
if let Some(parent) = db_path.parent()
&& !parent.as_os_str().is_empty()
{
std::fs::create_dir_all(parent).map_err(|source| McpError::Io {
path: parent.display().to_string(),
source,
})?;
}
let store = mif_store::VectorStore::open(&db_path)?;
let hash = content_hash(&contents);
let updated_at = chrono::Utc::now().to_rfc3339();
store.upsert(&id, &vector, &hash, &updated_at)?;
Ok(IngestReport {
lint: "ok",
validate: "ok",
roundtrip: "lossless",
embedding_dim: vector.len(),
stored: true,
id,
db: db_path.display().to_string(),
})
}
fn resolve_db_path(db_path: Option<&Path>) -> PathBuf {
db_path.map_or_else(|| PathBuf::from(DEFAULT_DB_PATH), Path::to_path_buf)
}
fn to_search_result(matches: Vec<mif_store::SimilarityMatch>) -> SearchResult {
SearchResult {
matches: matches
.into_iter()
.map(|m| MatchEntry {
id: m.id,
score: if m.score.is_finite() { m.score } else { 0.0 },
})
.collect(),
}
}
fn search_documents_inner(
query: &str,
db_path: Option<&Path>,
limit: usize,
) -> Result<SearchResult, McpError> {
let embedder = mif_embed::Embedder::load()?;
let vector = embedder.embed(query)?;
let db_path = resolve_db_path(db_path);
let store = mif_store::VectorStore::open(&db_path)?;
let matches = store.top_k_similar(&vector, limit)?;
Ok(to_search_result(matches))
}
fn find_similar_documents_inner(
id: &str,
db_path: Option<&Path>,
limit: usize,
) -> Result<SearchResult, McpError> {
let db_path = resolve_db_path(db_path);
let store = mif_store::VectorStore::open(&db_path)?;
let anchor = store
.get(id)?
.ok_or_else(|| McpError::DocumentNotFound(id.to_string()))?;
let matches: Vec<_> = store
.top_k_similar(&anchor.vector, limit.saturating_add(1))?
.into_iter()
.filter(|m| m.id != id)
.take(limit)
.collect();
Ok(to_search_result(matches))
}
fn corpus_stats_inner(db_path: Option<&Path>) -> Result<CorpusStatsResult, McpError> {
let db_path = resolve_db_path(db_path);
let store = mif_store::VectorStore::open(&db_path)?;
let stats = store.stats()?;
Ok(CorpusStatsResult {
count: stats.count,
dim: stats.dim,
db: db_path.display().to_string(),
})
}
fn parse_shape(shape: Option<&str>) -> Result<mif_frontmatter::FrontmatterShape, McpError> {
shape.map_or(Ok(mif_frontmatter::FrontmatterShape::V1Canonical), |s| {
Ok(mif_frontmatter::FrontmatterShape::try_from(s)?)
})
}
fn roundtrip_mif_document_inner(
file: &Path,
shape: mif_frontmatter::FrontmatterShape,
) -> Result<String, McpError> {
let contents = std::fs::read_to_string(file).map_err(|source| McpError::Io {
path: file.display().to_string(),
source,
})?;
project_to_jsonld(file, &contents, shape)?;
Ok(format!("{}: roundtrip lossless", file.display()))
}
fn emit_jsonld_document_inner(
file: &Path,
out: Option<&Path>,
shape: mif_frontmatter::FrontmatterShape,
) -> Result<String, McpError> {
let contents = std::fs::read_to_string(file).map_err(|source| McpError::Io {
path: file.display().to_string(),
source,
})?;
let jsonld = project_to_jsonld(file, &contents, shape)?;
let pretty = serde_json::to_string_pretty(&jsonld)
.map_err(|source| McpError::JsonSerialize { source })?;
if let Some(out) = out {
std::fs::write(out, format!("{pretty}\n")).map_err(|source| McpError::Io {
path: out.display().to_string(),
source,
})?;
Ok(format!(
"{}: wrote JSON-LD to {}",
file.display(),
out.display()
))
} else {
Ok(pretty)
}
}
fn emit_markdown_document_inner(
file: &Path,
out: Option<&Path>,
shape: mif_frontmatter::FrontmatterShape,
) -> Result<String, McpError> {
let contents = std::fs::read_to_string(file).map_err(|source| McpError::Io {
path: file.display().to_string(),
source,
})?;
let contents = contents.strip_prefix('\u{feff}').unwrap_or(&contents);
let jsonld: serde_json::Value =
serde_json::from_str(contents).map_err(|source| McpError::Json {
path: file.display().to_string(),
source,
})?;
let (frontmatter, body) = mif_frontmatter::jsonld_roundtrip_lossless(&jsonld, shape)?;
let markdown = mif_frontmatter::serialize_markdown(&frontmatter, &body)?;
if let Some(out) = out {
std::fs::write(out, &markdown).map_err(|source| McpError::Io {
path: out.display().to_string(),
source,
})?;
Ok(format!(
"{}: wrote markdown to {}",
file.display(),
out.display()
))
} else {
Ok(markdown)
}
}
#[derive(Clone)]
struct Mif;
#[allow(clippy::unused_self)]
#[tool_router]
impl Mif {
#[tool(
description = "Validate a MIF document (markdown with frontmatter, or a JSON-LD \
projection) against the canonical MIF JSON Schema and an optional \
L1/L2/L3 level floor (defaults to 1). No side effects."
)]
fn validate_mif_document(
&self,
Parameters(ValidateParams { file, level }): Parameters<ValidateParams>,
) -> String {
validate_mif_document_inner(&file, level)
.unwrap_or_else(|error| error.to_problem().to_json())
}
#[tool(description = "Resolve an ontology's three-tier extends chain")]
fn resolve_ontology_reference(
&self,
Parameters(ResolveParams { id, ontologies_dir }): Parameters<ResolveParams>,
) -> String {
resolve_ontology_reference_inner(&id, &ontologies_dir)
.unwrap_or_else(|error| error.to_problem().to_json())
}
#[tool(
description = "Lint, validate, prove a lossless round trip, compute an embedding, and \
store the embedding vector for one MIF document"
)]
fn ingest_mif_document(
&self,
Parameters(IngestParams { file, db_path }): Parameters<IngestParams>,
) -> String {
match ingest_mif_document_inner(&file, db_path.as_deref()) {
Ok(report) => serde_json::to_string(&report).unwrap_or_else(|_| "{}".to_string()),
Err(error) => error.to_problem().to_json(),
}
}
#[tool(description = "Free-text semantic search over previously ingested documents")]
fn search_documents(
&self,
Parameters(SearchParams {
query,
db_path,
limit,
}): Parameters<SearchParams>,
) -> String {
match search_documents_inner(&query, db_path.as_deref(), limit.unwrap_or(DEFAULT_LIMIT)) {
Ok(result) => serde_json::to_string(&result).unwrap_or_else(|_| "{}".to_string()),
Err(error) => error.to_problem().to_json(),
}
}
#[tool(description = "Find previously ingested documents similar to an already-ingested one")]
fn find_similar_documents(
&self,
Parameters(FindSimilarParams { id, db_path, limit }): Parameters<FindSimilarParams>,
) -> String {
match find_similar_documents_inner(&id, db_path.as_deref(), limit.unwrap_or(DEFAULT_LIMIT))
{
Ok(result) => serde_json::to_string(&result).unwrap_or_else(|_| "{}".to_string()),
Err(error) => error.to_problem().to_json(),
}
}
#[tool(description = "Summary statistics over the vector store")]
fn corpus_stats(
&self,
Parameters(CorpusStatsParams { db_path }): Parameters<CorpusStatsParams>,
) -> String {
match corpus_stats_inner(db_path.as_deref()) {
Ok(result) => serde_json::to_string(&result).unwrap_or_else(|_| "{}".to_string()),
Err(error) => error.to_problem().to_json(),
}
}
#[tool(
description = "Prove a MIF document's markdown <-> JSON-LD round trip is lossless. Pure: \
no db, no embedder."
)]
fn roundtrip_mif_document(
&self,
Parameters(RoundtripParams { file, shape }): Parameters<RoundtripParams>,
) -> String {
parse_shape(shape.as_deref())
.and_then(|shape| roundtrip_mif_document_inner(&file, shape))
.unwrap_or_else(|error| error.to_problem().to_json())
}
#[tool(
description = "Project a MIF document to its canonical JSON-LD form, proving the round \
trip is lossless in the process. Pure: no db, no embedder."
)]
fn emit_jsonld_document(
&self,
Parameters(EmitJsonldParams { file, out, shape }): Parameters<EmitJsonldParams>,
) -> String {
parse_shape(shape.as_deref())
.and_then(|shape| emit_jsonld_document_inner(&file, out.as_deref(), shape))
.unwrap_or_else(|error| error.to_problem().to_json())
}
#[tool(description = "Project a JSON-LD MIF document to its canonical \
markdown-with-frontmatter form, proving the round trip is lossless in \
the process. Pure: no db, no embedder.")]
fn emit_markdown_document(
&self,
Parameters(EmitMarkdownParams { file, out, shape }): Parameters<EmitMarkdownParams>,
) -> String {
parse_shape(shape.as_deref())
.and_then(|shape| emit_markdown_document_inner(&file, out.as_deref(), shape))
.unwrap_or_else(|error| error.to_problem().to_json())
}
}
#[tool_handler(
name = "mif-mcp",
instructions = "Validate, ingest, and semantically search 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 mif_problem::ToProblem;
use super::{
CorpusStatsParams, EmitJsonldParams, EmitMarkdownParams, FindSimilarParams, IngestParams,
McpError, Mif, Parameters, ResolveParams, RoundtripParams, SearchParams, ValidateParams,
ingest_mif_document_inner, to_search_result,
};
fn write_temp_file(contents: &str) -> tempfile::NamedTempFile {
let file = tempfile::NamedTempFile::new().unwrap();
fs::write(file.path(), contents).unwrap();
file
}
fn warm_embedding_model_cache() {
static ONCE: std::sync::Once = std::sync::Once::new();
ONCE.call_once(|| {
let _ = mif_embed::Embedder::load();
});
}
#[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(),
level: None,
}));
assert!(result.ends_with(": valid"));
}
#[test]
fn validate_tool_accepts_a_conformant_markdown_document() {
let file = write_temp_file(VALID_MARKDOWN_FIXTURE);
let result = Mif.validate_mif_document(Parameters(ValidateParams {
file: file.path().to_path_buf(),
level: None,
}));
assert!(result.ends_with(": valid"));
}
#[test]
fn validate_tool_accepts_a_markdown_document_with_a_leading_byte_order_mark() {
let file = write_temp_file(&format!("\u{feff}{VALID_MARKDOWN_FIXTURE}"));
let result = Mif.validate_mif_document(Parameters(ValidateParams {
file: file.path().to_path_buf(),
level: None,
}));
assert!(result.ends_with(": valid"));
}
#[test]
fn validate_tool_reports_invalid_document_as_problem_json() {
let file = write_temp_file(r#"{"content": "missing required fields"}"#);
let result = Mif.validate_mif_document(Parameters(ValidateParams {
file: file.path().to_path_buf(),
level: None,
}));
let value: serde_json::Value = serde_json::from_str(&result).unwrap();
assert_eq!(
value["type"],
"https://modeled-information-format.github.io/mif-rs/references/errors/invalid-document/v1"
);
assert_eq!(value["status"], 422);
}
#[test]
fn validate_tool_reports_a_level_floor_violation_as_problem_json() {
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(),
level: Some(2),
}));
let value: serde_json::Value = serde_json::from_str(&result).unwrap();
assert_eq!(
value["type"],
"https://modeled-information-format.github.io/mif-rs/references/errors/level-floor-violation/v1"
);
assert_eq!(value["status"], 422);
}
#[test]
fn validate_tool_accepts_a_document_satisfying_the_l2_floor() {
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",
"namespace": "test",
"modified": "2026-07-02T00:00:00Z",
"temporal": {}
}"#,
);
let result = Mif.validate_mif_document(Parameters(ValidateParams {
file: file.path().to_path_buf(),
level: Some(2),
}));
assert!(result.ends_with(": valid"));
}
#[test]
fn validate_tool_reports_an_out_of_range_level_as_problem_json() {
let file = write_temp_file(VALID_MARKDOWN_FIXTURE);
let result = Mif.validate_mif_document(Parameters(ValidateParams {
file: file.path().to_path_buf(),
level: Some(9),
}));
let value: serde_json::Value = serde_json::from_str(&result).unwrap();
assert_eq!(
value["type"],
"https://modeled-information-format.github.io/mif-rs/references/errors/unsupported-level/v1"
);
assert_eq!(value["status"], 400);
}
#[test]
fn validate_tool_reports_missing_file_as_problem_json() {
let result = Mif.validate_mif_document(Parameters(ValidateParams {
file: "/nonexistent/mif-mcp-test-fixture.json".into(),
level: None,
}));
let value: serde_json::Value = serde_json::from_str(&result).unwrap();
assert_eq!(
value["type"],
"https://modeled-information-format.github.io/mif-rs/references/errors/mif-mcp-io/v1"
);
assert_eq!(value["status"], 404);
assert_eq!(value["suggested_fix"]["applicability"], "maybe_incorrect");
}
#[test]
fn validate_tool_reports_a_directory_io_fault_at_500() {
let dir = tempfile::tempdir().unwrap();
let result = Mif.validate_mif_document(Parameters(ValidateParams {
file: dir.path().to_path_buf(),
level: None,
}));
let value: serde_json::Value = serde_json::from_str(&result).unwrap();
assert_eq!(
value["type"],
"https://modeled-information-format.github.io/mif-rs/references/errors/mif-mcp-io/v1"
);
#[cfg(not(windows))]
{
assert_eq!(value["status"], 500);
assert_eq!(value["suggested_fix"]["applicability"], "unspecified");
}
#[cfg(windows)]
{
assert_eq!(value["status"], 403);
assert_eq!(value["suggested_fix"]["applicability"], "maybe_incorrect");
}
}
#[test]
fn validate_tool_reports_invalid_json_as_problem_json() {
let file = write_temp_file("not json");
let result = Mif.validate_mif_document(Parameters(ValidateParams {
file: file.path().to_path_buf(),
level: None,
}));
let value: serde_json::Value = serde_json::from_str(&result).unwrap();
assert_eq!(
value["type"],
"https://modeled-information-format.github.io/mif-rs/references/errors/mif-mcp-invalid-json/v1"
);
}
#[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_as_problem_json() {
let dir = tempfile::tempdir().unwrap();
let result = Mif.resolve_ontology_reference(Parameters(ResolveParams {
id: "missing".to_string(),
ontologies_dir: dir.path().to_path_buf(),
}));
let value: serde_json::Value = serde_json::from_str(&result).unwrap();
assert_eq!(
value["type"],
"https://modeled-information-format.github.io/mif-rs/references/errors/ontology-not-found/v1"
);
assert_eq!(value["status"], 404);
}
const VALID_MARKDOWN_FIXTURE: &str = "---
id: memory:mcp-test-001
type: semantic
created: 2026-07-02T00:00:00Z
---
Test content via MCP.
";
const DRIFTING_MARKDOWN_FIXTURE: &str =
"---\nid: x\ntype: semantic\n123: orphaned-value\n---\n\nBody.\n";
#[test]
fn roundtrip_tool_accepts_a_conformant_document() {
let file = write_temp_file(VALID_MARKDOWN_FIXTURE);
let result = Mif.roundtrip_mif_document(Parameters(RoundtripParams {
file: file.path().to_path_buf(),
shape: None,
}));
assert!(result.ends_with("roundtrip lossless"));
}
#[test]
fn roundtrip_tool_reports_a_drifting_document_as_problem_json() {
let file = write_temp_file(DRIFTING_MARKDOWN_FIXTURE);
let result = Mif.roundtrip_mif_document(Parameters(RoundtripParams {
file: file.path().to_path_buf(),
shape: None,
}));
let value: serde_json::Value = serde_json::from_str(&result).unwrap();
assert_eq!(
value["type"],
"https://modeled-information-format.github.io/mif-rs/references/errors/roundtrip-drift/v1"
);
}
#[test]
fn roundtrip_tool_reports_json_ld_field_loss_as_problem_json() {
let file = write_temp_file(
r#"{
"@context": "https://mif-spec.dev/schema/context.jsonld",
"@type": "Concept",
"@id": "urn:mif:memory:mcp-timestamp-loss-test",
"conceptType": "semantic",
"content": "Test content.",
"timestamp": "2026-01-01T00:00:00Z"
}"#,
);
let result = Mif.roundtrip_mif_document(Parameters(RoundtripParams {
file: file.path().to_path_buf(),
shape: None,
}));
let value: serde_json::Value = serde_json::from_str(&result).unwrap();
assert_eq!(
value["type"],
"https://modeled-information-format.github.io/mif-rs/references/errors/roundtrip-drift/v1"
);
}
#[test]
fn roundtrip_tool_accepts_json_ld_whose_timestamp_is_consistently_derived() {
let file = write_temp_file(
r#"{
"@context": "https://mif-spec.dev/schema/context.jsonld",
"@type": "Concept",
"@id": "urn:mif:memory:mcp-timestamp-consistent-test",
"conceptType": "semantic",
"content": "Test content.",
"created": "2026-01-01T00:00:00Z",
"timestamp": "2026-01-01T00:00:00Z"
}"#,
);
let result = Mif.roundtrip_mif_document(Parameters(RoundtripParams {
file: file.path().to_path_buf(),
shape: None,
}));
assert!(result.ends_with("roundtrip lossless"));
}
#[test]
fn roundtrip_tool_reports_an_unrecognized_shape_as_problem_json() {
let file = write_temp_file(VALID_MARKDOWN_FIXTURE);
let result = Mif.roundtrip_mif_document(Parameters(RoundtripParams {
file: file.path().to_path_buf(),
shape: Some("PreProjected".to_string()),
}));
let value: serde_json::Value = serde_json::from_str(&result).unwrap();
assert_eq!(
value["type"],
"https://modeled-information-format.github.io/mif-rs/references/errors/unknown-frontmatter-shape/v1"
);
}
#[test]
fn emit_jsonld_tool_returns_the_projection_inline_by_default() {
let file = write_temp_file(VALID_MARKDOWN_FIXTURE);
let result = Mif.emit_jsonld_document(Parameters(EmitJsonldParams {
file: file.path().to_path_buf(),
out: None,
shape: None,
}));
let value: serde_json::Value = serde_json::from_str(&result).unwrap();
assert_eq!(value["@id"], "urn:mif:memory:mcp-test-001");
}
#[test]
fn emit_jsonld_tool_writes_to_the_out_path_when_given() {
let file = write_temp_file(VALID_MARKDOWN_FIXTURE);
let out_dir = tempfile::tempdir().unwrap();
let out_path = out_dir.path().join("out.json");
let result = Mif.emit_jsonld_document(Parameters(EmitJsonldParams {
file: file.path().to_path_buf(),
out: Some(out_path.clone()),
shape: None,
}));
assert!(result.contains("wrote JSON-LD to"));
let written = fs::read_to_string(&out_path).unwrap();
let value: serde_json::Value = serde_json::from_str(&written).unwrap();
assert_eq!(value["@id"], "urn:mif:memory:mcp-test-001");
}
#[test]
fn emit_markdown_tool_returns_the_projection_inline_by_default() {
let file = write_temp_file(
r#"{
"@context": "https://mif-spec.dev/schema/context.jsonld",
"@type": "Concept",
"@id": "urn:mif:memory:mcp-emit-md-test",
"conceptType": "semantic",
"content": "Test content.",
"created": "2026-07-02T00:00:00Z"
}"#,
);
let result = Mif.emit_markdown_document(Parameters(EmitMarkdownParams {
file: file.path().to_path_buf(),
out: None,
shape: None,
}));
assert!(result.starts_with("---\n"));
}
#[test]
fn emit_markdown_tool_accepts_json_ld_with_a_leading_byte_order_mark() {
let mut contents = "\u{feff}".to_string();
contents.push_str(
r#"{
"@context": "https://mif-spec.dev/schema/context.jsonld",
"@type": "Concept",
"@id": "urn:mif:memory:mcp-bom-test",
"conceptType": "semantic",
"content": "Test content.",
"created": "2026-07-02T00:00:00Z"
}"#,
);
let file = write_temp_file(&contents);
let result = Mif.emit_markdown_document(Parameters(EmitMarkdownParams {
file: file.path().to_path_buf(),
out: None,
shape: None,
}));
assert!(result.starts_with("---\n"));
}
#[test]
fn emit_markdown_tool_reports_invalid_json_as_problem_json() {
let file = write_temp_file("not json");
let result = Mif.emit_markdown_document(Parameters(EmitMarkdownParams {
file: file.path().to_path_buf(),
out: None,
shape: None,
}));
let value: serde_json::Value = serde_json::from_str(&result).unwrap();
assert_eq!(
value["type"],
"https://modeled-information-format.github.io/mif-rs/references/errors/mif-mcp-invalid-json/v1"
);
}
#[test]
fn emit_markdown_tool_reports_json_ld_field_loss_as_problem_json() {
let file = write_temp_file(
r#"{
"@context": "https://mif-spec.dev/schema/context.jsonld",
"@type": "Concept",
"@id": "urn:mif:memory:mcp-emit-md-timestamp-loss-test",
"conceptType": "semantic",
"content": "Test content.",
"timestamp": "2026-01-01T00:00:00Z"
}"#,
);
let result = Mif.emit_markdown_document(Parameters(EmitMarkdownParams {
file: file.path().to_path_buf(),
out: None,
shape: None,
}));
let value: serde_json::Value = serde_json::from_str(&result).unwrap();
assert_eq!(
value["type"],
"https://modeled-information-format.github.io/mif-rs/references/errors/roundtrip-drift/v1"
);
}
#[test]
fn json_serialize_error_maps_to_a_versioned_problem_type() {
let source = serde_json::from_str::<serde_json::Value>("not json").unwrap_err();
let error = McpError::JsonSerialize { source };
let problem = error.to_problem();
assert_eq!(
problem.problem_type,
"https://modeled-information-format.github.io/mif-rs/references/errors/mif-mcp-json-serialize-failure/v1"
);
assert_eq!(problem.status, 500);
assert_eq!(problem.exit_code, Some(1));
}
#[test]
fn ingest_tool_accepts_a_conformant_document_and_stores_it() {
warm_embedding_model_cache();
let file = write_temp_file(VALID_MARKDOWN_FIXTURE);
let db_dir = tempfile::tempdir().unwrap();
let db_path = db_dir.path().join("vectors.db");
let result = Mif.ingest_mif_document(Parameters(IngestParams {
file: file.path().to_path_buf(),
db_path: Some(db_path.clone()),
}));
let value: serde_json::Value = serde_json::from_str(&result).unwrap();
assert_eq!(value["lint"], "ok");
assert_eq!(value["validate"], "ok");
assert_eq!(value["roundtrip"], "lossless");
assert_eq!(value["embedding_dim"], 384);
assert_eq!(value["stored"], true);
let store = mif_store::VectorStore::open(&db_path).unwrap();
assert_eq!(store.count().unwrap(), 1);
}
#[test]
fn ingest_tool_reports_invalid_document_as_problem_json_and_writes_no_row() {
let db_dir = tempfile::tempdir().unwrap();
let db_path = db_dir.path().join("vectors.db");
let invalid_file = write_temp_file(
"---
id: memory:mcp-test-002
created: 2026-07-02T00:00:00Z
---
No type field.
",
);
let result = Mif.ingest_mif_document(Parameters(IngestParams {
file: invalid_file.path().to_path_buf(),
db_path: Some(db_path.clone()),
}));
let value: serde_json::Value = serde_json::from_str(&result).unwrap();
assert_eq!(
value["type"],
"https://modeled-information-format.github.io/mif-rs/references/errors/invalid-document/v1"
);
let store = mif_store::VectorStore::open(&db_path).unwrap();
assert_eq!(store.count().unwrap(), 0);
}
#[test]
fn ingest_reports_the_real_file_path_on_a_json_ld_parse_error() {
let db_dir = tempfile::tempdir().unwrap();
let db_path = db_dir.path().join("vectors.db");
let file = write_temp_file("not valid json");
let error = ingest_mif_document_inner(file.path(), Some(&db_path)).unwrap_err();
let message = error.to_string();
assert!(
message.contains(&file.path().display().to_string()),
"expected the real file path in {message:?}, not the ingest-input placeholder"
);
}
#[test]
fn delegated_error_variants_render_a_sane_problem_if_ever_directly_matched() {
for error in [
McpError::Frontmatter(mif_frontmatter::FrontmatterError::MissingFrontmatter),
McpError::Embed(mif_embed::EmbedError::NoCacheDir { model: "test" }),
McpError::Store(mif_store::StoreError::MissingParentDir {
path: "test".to_string(),
}),
] {
let problem = error.to_problem();
assert!(problem.status >= 400, "status was {}", problem.status);
let meta = error.meta();
assert_eq!(meta.status, 500);
}
}
#[test]
fn ingest_tool_missing_file_reports_a_404_problem() {
let db_dir = tempfile::tempdir().unwrap();
let db_path = db_dir.path().join("vectors.db");
let error = ingest_mif_document_inner(
std::path::Path::new("/nonexistent/mif-mcp-fixture.json"),
Some(&db_path),
)
.unwrap_err();
assert_eq!(error.to_problem().status, 404);
}
#[test]
fn ingest_tool_reports_an_io_error_when_the_db_parent_directory_cannot_be_created() {
warm_embedding_model_cache();
let file = write_temp_file(VALID_MARKDOWN_FIXTURE);
let parent_dir = tempfile::tempdir().unwrap();
let blocker = parent_dir.path().join("blocker");
fs::write(&blocker, "not a directory").unwrap();
let db_path = blocker.join("subdir").join("vectors.db");
let error = ingest_mif_document_inner(file.path(), Some(&db_path)).unwrap_err();
assert_eq!(error.to_problem().status, 500);
}
#[test]
fn search_tool_reports_a_problem_when_the_store_cannot_be_opened() {
let db_dir = tempfile::tempdir().unwrap();
let result = Mif.search_documents(Parameters(SearchParams {
query: "anything".to_string(),
db_path: Some(db_dir.path().to_path_buf()),
limit: None,
}));
let value: serde_json::Value = serde_json::from_str(&result).unwrap();
assert!(value.get("status").is_some());
}
#[test]
fn corpus_stats_tool_reports_a_problem_when_the_store_cannot_be_opened() {
let db_dir = tempfile::tempdir().unwrap();
let result = Mif.corpus_stats(Parameters(CorpusStatsParams {
db_path: Some(db_dir.path().to_path_buf()),
}));
let value: serde_json::Value = serde_json::from_str(&result).unwrap();
assert!(value.get("status").is_some());
}
#[test]
fn to_search_result_clamps_a_non_finite_score_to_zero() {
let result = to_search_result(vec![
mif_store::SimilarityMatch {
id: "urn:mif:memory:a".to_string(),
score: f32::NAN,
},
mif_store::SimilarityMatch {
id: "urn:mif:memory:b".to_string(),
score: f32::INFINITY,
},
]);
let json = serde_json::to_string(&result).unwrap();
assert!(json.contains("\"score\":0.0"));
}
fn ingest_fixture(db_path: &std::path::Path, id: &str, content: &str) {
warm_embedding_model_cache();
let file = write_temp_file(&format!(
"---\nid: {id}\ntype: semantic\ncreated: 2026-07-02T00:00:00Z\n---\n\n{content}\n"
));
Mif.ingest_mif_document(Parameters(IngestParams {
file: file.path().to_path_buf(),
db_path: Some(db_path.to_path_buf()),
}));
}
#[test]
fn search_tool_ranks_ingested_documents_by_relevance() {
let db_dir = tempfile::tempdir().unwrap();
let db_path = db_dir.path().join("vectors.db");
ingest_fixture(&db_path, "mcp:cats", "Cats are small domesticated felines.");
ingest_fixture(
&db_path,
"mcp:finance",
"Quarterly revenue exceeded analyst expectations.",
);
let result = Mif.search_documents(Parameters(SearchParams {
query: "A furry pet cat".to_string(),
db_path: Some(db_path),
limit: None,
}));
let value: serde_json::Value = serde_json::from_str(&result).unwrap();
assert_eq!(value["matches"][0]["id"], "urn:mif:mcp:cats");
}
#[test]
fn find_similar_tool_excludes_the_anchor_document_itself() {
let db_dir = tempfile::tempdir().unwrap();
let db_path = db_dir.path().join("vectors.db");
ingest_fixture(&db_path, "mcp:a", "Cats are small domesticated felines.");
ingest_fixture(&db_path, "mcp:b", "Dogs are loyal domesticated canines.");
let result = Mif.find_similar_documents(Parameters(FindSimilarParams {
id: "urn:mif:mcp:a".to_string(),
db_path: Some(db_path),
limit: None,
}));
let value: serde_json::Value = serde_json::from_str(&result).unwrap();
let ids: Vec<&str> = value["matches"]
.as_array()
.unwrap()
.iter()
.map(|m| m["id"].as_str().unwrap())
.collect();
assert!(!ids.contains(&"urn:mif:mcp:a"));
assert!(ids.contains(&"urn:mif:mcp:b"));
}
#[test]
fn find_similar_tool_reports_document_not_found_as_problem_json() {
let db_dir = tempfile::tempdir().unwrap();
let db_path = db_dir.path().join("vectors.db");
mif_store::VectorStore::open(&db_path).unwrap();
let result = Mif.find_similar_documents(Parameters(FindSimilarParams {
id: "urn:mif:mcp:missing".to_string(),
db_path: Some(db_path),
limit: None,
}));
let value: serde_json::Value = serde_json::from_str(&result).unwrap();
assert_eq!(
value["type"],
"https://modeled-information-format.github.io/mif-rs/references/errors/mif-mcp-document-not-found/v1"
);
assert_eq!(value["status"], 404);
}
#[test]
fn corpus_stats_tool_reports_count_and_dim() {
let db_dir = tempfile::tempdir().unwrap();
let db_path = db_dir.path().join("vectors.db");
let empty = Mif.corpus_stats(Parameters(CorpusStatsParams {
db_path: Some(db_path.clone()),
}));
let value: serde_json::Value = serde_json::from_str(&empty).unwrap();
assert_eq!(value["count"], 0);
assert!(value["dim"].is_null());
ingest_fixture(&db_path, "mcp:one", "Some content.");
let result = Mif.corpus_stats(Parameters(CorpusStatsParams {
db_path: Some(db_path),
}));
let value: serde_json::Value = serde_json::from_str(&result).unwrap();
assert_eq!(value["count"], 1);
assert_eq!(value["dim"], 384);
}
}