use rmcp::model::{CallToolResult, ErrorData as McpError};
use super::{PatchloomService, validate_content_size, validate_json_depth, validate_param_size};
use crate::plan::Operation;
pub(super) enum FieldValidation {
Path(&'static str),
ParamSize(&'static str),
ContentSize(&'static str),
JsonDepth(&'static str),
}
pub(super) struct McpToolMeta {
pub(super) tool_name: &'static str,
pub(super) op_name: &'static str,
pub(super) extra: Option<&'static str>,
pub(super) has_strict: bool,
pub(super) validations: &'static [FieldValidation],
}
impl McpToolMeta {
pub(super) fn description(&self) -> String {
crate::schema::mcp_tool_description(self.op_name, self.extra)
}
}
pub(super) const MCP_TOOL_REGISTRY: &[McpToolMeta] = &[
McpToolMeta {
tool_name: "doc_set",
op_name: "doc.set",
extra: Some(
"IMPORTANT: do NOT issue concurrent calls targeting the same file; use execute_plan for multi-op atomicity.",
),
has_strict: true,
validations: &[
FieldValidation::Path("path"),
FieldValidation::ParamSize("selector"),
FieldValidation::JsonDepth("value"),
],
},
McpToolMeta {
tool_name: "doc_delete",
op_name: "doc.delete",
extra: Some(
"IMPORTANT: do NOT issue concurrent calls targeting the same file; use execute_plan for multi-op atomicity.",
),
has_strict: false,
validations: &[
FieldValidation::Path("path"),
FieldValidation::ParamSize("selector"),
],
},
McpToolMeta {
tool_name: "doc_merge",
op_name: "doc.merge",
extra: Some(
"IMPORTANT: do NOT issue concurrent calls targeting the same file; use execute_plan for multi-op atomicity.",
),
has_strict: false,
validations: &[
FieldValidation::Path("path"),
FieldValidation::JsonDepth("value"),
],
},
McpToolMeta {
tool_name: "doc_append",
op_name: "doc.append",
extra: Some(
"IMPORTANT: do NOT issue concurrent calls targeting the same file; use execute_plan for multi-op atomicity.",
),
has_strict: false,
validations: &[
FieldValidation::Path("path"),
FieldValidation::ParamSize("selector"),
FieldValidation::JsonDepth("value"),
],
},
McpToolMeta {
tool_name: "doc_prepend",
op_name: "doc.prepend",
extra: Some(
"IMPORTANT: do NOT issue concurrent calls targeting the same file; use execute_plan for multi-op atomicity.",
),
has_strict: false,
validations: &[
FieldValidation::Path("path"),
FieldValidation::ParamSize("selector"),
FieldValidation::JsonDepth("value"),
],
},
McpToolMeta {
tool_name: "doc_ensure",
op_name: "doc.ensure",
extra: Some(
"IMPORTANT: do NOT issue concurrent calls targeting the same file; use execute_plan for multi-op atomicity.",
),
has_strict: false,
validations: &[
FieldValidation::Path("path"),
FieldValidation::ParamSize("selector"),
FieldValidation::JsonDepth("value"),
],
},
McpToolMeta {
tool_name: "doc_delete_where",
op_name: "doc.delete_where",
extra: Some(
"For object arrays: predicate='role=admin'. For simple arrays: predicate='_=value'. Nested paths: predicate='settings.theme=dark'. IMPORTANT: do NOT issue concurrent calls targeting the same file; use execute_plan for multi-op atomicity.",
),
has_strict: false,
validations: &[
FieldValidation::Path("path"),
FieldValidation::ParamSize("selector"),
FieldValidation::ParamSize("predicate"),
],
},
McpToolMeta {
tool_name: "doc_update",
op_name: "doc.update",
extra: Some(
"IMPORTANT: do NOT issue concurrent calls targeting the same file; use execute_plan for multi-op atomicity.",
),
has_strict: false,
validations: &[
FieldValidation::Path("path"),
FieldValidation::ParamSize("selector"),
FieldValidation::JsonDepth("value"),
],
},
McpToolMeta {
tool_name: "doc_move",
op_name: "doc.move",
extra: Some(
"IMPORTANT: do NOT issue concurrent calls targeting the same file; use execute_plan for multi-op atomicity.",
),
has_strict: false,
validations: &[
FieldValidation::Path("path"),
FieldValidation::ParamSize("from"),
FieldValidation::ParamSize("to"),
],
},
McpToolMeta {
tool_name: "read_file",
op_name: "read",
extra: Some("Optional lines range uses start:end (1-based)."),
has_strict: false,
validations: &[FieldValidation::Path("path")],
},
McpToolMeta {
tool_name: "md_upsert_bullet",
op_name: "md.upsert_bullet",
extra: Some(
"Idempotent: skipped if the bullet is already present. IMPORTANT: do NOT issue concurrent calls targeting the same file; use execute_plan for multi-op atomicity.",
),
has_strict: false,
validations: &[
FieldValidation::Path("path"),
FieldValidation::ParamSize("bullet"),
],
},
McpToolMeta {
tool_name: "md_table_append",
op_name: "md.table_append",
extra: Some(
"IMPORTANT: do NOT issue concurrent calls targeting the same file; use execute_plan for multi-op atomicity.",
),
has_strict: false,
validations: &[
FieldValidation::Path("path"),
FieldValidation::ParamSize("row"),
],
},
McpToolMeta {
tool_name: "md_replace_section",
op_name: "md.replace_section",
extra: Some(
"IMPORTANT: do NOT issue concurrent calls targeting the same file; use execute_plan for multi-op atomicity.",
),
has_strict: false,
validations: &[
FieldValidation::Path("path"),
FieldValidation::ContentSize("content"),
],
},
McpToolMeta {
tool_name: "md_insert_after_heading",
op_name: "md.insert_after_heading",
extra: Some(
"IMPORTANT: do NOT issue concurrent calls targeting the same file; use execute_plan for multi-op atomicity.",
),
has_strict: false,
validations: &[
FieldValidation::Path("path"),
FieldValidation::ContentSize("content"),
],
},
McpToolMeta {
tool_name: "md_insert_before_heading",
op_name: "md.insert_before_heading",
extra: Some(
"IMPORTANT: do NOT issue concurrent calls targeting the same file; use execute_plan for multi-op atomicity.",
),
has_strict: false,
validations: &[
FieldValidation::Path("path"),
FieldValidation::ContentSize("content"),
],
},
McpToolMeta {
tool_name: "md_dedupe_headings",
op_name: "md.dedupe_headings",
extra: Some(
"IMPORTANT: do NOT issue concurrent calls targeting the same file; use execute_plan for multi-op atomicity.",
),
has_strict: false,
validations: &[FieldValidation::Path("path")],
},
McpToolMeta {
tool_name: "move_file",
op_name: "file.rename",
extra: Some(
"Use force=true to overwrite an existing destination. IMPORTANT: do NOT issue concurrent calls targeting the same file; use execute_plan for multi-op atomicity.",
),
has_strict: false,
validations: &[FieldValidation::Path("from"), FieldValidation::Path("to")],
},
McpToolMeta {
tool_name: "append_file",
op_name: "file.append",
extra: Some(
"IMPORTANT: do NOT issue concurrent calls targeting the same file; use execute_plan for multi-op atomicity.",
),
has_strict: false,
validations: &[
FieldValidation::Path("path"),
FieldValidation::ContentSize("content"),
],
},
McpToolMeta {
tool_name: "prepend_file",
op_name: "file.prepend",
extra: Some(
"IMPORTANT: do NOT issue concurrent calls targeting the same file; use execute_plan for multi-op atomicity.",
),
has_strict: false,
validations: &[
FieldValidation::Path("path"),
FieldValidation::ContentSize("content"),
],
},
McpToolMeta {
tool_name: "create_file",
op_name: "file.create",
extra: Some(
"Fails if the file exists unless force=true. IMPORTANT: do NOT issue concurrent calls targeting the same file; use execute_plan for multi-op atomicity.",
),
has_strict: false,
validations: &[
FieldValidation::Path("path"),
FieldValidation::ContentSize("content"),
],
},
McpToolMeta {
tool_name: "delete_file",
op_name: "file.delete",
extra: Some(
"IMPORTANT: do NOT issue concurrent calls targeting the same file; use execute_plan for multi-op atomicity.",
),
has_strict: false,
validations: &[FieldValidation::Path("path")],
},
McpToolMeta {
tool_name: "fix_whitespace",
op_name: "tidy.fix",
extra: Some(
"Defaults: trim trailing whitespace and ensure final newline when those fields are omitted. IMPORTANT: do NOT issue concurrent calls targeting the same file; use execute_plan for multi-op atomicity.",
),
has_strict: false,
validations: &[FieldValidation::Path("path")],
},
];
pub(super) fn inject_strict_into_schema(mut schema: serde_json::Value) -> serde_json::Value {
if let Some(obj) = schema.as_object_mut()
&& let Some(props) = obj.get_mut("properties").and_then(|p| p.as_object_mut())
{
props.insert(
"strict".to_string(),
serde_json::json!({
"type": "boolean",
"description": "Enable strict mode (default true). When true, roll back all file writes if format or validate lifecycle steps fail.",
"default": true
}),
);
}
schema
}
pub(super) fn handle_simple_op(
service: &PatchloomService,
meta: &McpToolMeta,
mut args: serde_json::Value,
allowed_fields: &std::collections::HashSet<String>,
) -> Result<CallToolResult, McpError> {
let args_obj = args
.as_object()
.ok_or_else(|| McpError::invalid_params("expected JSON object", None))?;
let unknown: Vec<&str> = args_obj
.keys()
.filter(|k| !allowed_fields.contains(k.as_str()))
.map(|k| k.as_str())
.collect();
if !unknown.is_empty() {
return Err(McpError::invalid_params(
format!("unknown field(s): {}", unknown.join(", ")),
None,
));
}
for v in meta.validations {
match v {
FieldValidation::Path(field) => {
if let Some(val) = args_obj.get(*field).and_then(|v| v.as_str()) {
service.check_path(val)?;
}
}
FieldValidation::ParamSize(field) => {
if let Some(val) = args_obj.get(*field).and_then(|v| v.as_str()) {
validate_param_size(field, val)?;
}
}
FieldValidation::ContentSize(field) => {
if let Some(val) = args_obj.get(*field).and_then(|v| v.as_str()) {
validate_content_size(field, val)?;
}
}
FieldValidation::JsonDepth(field) => {
if let Some(val) = args_obj.get(*field) {
validate_json_depth(field, val)?;
}
}
}
}
let strict = if meta.has_strict {
let s = args
.as_object()
.and_then(|o| o.get("strict"))
.and_then(|v| v.as_bool());
if let Some(obj) = args.as_object_mut() {
obj.remove("strict");
}
s
} else {
None
};
if let Some(obj) = args.as_object_mut() {
obj.insert(
"op".to_string(),
serde_json::Value::String(meta.op_name.to_string()),
);
}
let mut op: Operation = serde_json::from_value(args)
.map_err(|e| McpError::invalid_params(format!("invalid parameters: {e}"), None))?;
if meta.tool_name == "fix_whitespace"
&& let Operation::TidyFix {
ensure_final_newline,
trim_trailing_whitespace,
..
} = &mut op
{
if ensure_final_newline.is_none() {
*ensure_final_newline = Some(true);
}
if trim_trailing_whitespace.is_none() {
*trim_trailing_whitespace = Some(true);
}
}
service.run_one_op(op, strict)
}