pub mod skill;
use schemars::{JsonSchema, Schema, generate::SchemaSettings};
use serde::Serialize;
use super::types::{
CreateBranchInput, CreateBranchOutput, DefineCollectionInput, DefineCollectionOutput,
DeleteRecordInput, DeleteRecordOutput, DescribeStoreInput, DescribeStoreOutput,
LinkRecordsInput, LinkRecordsOutput, MergeBranchInput, MergeBranchOutput, PutRecordsInput,
PutRecordsOutput, ReadHistoryInput, ReadHistoryOutput, ReviewBranchInput, ReviewBranchOutput,
RunQueryInput, RunQueryOutput, UnlinkRecordsInput, UnlinkRecordsOutput,
};
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ToolSchema {
pub name: &'static str,
pub description: &'static str,
pub input_schema: Schema,
pub output_schema: Schema,
}
fn definition<Input: JsonSchema, Output: JsonSchema>(
name: &'static str,
description: &'static str,
) -> ToolSchema {
ToolSchema {
name,
description,
input_schema: SchemaSettings::draft2020_12()
.for_deserialize()
.into_generator()
.into_root_schema_for::<Input>(),
output_schema: SchemaSettings::draft2020_12()
.for_serialize()
.into_generator()
.into_root_schema_for::<Output>(),
}
}
#[must_use]
pub fn tools() -> [ToolSchema; 11] {
[
definition::<DescribeStoreInput, DescribeStoreOutput>(
"describe_store",
"Describe one captured store version in bounded pages.",
),
definition::<RunQueryInput, RunQueryOutput>(
"run_query",
"Read one bounded page from a query.",
),
definition::<ReadHistoryInput, ReadHistoryOutput>(
"read_history",
"Read retained branch snapshots or a record at a captured version.",
),
definition::<DefineCollectionInput, DefineCollectionOutput>(
"define_collection",
"Define a collection and its shape on a branch.",
),
definition::<PutRecordsInput, PutRecordsOutput>(
"put_records",
"Put one or more records on a branch atomically.",
),
definition::<DeleteRecordInput, DeleteRecordOutput>(
"delete_record",
"Delete a record and its incident edges and vectors.",
),
definition::<LinkRecordsInput, LinkRecordsOutput>(
"link_records",
"Link two records on a branch.",
),
definition::<UnlinkRecordsInput, UnlinkRecordsOutput>(
"unlink_records",
"Remove a link between two records on a branch.",
),
definition::<CreateBranchInput, CreateBranchOutput>(
"create_branch",
"Fork a branch from a retained version.",
),
definition::<ReviewBranchInput, ReviewBranchOutput>(
"review_branch",
"Read bounded branch differences, preparation states and dangling findings.",
),
definition::<MergeBranchInput, MergeBranchOutput>(
"merge_branch",
"Merge a reviewed branch, at the version its review read, into the branch it was forked from.",
),
]
}
#[cfg(test)]
mod tests {
use std::collections::BTreeSet;
use std::error::Error;
use serde_json::{Value, json};
use super::{SchemaSettings, tools};
use crate::mcp::types::{Version, tests::inputs};
type TestResult = Result<(), Box<dyn Error>>;
#[test]
fn registry_has_exactly_the_eleven_tools_and_both_schemas() -> TestResult {
let definitions = tools();
let expected = [
"describe_store",
"run_query",
"read_history",
"define_collection",
"put_records",
"delete_record",
"link_records",
"unlink_records",
"create_branch",
"review_branch",
"merge_branch",
];
assert_eq!(definitions.len(), 11);
assert_eq!(
definitions.iter().map(|tool| tool.name).collect::<Vec<_>>(),
expected
);
assert_eq!(
definitions
.iter()
.map(|tool| tool.name)
.collect::<BTreeSet<_>>()
.len(),
11
);
for tool in definitions {
for schema in [&tool.input_schema, &tool.output_schema] {
let value = serde_json::to_value(schema)?;
assert_eq!(value["type"], "object", "{}", tool.name);
assert!(
!value["properties"]
.as_object()
.ok_or("schema has no properties")?
.is_empty()
);
assert!(value["required"].as_array().is_some(), "{}", tool.name);
let validator = jsonschema::validator_for(&value)?;
assert!(!validator.is_valid(&json!("untyped blob")), "{}", tool.name);
}
}
Ok(())
}
#[test]
fn all_generated_input_schemas_match_the_typed_fixtures() -> TestResult {
let fixtures = inputs();
for (tool, (name, input)) in tools().into_iter().zip(fixtures) {
assert_eq!(tool.name, name);
let schema = serde_json::to_value(tool.input_schema)?;
let validator = jsonschema::validator_for(&schema)?;
let mut value: Value = serde_json::from_str(&input)?;
assert!(validator.is_valid(&value), "{name}: {schema}");
value
.as_object_mut()
.ok_or("input is not an object")?
.insert("unknown".to_owned(), json!(true));
assert!(!validator.is_valid(&value), "{name}");
}
Ok(())
}
#[test]
fn version_schema_matches_its_string_wire_form() -> TestResult {
let schema = SchemaSettings::draft2020_12()
.into_generator()
.into_root_schema_for::<Version>();
let validator = jsonschema::validator_for(&serde_json::to_value(schema)?)?;
assert!(validator.is_valid(&json!({"generation":"1","sequence":"2"})));
assert!(!validator.is_valid(&json!({"generation":1,"sequence":2})));
Ok(())
}
fn output_fixtures() -> Vec<String> {
let selection = r#"{"branch":"feature","version":{"generation":"1","sequence":"2"}}"#;
let page = r#"{"entries":[],"next_after":null}"#;
let counts = r#"{"collections":0,"documents":0,"edges":0,"vectors":0}"#;
let receipt =
r#"{"branch":"feature","version":{"generation":"1","sequence":"2"},"request_id":"r1"}"#;
vec![
format!(
r#"{{"selection":{selection},"branches":{page},"collections":{page},"relations":{page},"vector_fields":{page},"counts":{counts}}}"#
),
format!(r#"{{"selection":{selection},"rows":{page}}}"#),
format!(r#"{{"selection":{selection},"history":{page},"record":null}}"#),
receipt.to_owned(),
receipt.to_owned(),
receipt.to_owned(),
receipt.to_owned(),
receipt.to_owned(),
receipt.to_owned(),
format!(
r#"{{"selection":{selection},"from":{selection},"target":{selection},"diff":{page},"counts":{{"partial":{{"changes":0,"conflicts":0}},"next_after":null}},"conflicts":{page},"dangling":{page},"base_moved":null}}"#
),
receipt.to_owned(),
]
}
fn roundtrip<T: serde::de::DeserializeOwned + serde::Serialize>(
text: &str,
) -> Result<Value, Box<dyn Error>> {
let output: T = serde_json::from_str(text)?;
Ok(serde_json::from_str(&serde_json::to_string(&output)?)?)
}
#[test]
fn every_generated_output_schema_matches_its_typed_output() -> TestResult {
use crate::mcp::types::{
CreateBranchOutput, DefineCollectionOutput, DeleteRecordOutput, DescribeStoreOutput,
LinkRecordsOutput, MergeBranchOutput, PutRecordsOutput, ReadHistoryOutput,
ReviewBranchOutput, RunQueryOutput, UnlinkRecordsOutput,
};
let fixtures = output_fixtures();
let values = [
roundtrip::<DescribeStoreOutput>(&fixtures[0])?,
roundtrip::<RunQueryOutput>(&fixtures[1])?,
roundtrip::<ReadHistoryOutput>(&fixtures[2])?,
roundtrip::<DefineCollectionOutput>(&fixtures[3])?,
roundtrip::<PutRecordsOutput>(&fixtures[4])?,
roundtrip::<DeleteRecordOutput>(&fixtures[5])?,
roundtrip::<LinkRecordsOutput>(&fixtures[6])?,
roundtrip::<UnlinkRecordsOutput>(&fixtures[7])?,
roundtrip::<CreateBranchOutput>(&fixtures[8])?,
roundtrip::<ReviewBranchOutput>(&fixtures[9])?,
roundtrip::<MergeBranchOutput>(&fixtures[10])?,
];
assert_eq!(values.len(), 11);
for (tool, value) in tools().into_iter().zip(values) {
let schema = serde_json::to_value(tool.output_schema)?;
let validator = jsonschema::validator_for(&schema)?;
assert!(validator.is_valid(&value), "{}: {schema}", tool.name);
}
Ok(())
}
#[test]
fn a_shape_is_required_and_null_explicitly_removes_it() -> TestResult {
use crate::mcp::types::DefineCollectionInput;
let prefix = r#""request_id":"r1","branch":"feature","expected":{"generation":"1","sequence":"2"},"collection":"records""#;
let definitions = tools();
let tool = definitions
.iter()
.find(|tool| tool.name == "define_collection")
.ok_or("collection tool absent")?;
let validator = jsonschema::validator_for(&serde_json::to_value(&tool.input_schema)?)?;
for shape in ["null", "true", "false", r#"{"type":"object"}"#] {
let text = format!(r#"{{{prefix},"schema":{shape}}}"#);
let _: DefineCollectionInput = serde_json::from_str(&text)?;
assert!(
validator.is_valid(&serde_json::from_str::<Value>(&text)?),
"{shape}"
);
}
let missing = format!("{{{prefix}}}");
assert!(serde_json::from_str::<DefineCollectionInput>(&missing).is_err());
assert!(!validator.is_valid(&serde_json::from_str::<Value>(&missing)?));
Ok(())
}
#[test]
fn describe_counts_are_complete_and_never_advertise_a_cursor() -> TestResult {
let cursors = serde_json::to_value(crate::mcp::types::DescribeCursors::default())?;
assert!(cursors.get("counts").is_none());
let tool = tools()
.into_iter()
.find(|tool| tool.name == "describe_store")
.ok_or("describe schema missing")?;
let validator = jsonschema::validator_for(tool.output_schema.as_value())?;
let selection = json!({"branch":"main","version":{"generation":"1","sequence":"2"}});
let page = json!({"entries":[],"next_after":null});
let output = json!({
"selection":selection,
"branches":page,
"collections":page,
"relations":page,
"vector_fields":page,
"counts":{"collections":0,"documents":0,"edges":0,"vectors":0}
});
assert!(validator.is_valid(&output));
let output: crate::mcp::types::DescribeStoreOutput = serde_json::from_value(output)?;
let encoded = serde_json::to_value(output)?;
assert!(encoded["counts"].get("partial").is_none());
assert!(encoded["counts"].get("next_after").is_none());
Ok(())
}
#[test]
fn page_schemas_and_typed_limits_refuse_rows_above_u32_max() -> TestResult {
for (tool, (_, fixture)) in tools().into_iter().zip(inputs()) {
if ![
"describe_store",
"run_query",
"read_history",
"review_branch",
]
.contains(&tool.name)
{
continue;
}
let validator = jsonschema::validator_for(tool.input_schema.as_value())?;
let mut input: Value = serde_json::from_str(&fixture)?;
input["page"]["rows"] = json!(u32::MAX);
assert!(validator.is_valid(&input), "{} rejects u32::MAX", tool.name);
input["page"]["rows"] = json!(u64::from(u32::MAX) + 1);
assert!(
!validator.is_valid(&input),
"{} accepts rows above u32::MAX",
tool.name
);
}
assert!(
serde_json::from_value::<crate::mcp::types::PageLimits>(
json!({"rows":u32::MAX,"bytes":4096})
)
.is_ok()
);
assert!(
serde_json::from_value::<crate::mcp::types::PageLimits>(
json!({"rows":u64::from(u32::MAX)+1,"bytes":4096})
)
.is_err()
);
Ok(())
}
#[test]
fn review_counts_do_not_advertise_a_dangling_presence_boolean() -> TestResult {
let counts: crate::mcp::types::ReviewCounts =
serde_json::from_value(json!({"changes":0,"conflicts":0}))?;
assert_eq!(
serde_json::to_value(counts)?,
json!({"changes":0,"conflicts":0})
);
assert!(
serde_json::from_value::<crate::mcp::types::ReviewCounts>(
json!({"changes":0,"conflicts":0,"dangling":false})
)
.is_err()
);
let tool = tools()
.into_iter()
.find(|tool| tool.name == "review_branch")
.ok_or("review schema missing")?;
let validator = jsonschema::validator_for(tool.output_schema.as_value())?;
let mut output: Value =
serde_json::from_str(output_fixtures().get(9).ok_or("review fixture missing")?)?;
output["counts"]["partial"] = json!({"changes":0,"conflicts":0});
assert!(validator.is_valid(&output));
output["counts"]["partial"]["dangling"] = json!(false);
assert!(!validator.is_valid(&output));
Ok(())
}
}