use std::fmt;
#[path = "adapter/describe.rs"]
mod describe;
#[path = "adapter/history.rs"]
mod history;
#[path = "adapter/read_support.rs"]
mod read_support;
#[path = "adapter/review.rs"]
mod review;
#[path = "adapter/review_lines.rs"]
mod review_lines;
#[path = "adapter/writes.rs"]
mod writes;
#[cfg(test)]
#[path = "adapter/describe_tests.rs"]
mod describe_tests;
#[cfg(test)]
#[path = "adapter/history_tests.rs"]
mod history_tests;
#[cfg(test)]
#[path = "adapter/review_tests.rs"]
mod review_tests;
#[cfg(test)]
#[path = "adapter/writes_tests.rs"]
mod writes_tests;
use serde::{Deserialize, Deserializer, Serialize};
use serde_json::value::RawValue;
use super::{rpc, types};
use crate::serve::protocol;
pub trait Sender {
fn send(&mut self, request: protocol::Operation) -> Result<Box<RawValue>, ServiceRefusal>;
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ServiceRefusal {
pub code: String,
pub message: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub receipt: Option<Box<RawValue>>,
}
#[derive(Debug, Serialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum TextContent {
Text { text: String },
}
#[derive(Debug)]
pub struct ToolResult {
pub content: [TextContent; 1],
pub structured_content: Box<RawValue>,
pub is_error: bool,
}
impl Serialize for ToolResult {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct Encoded<'a> {
content: &'a [TextContent; 1],
#[serde(skip_serializing_if = "Option::is_none")]
structured_content: Option<&'a RawValue>,
is_error: bool,
}
Encoded {
content: &self.content,
structured_content: Some(self.structured_content.as_ref()),
is_error: self.is_error,
}
.serialize(serializer)
}
}
#[derive(Debug)]
pub struct EncodeError {
cause: serde_json::Error,
receipt: Option<Box<RawValue>>,
}
impl fmt::Display for EncodeError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
formatter,
"mcp_result_encoding_failed: {}; retained receipt {}",
self.cause,
self.receipt.is_some()
)
}
}
impl std::error::Error for EncodeError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&self.cause)
}
}
pub fn dispatch(sender: &mut impl Sender, call: &rpc::ToolCall) -> Result<ToolResult, EncodeError> {
match call.name.as_str() {
"describe_store" => return describe::dispatch(sender, call),
"read_history" => return history::dispatch(sender, call),
"review_branch" => return review::dispatch(sender, call),
"define_collection" => return writes::define_collection(sender, call),
"put_records" => return writes::put_records(sender, call),
"delete_record" => return writes::delete_record(sender, call),
"link_records" => return writes::link_records(sender, call),
"unlink_records" => return writes::unlink_records(sender, call),
"create_branch" => return writes::create_branch(sender, call),
"merge_branch" => return writes::merge_branch(sender, call),
_ => {}
}
if call.name != "run_query" {
return refused(&ServiceRefusal {
code: "mcp_tool_unknown".into(),
message: format!("{} is not a registered tool", call.name),
receipt: None,
});
}
let input: types::RunQueryInput = match serde_json::from_str(call.arguments.get()) {
Ok(input) => input,
Err(error) => {
return refused(&ServiceRefusal {
code: "mcp_arguments_invalid".into(),
message: error.to_string(),
receipt: None,
});
}
};
let requested = input.selection;
let bytes = input.page.bytes.get();
let request = protocol::Operation::QueryRun(protocol::QueryRequest {
selection: protocol::Selection {
branch: requested.branch.clone(),
version: requested.version.map(|version| version.0),
},
query: input.query,
parameters: input.parameters,
page: protocol::QueryPage {
rows: input.page.rows.into(),
bytes: input.page.bytes,
},
continuation: input.after,
rows: crate::serve::query::Rows::Values,
});
let raw = match sender.send(request) {
Ok(raw) => raw,
Err(refusal) => return refused(&refusal),
};
let answer: QueryAnswer = match serde_json::from_str(raw.get()) {
Ok(answer) => answer,
Err(error) => {
return refused(&ServiceRefusal {
code: "mcp_service_answer_invalid".into(),
message: error.to_string(),
receipt: None,
});
}
};
if let Err(error) = answer.selection.requested(&requested) {
return refused(&error);
}
let output = types::RunQueryOutput {
selection: answer.selection.output(),
rows: types::Page {
entries: answer
.rows
.into_iter()
.map(|row| types::QueryRow {
value: row.value,
origin: row.origin.map(|origin| types::RecordRef {
collection: origin.collection,
document_id: origin.id,
}),
})
.collect(),
next_after: answer.next,
},
};
match read_support::bounded(&output, bytes) {
Ok(output) => Ok(output),
Err(error) => refused(&error),
}
}
fn refused(refusal: &ServiceRefusal) -> Result<ToolResult, EncodeError> {
let structured_content =
serde_json::value::to_raw_value(refusal).map_err(|cause| EncodeError {
cause,
receipt: refusal.receipt.clone(),
})?;
let text = structured_content.get().to_owned();
Ok(ToolResult {
content: [TextContent::Text { text }],
structured_content,
is_error: true,
})
}
fn result(body: &impl Serialize, is_error: bool) -> Result<ToolResult, EncodeError> {
let structured_content =
serde_json::value::to_raw_value(body).map_err(|cause| EncodeError {
cause,
receipt: None,
})?;
let text = structured_content.get().to_owned();
Ok(ToolResult {
content: [TextContent::Text { text }],
structured_content,
is_error,
})
}
#[derive(Deserialize)]
struct QueryAnswer {
selection: read_support::Snapshot,
rows: Vec<QueryRow>,
#[serde(deserialize_with = "present_next")]
next: Option<String>,
}
#[derive(Deserialize)]
struct QueryRow {
value: Box<RawValue>,
#[serde(deserialize_with = "present_origin")]
origin: Option<QueryOrigin>,
}
#[derive(Deserialize)]
struct QueryOrigin {
collection: String,
id: String,
}
fn present_next<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Option<String>, D::Error> {
Option::deserialize(deserializer)
}
fn present_origin<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<Option<QueryOrigin>, D::Error> {
Option::deserialize(deserializer)
}
#[cfg(test)]
mod tests {
use super::*;
type TestResult = Result<(), Box<dyn std::error::Error>>;
const ARGUMENTS: &str = r#"{"selection":{"branch":"main","version":{"generation":"7","sequence":"11"}},"query":".amount","parameters":{"amount":1e2,"identity":123456789012345678},"page":{"rows":17,"bytes":4096},"after":"opaque+/=cursor"}"#;
const ANSWER: &str = r#"{"selection":{"branch":"main","version":{"generation":"7","sequence":"11"},"roots":[]},"rows":[{"value":{"amount":1e2,"identity":123456789012345678},"origin":{"collection":"accounts","id":"a","version":11,"score":null}}],"next":"next+/=cursor","engine":"jq","records_read":1}"#;
struct RecordingSender {
requests: Vec<protocol::Operation>,
answer: Option<Result<Box<RawValue>, ServiceRefusal>>,
}
impl RecordingSender {
fn answered(text: &str) -> Result<Self, serde_json::Error> {
Ok(Self {
requests: Vec::new(),
answer: Some(Ok(serde_json::from_str(text)?)),
})
}
}
impl Sender for RecordingSender {
fn send(&mut self, request: protocol::Operation) -> Result<Box<RawValue>, ServiceRefusal> {
self.requests.push(request);
self.answer.take().ok_or_else(|| ServiceRefusal {
code: "test_unexpected_request".into(),
message: "no further answer was supplied".into(),
receipt: None,
})?
}
}
fn call(name: &str, arguments: &str) -> Result<rpc::ToolCall, serde_json::Error> {
Ok(rpc::ToolCall {
name: name.into(),
arguments: serde_json::from_str(arguments)?,
metadata: None,
})
}
fn assert_refusal(result: &ToolResult, code: &str) -> TestResult {
let refusal: ServiceRefusal = serde_json::from_str(result.structured_content.get())?;
assert!(result.is_error);
assert_eq!(refusal.code, code);
let TextContent::Text { text } = &result.content[0];
assert_eq!(text, result.structured_content.get());
let encoded = serde_json::to_string(result)?;
assert!(!encoded.contains("\"jsonrpc\""));
assert!(!encoded.contains("\"error\""));
let wire: serde_json::Value = serde_json::to_value(result)?;
assert_eq!(wire["isError"], true);
assert_eq!(
wire["structuredContent"],
serde_json::from_str::<serde_json::Value>(result.structured_content.get())?
);
let text = wire["content"][0]["text"]
.as_str()
.ok_or("refusal text missing")?;
let refusal: ServiceRefusal = serde_json::from_str(text)?;
assert_eq!(refusal.code, code);
Ok(())
}
#[test]
fn query_sends_one_request_with_exact_inputs() -> TestResult {
let mut sender = RecordingSender::answered(ANSWER)?;
dispatch(&mut sender, &call("run_query", ARGUMENTS)?)?;
assert_eq!(sender.requests.len(), 1);
let Some(protocol::Operation::QueryRun(request)) = sender.requests.first() else {
return Err(std::io::Error::other("expected query.run").into());
};
assert_eq!(request.selection.branch, "main");
assert_eq!(
request.selection.version,
Some(protocol::Version {
generation: 7,
sequence: 11
})
);
assert_eq!(request.query, ".amount");
assert_eq!(
request.parameters.get("amount").map(|raw| raw.get()),
Some("1e2")
);
assert_eq!(
request.parameters.get("identity").map(|raw| raw.get()),
Some("123456789012345678")
);
assert_eq!(request.page.rows.get(), 17);
assert_eq!(request.page.bytes.get(), 4096);
assert_eq!(request.continuation.as_deref(), Some("opaque+/=cursor"));
assert_eq!(serde_json::to_string(&request.rows)?, "\"values\"");
Ok(())
}
#[test]
fn raw_row_numbers_survive_both_result_representations() -> TestResult {
#[derive(serde::Deserialize)]
struct EncodedResult {
#[serde(rename = "structuredContent")]
structured_content: Box<RawValue>,
}
let mut sender = RecordingSender::answered(ANSWER)?;
let result = dispatch(&mut sender, &call("run_query", ARGUMENTS)?)?;
assert!(!result.is_error);
assert_eq!(result.content.len(), 1);
let TextContent::Text { text } = &result.content[0];
assert_eq!(text, result.structured_content.get());
assert!(text.contains(r#""amount":1e2"#));
assert!(text.contains(r#""identity":123456789012345678"#));
let output: types::RunQueryOutput = serde_json::from_str(result.structured_content.get())?;
assert_eq!(output.selection.branch, "main");
assert_eq!(
output.selection.version.0,
protocol::Version {
generation: 7,
sequence: 11
}
);
assert_eq!(output.rows.next_after.as_deref(), Some("next+/=cursor"));
let row = output
.rows
.entries
.first()
.ok_or_else(|| std::io::Error::other("expected one row"))?;
assert_eq!(
row.value.get(),
r#"{"amount":1e2,"identity":123456789012345678}"#
);
let origin = row
.origin
.as_ref()
.ok_or_else(|| std::io::Error::other("expected origin"))?;
assert_eq!(origin.collection, "accounts");
assert_eq!(origin.document_id, "a");
let encoded: EncodedResult = serde_json::from_str(&serde_json::to_string(&result)?)?;
assert_eq!(
encoded.structured_content.get(),
result.structured_content.get()
);
Ok(())
}
#[test]
fn fresh_capture_and_scalar_rows_are_preserved() -> TestResult {
let mut sender = RecordingSender::answered(
r#"{"selection":{"branch":"main","version":{"generation":"8","sequence":"12"}},"rows":[{"value":1e2,"origin":null}],"next":null}"#,
)?;
let args = ARGUMENTS.replace(r#"{"generation":"7","sequence":"11"}"#, "null");
let result = dispatch(&mut sender, &call("run_query", &args)?)?;
let Some(protocol::Operation::QueryRun(request)) = sender.requests.first() else {
return Err(std::io::Error::other("expected query.run").into());
};
assert_eq!(request.selection.version, None);
let output: types::RunQueryOutput = serde_json::from_str(result.structured_content.get())?;
assert_eq!(
output.selection.version.0,
protocol::Version {
generation: 8,
sequence: 12
}
);
assert_eq!(
output.rows.entries.first().map(|row| row.value.get()),
Some("1e2")
);
assert!(
output
.rows
.entries
.first()
.is_some_and(|row| row.origin.is_none())
);
assert_eq!(output.rows.next_after, None);
Ok(())
}
#[test]
fn service_refusals_keep_their_codes_and_messages() -> TestResult {
for code in ["collection_missing", "version_conflict"] {
let mut sender = RecordingSender {
requests: Vec::new(),
answer: Some(Err(ServiceRefusal {
code: code.into(),
message: "original refusal".into(),
receipt: None,
})),
};
let result = dispatch(&mut sender, &call("run_query", ARGUMENTS)?)?;
assert_refusal(&result, code)?;
let refusal: ServiceRefusal = serde_json::from_str(result.structured_content.get())?;
assert_eq!(refusal.message, "original refusal");
assert_eq!(sender.requests.len(), 1);
let encoded = serde_json::to_string(&result)?;
assert!(!encoded.contains("\"jsonrpc\""));
assert!(!encoded.contains("\"error\""));
}
Ok(())
}
#[test]
fn unknown_argument_member_makes_no_request() -> TestResult {
let mut sender = RecordingSender::answered(ANSWER)?;
let args = ARGUMENTS.replacen('{', r#"{"caller":{},"#, 1);
let result = dispatch(&mut sender, &call("run_query", &args)?)?;
assert_refusal(&result, "mcp_arguments_invalid")?;
assert!(sender.requests.is_empty());
Ok(())
}
#[test]
fn invalid_limits_and_nested_members_make_no_request() -> TestResult {
for args in [
ARGUMENTS.replace("\"rows\":17", "\"rows\":0"),
ARGUMENTS.replace("\"bytes\":4096", "\"bytes\":0"),
ARGUMENTS.replace("\"rows\":17", "\"rows\":17,\"extra\":true"),
"{}".into(),
"[]".into(),
] {
let mut sender = RecordingSender::answered(ANSWER)?;
let result = dispatch(&mut sender, &call("run_query", &args)?)?;
assert_refusal(&result, "mcp_arguments_invalid")?;
assert!(sender.requests.is_empty());
}
Ok(())
}
#[test]
fn merge_branch_checks_its_arguments_before_any_request() -> TestResult {
let mut sender = RecordingSender::answered(ANSWER)?;
let result = dispatch(&mut sender, &call("merge_branch", "{}")?)?;
assert_refusal(&result, "mcp_arguments_invalid")?;
assert!(sender.requests.is_empty());
Ok(())
}
#[test]
fn malformed_write_receipts_leave_the_mutation_outcome_unknown() -> TestResult {
let arguments = r#"{"request_id":"new","branch":"main","expected":{"generation":"7","sequence":"11"},"collection":"records","schema":null}"#;
for answer in [
"{}",
r#"{"branch":"main","version":{"generation":"7","sequence":"12"},"request_id":"different"}"#,
r#"{"branch":"other","version":{"generation":"7","sequence":"12"},"request_id":"new"}"#,
] {
let mut sender = RecordingSender::answered(answer)?;
let result = dispatch(&mut sender, &call("define_collection", arguments)?)?;
assert_refusal(&result, "mcp_client_outcome_unknown")?;
let refusal: ServiceRefusal = serde_json::from_str(result.structured_content.get())?;
assert!(refusal.message.contains("mcp_service_answer_invalid"));
assert!(refusal.message.contains("request_id new"));
assert!(refusal.message.contains("mutation.receipt"));
assert_eq!(sender.requests.len(), 1);
}
Ok(())
}
#[test]
fn adapted_tools_reject_missing_input_before_a_service_request() -> TestResult {
for tool in crate::mcp::schema::tools() {
let mut sender = RecordingSender::answered(ANSWER)?;
let result = dispatch(&mut sender, &call(tool.name, "{}")?)?;
assert_refusal(&result, "mcp_arguments_invalid")?;
assert!(sender.requests.is_empty());
}
Ok(())
}
#[test]
fn malformed_service_answers_are_named_tool_refusals() -> TestResult {
for answer in [
"{}",
r#"{"selection":{"branch":"main","version":{"generation":"7","sequence":"11"}},"rows":[{"origin":null}],"next":null}"#,
r#"{"selection":{"branch":"main","version":{"generation":"7","sequence":"11"}},"rows":[{"value":1}] ,"next":null}"#,
r#"{"selection":{"branch":"main","version":{"generation":"7","sequence":"11"}},"rows":[]}"#,
r#"{"selection":{"branch":"main","version":{"generation":"7","sequence":"11"}},"rows":[],"next":42}"#,
] {
let mut sender = RecordingSender::answered(answer)?;
let result = dispatch(&mut sender, &call("run_query", ARGUMENTS)?)?;
assert_refusal(&result, "mcp_service_answer_invalid")?;
assert_eq!(sender.requests.len(), 1);
}
Ok(())
}
#[test]
fn read_tools_use_after_and_pages_with_entries_and_next_after() -> TestResult {
for tool in crate::mcp::schema::tools() {
if [
"describe_store",
"run_query",
"read_history",
"review_branch",
]
.contains(&tool.name)
{
let properties = tool.input_schema.as_value()["properties"]
.as_object()
.ok_or("input properties missing")?;
assert!(
properties.contains_key("after"),
"{} has no after field",
tool.name
);
assert!(
!properties.contains_key("cursors"),
"{} still has cursors",
tool.name
);
assert!(
!properties.contains_key("continuation"),
"{} still has continuation",
tool.name
);
}
}
let mut arguments: serde_json::Value = serde_json::from_str(ARGUMENTS)?;
let arguments = arguments
.as_object_mut()
.ok_or("arguments are not an object")?;
arguments.remove("continuation");
arguments.insert("after".to_owned(), serde_json::json!("opaque+/=cursor"));
let mut sender = RecordingSender::answered(ANSWER)?;
let result = dispatch(
&mut sender,
&call("run_query", &serde_json::to_string(arguments)?)?,
)?;
assert!(!result.is_error, "{}", result.structured_content.get());
assert_eq!(sender.requests.len(), 1);
let Some(protocol::Operation::QueryRun(request)) = sender.requests.first() else {
return Err(std::io::Error::other("expected query.run").into());
};
assert_eq!(request.continuation.as_deref(), Some("opaque+/=cursor"));
let output: serde_json::Value = serde_json::from_str(result.structured_content.get())?;
assert_eq!(
output["rows"]["entries"]
.as_array()
.ok_or("entries missing")?
.len(),
1
);
assert_eq!(output["rows"]["next_after"], "next+/=cursor");
assert!(output.get("next").is_none());
Ok(())
}
#[test]
fn query_and_fresh_description_refuse_unrequested_snapshots_before_more_calls() -> TestResult {
for (answer, arguments) in [
(
ANSWER.replace("\"branch\":\"main\"", "\"branch\":\"other\""),
ARGUMENTS.to_owned(),
),
(
ANSWER.replace("\"sequence\":\"11\"", "\"sequence\":\"12\""),
ARGUMENTS.to_owned(),
),
(
ANSWER.replace("\"branch\":\"main\"", "\"branch\":\"other\""),
ARGUMENTS.replace(r#"{"generation":"7","sequence":"11"}"#, "null"),
),
] {
let mut sender = RecordingSender::answered(&answer)?;
let result = dispatch(&mut sender, &call("run_query", &arguments)?)?;
assert_refusal(&result, "mcp_service_snapshot_mismatch")?;
assert_eq!(sender.requests.len(), 1);
}
let mut arguments: serde_json::Value = serde_json::from_str(&types::tests::inputs()[0].1)?;
arguments["selection"] = serde_json::json!({"branch":"main","version":null});
let mut sender = RecordingSender::answered(
r#"{"selection":{"branch":"other","version":{"generation":"7","sequence":"11"}},"entries":[],"next_after":null}"#,
)?;
let result = dispatch(
&mut sender,
&call("describe_store", &arguments.to_string())?,
)?;
assert_refusal(&result, "mcp_service_snapshot_mismatch")?;
assert_eq!(sender.requests.len(), 1);
Ok(())
}
#[test]
fn query_refuses_a_projected_page_that_exceeds_the_requested_bytes() -> TestResult {
let answer = r#"{"selection":{"branch":"main","version":{"generation":"7","sequence":"11"}},"rows":[{"value":1,"origin":{"collection":"records","id":"a"}}],"next":null}"#;
let mut arguments: serde_json::Value = serde_json::from_str(ARGUMENTS)?;
arguments["page"]["bytes"] = serde_json::json!(answer.len());
let mut sender = RecordingSender::answered(answer)?;
let result = dispatch(&mut sender, &call("run_query", &arguments.to_string())?)?;
assert_refusal(&result, "mcp_page_bytes_exceeded")?;
assert_eq!(sender.requests.len(), 1);
Ok(())
}
#[test]
fn diff_and_conflict_values_distinguish_absence_null_and_raw_json() -> TestResult {
let difference: review_lines::Difference = serde_json::from_value(serde_json::json!({
"collection":"records","document_id":"a","what":"document",
"base":"null","here":"\"\""
}))?;
let difference = review_lines::difference(difference).map_err(|refusal| {
std::io::Error::other(format!("{}: {}", refusal.code, refusal.message))
})?;
let difference = serde_json::to_value(difference)?;
assert_eq!(
difference["before"],
serde_json::json!({"kind":"value","value":null})
);
assert_eq!(
difference["after"],
serde_json::json!({"kind":"value","value":""})
);
let conflict: review_lines::Conflict = serde_json::from_value(serde_json::json!({
"collection":"records","document_id":"a","what":"document",
"ancestor":"","target":"null","source":r#"{"n":1e2,"id":123456789012345678}"#,
"state":"conflict","choice_token":"chosen"
}))?;
let conflict = review_lines::conflict(conflict).map_err(|refusal| {
std::io::Error::other(format!("{}: {}", refusal.code, refusal.message))
})?;
let encoded = serde_json::to_string(&conflict)?;
assert!(encoded.contains(r#""n":1e2"#));
assert!(encoded.contains(r#""id":123456789012345678"#));
let conflict: serde_json::Value = serde_json::from_str(&encoded)?;
assert_eq!(conflict["ancestor"], serde_json::json!({"kind":"absent"}));
assert_eq!(
conflict["target"],
serde_json::json!({"kind":"value","value":null})
);
assert_eq!(conflict["source"]["kind"], "value");
assert_eq!(conflict["state"], "conflict");
assert_eq!(conflict["choice_token"], "chosen");
let malformed: review_lines::Conflict = serde_json::from_value(serde_json::json!({
"collection":"records","document_id":"a","what":"document",
"ancestor":"","target":"not-json","source":"null",
"state":"conflict","choice_token":"chosen"
}))?;
let refusal = review_lines::conflict(malformed)
.err()
.ok_or("invalid JSON accepted")?;
assert_eq!(refusal.code, "mcp_service_answer_invalid");
Ok(())
}
}
#[cfg(test)]
mod typed_error_retention_tests {
use super::*;
#[test]
fn is_error_keeps_the_full_structured_error_data() -> Result<(), Box<dyn std::error::Error>> {
let refusal = ServiceRefusal {
code: "partial_commit".into(),
message: "original complete cause".into(),
receipt: None,
};
let result = refused(&refusal)?;
let encoded = serde_json::to_value(&result)?;
assert_eq!(encoded["isError"], true);
assert_eq!(encoded["structuredContent"]["code"], "partial_commit");
assert_eq!(
encoded["structuredContent"]["message"],
"original complete cause"
);
Ok(())
}
}