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) description: &'static str,
pub(super) has_strict: bool,
pub(super) validations: &'static [FieldValidation],
}
pub(super) const MCP_TOOL_REGISTRY: &[McpToolMeta] = &[
McpToolMeta {
tool_name: "doc_set",
op_name: "doc.set",
description: "Set a value in a JSON, YAML, or TOML file. Parser-backed, preserves comments. Use dot notation for nested paths. IMPORTANT: do NOT issue concurrent calls targeting the same file; use execute_plan for multi-op atomicity. Example: {\"path\": \"package.json\", \"key\": \"version\", \"value\": \"2.0.0\"}",
has_strict: true,
validations: &[
FieldValidation::Path("path"),
FieldValidation::ParamSize("key"),
FieldValidation::JsonDepth("value"),
],
},
McpToolMeta {
tool_name: "doc_delete",
op_name: "doc.delete",
description: "Delete a value from a JSON, YAML, or TOML file. Example: {\"path\": \"package.json\", \"key\": \"scripts.test\"}",
has_strict: false,
validations: &[
FieldValidation::Path("path"),
FieldValidation::ParamSize("key"),
],
},
McpToolMeta {
tool_name: "doc_merge",
op_name: "doc.merge",
description: "Deep-merge an object into a JSON, YAML, or TOML document. Example: {\"path\": \"config.yaml\", \"value\": {\"server\": {\"port\": 8080}} }",
has_strict: false,
validations: &[
FieldValidation::Path("path"),
FieldValidation::JsonDepth("value"),
],
},
McpToolMeta {
tool_name: "doc_append",
op_name: "doc.append",
description: "Append a value to an array in a JSON, YAML, or TOML file. Example: {\"path\": \"package.json\", \"key\": \"dependencies\", \"value\": \"new-pkg\"}",
has_strict: false,
validations: &[
FieldValidation::Path("path"),
FieldValidation::ParamSize("key"),
FieldValidation::JsonDepth("value"),
],
},
McpToolMeta {
tool_name: "doc_prepend",
op_name: "doc.prepend",
description: "Prepend a value to an array in a JSON, YAML, or TOML file. Inserts at position 0.",
has_strict: false,
validations: &[
FieldValidation::Path("path"),
FieldValidation::ParamSize("key"),
FieldValidation::JsonDepth("value"),
],
},
McpToolMeta {
tool_name: "doc_ensure",
op_name: "doc.ensure",
description: "Set a value in JSON/YAML/TOML only if it does not already exist. Idempotent: no-op if present. Example: {\"path\": \"config.json\", \"key\": \"debug\", \"value\": false}",
has_strict: false,
validations: &[
FieldValidation::Path("path"),
FieldValidation::ParamSize("key"),
FieldValidation::JsonDepth("value"),
],
},
McpToolMeta {
tool_name: "doc_delete_where",
op_name: "doc.delete_where",
description: "Remove array items matching a predicate from JSON/YAML/TOML. For object arrays: predicate='role=admin'. For simple arrays: predicate='_=value'. Nested paths: predicate='settings.theme=dark'. Example: {\"path\": \"config.yaml\", \"key\": \"users\", \"predicate\": \"role=admin\"}",
has_strict: false,
validations: &[
FieldValidation::Path("path"),
FieldValidation::ParamSize("key"),
FieldValidation::ParamSize("predicate"),
],
},
McpToolMeta {
tool_name: "doc_update",
op_name: "doc.update",
description: "Update all items matching a wildcard key in a JSON, YAML, or TOML file. Example: {\"path\": \"config.yaml\", \"key\": \"servers[*].port\", \"value\": 8080}",
has_strict: false,
validations: &[
FieldValidation::Path("path"),
FieldValidation::ParamSize("key"),
FieldValidation::JsonDepth("value"),
],
},
McpToolMeta {
tool_name: "doc_move",
op_name: "doc.move",
description: "Move/rename a key in a JSON, YAML, or TOML file. Example: {\"path\": \"config.json\", \"from\": \"old_name\", \"to\": \"new_name\"}",
has_strict: false,
validations: &[
FieldValidation::Path("path"),
FieldValidation::ParamSize("from"),
FieldValidation::ParamSize("to"),
],
},
McpToolMeta {
tool_name: "read_file",
op_name: "read",
description: "Read file contents with optional line range. Example: {\"path\": \"src/main.rs\", \"lines\": \"1:50\"}",
has_strict: false,
validations: &[FieldValidation::Path("path")],
},
McpToolMeta {
tool_name: "md_upsert_bullet",
op_name: "md.upsert_bullet",
description: "Add a bullet under a markdown heading. Idempotent: skipped if already present. Example: {\"path\": \"CHANGELOG.md\", \"heading\": \"## Changes\", \"bullet\": \"- Added new feature\"}",
has_strict: false,
validations: &[
FieldValidation::Path("path"),
FieldValidation::ParamSize("bullet"),
],
},
McpToolMeta {
tool_name: "md_table_append",
op_name: "md.table_append",
description: "Append a row to a markdown table under a heading. Example: {\"path\": \"docs/api.md\", \"heading\": \"## Endpoints\", \"row\": \"| GET | /health | 200 |\"}",
has_strict: false,
validations: &[
FieldValidation::Path("path"),
FieldValidation::ParamSize("row"),
],
},
McpToolMeta {
tool_name: "md_replace_section",
op_name: "md.replace_section",
description: "Replace the content of a markdown section (from heading to next same-level heading). Example: {\"path\": \"README.md\", \"heading\": \"## Installation\", \"content\": \"Run `cargo install patchloom`.\"}",
has_strict: false,
validations: &[
FieldValidation::Path("path"),
FieldValidation::ContentSize("content"),
],
},
McpToolMeta {
tool_name: "md_insert_after_heading",
op_name: "md.insert_after_heading",
description: "Insert content immediately after a markdown heading (before existing section body). Example: {\"path\": \"AGENTS.md\", \"heading\": \"## Rules\", \"content\": \"Always run tests.\"}",
has_strict: false,
validations: &[
FieldValidation::Path("path"),
FieldValidation::ContentSize("content"),
],
},
McpToolMeta {
tool_name: "md_insert_before_heading",
op_name: "md.insert_before_heading",
description: "Insert content before a markdown heading. Example: {\"path\": \"AGENTS.md\", \"heading\": \"## Rules\", \"content\": \"## Preamble\\nRead this first.\"}",
has_strict: false,
validations: &[
FieldValidation::Path("path"),
FieldValidation::ContentSize("content"),
],
},
McpToolMeta {
tool_name: "md_dedupe_headings",
op_name: "md.dedupe_headings",
description: "Remove duplicate markdown headings in a file. Keeps the first occurrence of each heading. Example: {\"path\": \"README.md\"}",
has_strict: false,
validations: &[FieldValidation::Path("path")],
},
McpToolMeta {
tool_name: "move_file",
op_name: "file.rename",
description: "Move or rename a file. Use force=true to overwrite existing. Example: {\"from\": \"old.txt\", \"to\": \"new.txt\"}",
has_strict: false,
validations: &[FieldValidation::Path("from"), FieldValidation::Path("to")],
},
McpToolMeta {
tool_name: "append_file",
op_name: "file.append",
description: "Append content to the end of an existing file. Example: {\"path\": \"log.txt\", \"content\": \"new entry\\n\"}",
has_strict: false,
validations: &[
FieldValidation::Path("path"),
FieldValidation::ContentSize("content"),
],
},
McpToolMeta {
tool_name: "prepend_file",
op_name: "file.prepend",
description: "Prepend content to the beginning of an existing file. Example: {\"path\": \"src/main.rs\", \"content\": \"// Copyright 2026\\n\"}",
has_strict: false,
validations: &[
FieldValidation::Path("path"),
FieldValidation::ContentSize("content"),
],
},
McpToolMeta {
tool_name: "create_file",
op_name: "file.create",
description: "Create a new file with content. Fails if file exists unless force=true. Example: {\"path\": \"src/new.rs\", \"content\": \"fn main() {}\"}",
has_strict: false,
validations: &[
FieldValidation::Path("path"),
FieldValidation::ContentSize("content"),
],
},
McpToolMeta {
tool_name: "delete_file",
op_name: "file.delete",
description: "Delete a file. Example: {\"path\": \"obsolete.txt\"}",
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 op: Operation = serde_json::from_value(args)
.map_err(|e| McpError::invalid_params(format!("invalid parameters: {e}"), None))?;
service.run_one_op(op, strict)
}