use serde_json::{Map, Value};
#[derive(Debug)]
pub struct AliasTable {
pub agent: &'static str,
pub binding: &'static str,
pub renders_binding: Option<&'static str>,
pub tools: &'static [(&'static str, &'static str)],
pub event_tools: &'static [EventTool],
pub keep: &'static [(&'static str, &'static [&'static str])],
pub inputs: &'static [(&'static str, &'static str, &'static str)],
pub creates: &'static [Creates],
pub mcp_rule: McpKeyRule,
pub multi: MultiValueRule,
}
#[derive(Debug)]
pub struct EventTool {
pub event: &'static str,
pub canonical: &'static str,
pub keys: &'static [&'static str],
}
#[derive(Debug)]
pub struct Creates {
pub tool: &'static str,
pub canonical: &'static str,
pub when_absent: &'static [&'static str],
pub renames: &'static [(&'static str, &'static str)],
}
#[derive(Debug)]
pub struct McpKeyRule {
pub server_field: Option<&'static str>,
pub bare_prefix: Option<&'static str>,
pub wrapper: Option<McpWrapper>,
pub joined: bool,
pub json_string_input: bool,
}
impl McpKeyRule {
pub const NATIVE: McpKeyRule = McpKeyRule {
server_field: None,
bare_prefix: None,
wrapper: None,
joined: false,
json_string_input: false,
};
}
#[derive(Debug)]
pub struct McpWrapper {
pub tool: &'static str,
pub server_key: &'static str,
pub tool_key: &'static str,
pub arguments_key: &'static str,
}
#[derive(Debug)]
pub struct MultiValueRule {
pub rules: &'static [MultiRule],
}
impl MultiValueRule {
pub const NONE: MultiValueRule = MultiValueRule { rules: &[] };
}
#[derive(Debug)]
pub struct MultiRule {
pub tools: &'static [&'static str],
pub from: &'static [&'static str],
pub element_key: Option<&'static str>,
pub to: &'static str,
pub join: Join,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Join {
Array,
Lines,
Argv,
First,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Resolved {
pub name: String,
pub input: Option<Value>,
pub native: String,
pub mapped: bool,
}
impl Resolved {
pub fn key(&self, binding: &str) -> String {
if self.mapped {
self.name.clone()
} else {
format!("{binding}:{}", self.native)
}
}
}
impl AliasTable {
pub const fn empty(agent: &'static str, binding: &'static str) -> AliasTable {
AliasTable {
agent,
binding,
renders_binding: None,
tools: &[],
event_tools: &[],
keep: &[],
inputs: &[],
creates: &[],
mcp_rule: McpKeyRule::NATIVE,
multi: MultiValueRule::NONE,
}
}
pub fn binding_id(&self, renders: bool) -> &'static str {
match (renders, self.renders_binding) {
(true, Some(plugin)) => plugin,
_ => self.binding,
}
}
pub fn resolve(&self, native: &str, input: Option<Value>, server: Option<&str>) -> Resolved {
let mapped = |name: String, input: Option<Value>| Resolved {
name,
input,
native: native.to_string(),
mapped: true,
};
if native.starts_with("mcp__") {
return mapped(native.to_string(), input);
}
let rule = &self.mcp_rule;
if let Some(server) = server {
let tool = rule
.bare_prefix
.and_then(|prefix| native.strip_prefix(prefix))
.unwrap_or(native);
let input = match input {
Some(Value::String(raw)) if rule.json_string_input => {
Some(serde_json::from_str(&raw).unwrap_or(Value::String(raw)))
}
other => other,
};
return mapped(format!("mcp__{server}__{tool}"), input);
}
if let Some(tool) = rule.bare_prefix.and_then(|p| native.strip_prefix(p)) {
return mapped(format!("mcp__unknown__{tool}"), input);
}
if let Some(wrapper) = rule.wrapper.as_ref().filter(|w| w.tool == native) {
if let Some(resolved) = unwrap_mcp(wrapper, native, &input) {
return resolved;
}
}
if let Some((_, canonical)) = self.tools.iter().find(|(name, _)| *name == native) {
return self.map_tool(native, canonical, input);
}
if rule.joined && native.contains("__") {
return mapped(format!("mcp__{native}"), input);
}
Resolved {
name: native.to_string(),
input,
native: native.to_string(),
mapped: false,
}
}
pub fn resolve_event(&self, event: &str, payload: &Value) -> Option<Resolved> {
let row = self.event_tools.iter().find(|row| row.event == event)?;
payload.get(row.keys.first()?)?.as_str()?;
let input: Map<String, Value> = row
.keys
.iter()
.filter_map(|key| Some((key.to_string(), payload.get(key)?.clone())))
.collect();
Some(Resolved {
name: row.canonical.to_string(),
input: Some(Value::Object(input)),
native: event.to_string(),
mapped: true,
})
}
fn map_tool(&self, native: &str, canonical: &str, input: Option<Value>) -> Resolved {
let mut canonical = canonical;
let Some(Value::Object(mut args)) = input else {
return Resolved {
name: canonical.to_string(),
input,
native: native.to_string(),
mapped: true,
};
};
if let Some((_, keys)) = self.keep.iter().find(|(tool, _)| *tool == native) {
args.retain(|key, _| keys.contains(&key.as_str()));
}
let mut renames: Vec<(&str, &str)> = Vec::new();
let creating = self.creates.iter().find(|row| {
row.tool == native
&& row
.when_absent
.iter()
.all(|key| args.get(*key).is_none_or(Value::is_null))
});
if let Some(row) = creating {
canonical = row.canonical;
for key in row.when_absent {
args.remove(*key);
}
renames.extend(row.renames);
}
renames.extend(
self.inputs
.iter()
.filter(|(tool, _, _)| *tool == "*" || *tool == native)
.map(|(_, from, to)| (*from, *to)),
);
for (from, to) in renames {
if let Some(value) = args.remove(from) {
args.entry(to.to_string()).or_insert(value);
}
}
for rule in self
.multi
.rules
.iter()
.filter(|rule| rule.tools.is_empty() || rule.tools.contains(&native))
{
apply_multi(rule, &mut args);
}
Resolved {
name: canonical.to_string(),
input: Some(Value::Object(args)),
native: native.to_string(),
mapped: true,
}
}
}
fn unwrap_mcp(wrapper: &McpWrapper, native: &str, input: &Option<Value>) -> Option<Resolved> {
let args = input.as_ref()?.as_object()?;
let server = args.get(wrapper.server_key)?.as_str()?;
let tool = args.get(wrapper.tool_key)?.as_str()?;
let arguments = match args.get(wrapper.arguments_key) {
Some(Value::String(raw)) => serde_json::from_str(raw)
.ok()
.filter(Value::is_object)
.unwrap_or_else(|| Value::String(raw.clone())),
Some(other) => other.clone(),
None => Value::Object(Map::new()),
};
Some(Resolved {
name: format!("mcp__{server}__{tool}"),
input: Some(arguments),
native: native.to_string(),
mapped: true,
})
}
fn values_of(value: &Value, element_key: Option<&str>, out: &mut Vec<Value>) {
let entry = |item: &Value, out: &mut Vec<Value>| {
let text = item
.as_str()
.or_else(|| element_key.and_then(|key| item.get(key)?.as_str()));
if let Some(text) = text {
out.push(Value::String(text.to_string()));
}
};
match value {
Value::Array(items) => items.iter().for_each(|item| entry(item, out)),
other => entry(other, out),
}
}
fn apply_multi(rule: &MultiRule, args: &mut Map<String, Value>) {
let Some((source, values)) = rule.from.iter().find_map(|key| {
let mut values = Vec::new();
values_of(args.get(*key)?, rule.element_key, &mut values);
(!values.is_empty()).then_some((*key, values))
}) else {
return;
};
match rule.join {
Join::Array => {
let kept = if values.len() == 1 {
values.into_iter().next().unwrap_or(Value::Null)
} else {
Value::Array(values)
};
args.entry(rule.to.to_string()).or_insert(kept);
}
Join::First => {
if let Some(first) = values.into_iter().next() {
args.entry(rule.to.to_string()).or_insert(first);
}
}
Join::Lines => {
let joined = values
.iter()
.filter_map(Value::as_str)
.collect::<Vec<_>>()
.join("\n");
args.remove(source);
args.entry(rule.to.to_string())
.or_insert(Value::String(joined));
}
Join::Argv => {
if !args.get(source).is_some_and(Value::is_array) {
return;
}
let joined = values
.iter()
.filter_map(Value::as_str)
.map(posix_quote)
.collect::<Vec<_>>()
.join(" ");
args.insert(rule.to.to_string(), Value::String(joined));
}
}
}
fn posix_quote(word: &str) -> String {
let plain = !word.is_empty()
&& word
.chars()
.all(|c| c.is_ascii_alphanumeric() || "_@%+=:,./-".contains(c));
if plain {
word.to_string()
} else {
format!("'{}'", word.replace('\'', "'\\''"))
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
static TABLE: AliasTable = AliasTable {
agent: "fixture",
binding: "fixture_hook",
renders_binding: Some("fixture_plugin"),
tools: &[
("Sh", "Bash"),
("Open", "Read"),
("Ed", "Edit"),
("Cmds", "Bash"),
],
event_tools: &[EventTool {
event: "beforeShell",
canonical: "Bash",
keys: &["command", "cwd"],
}],
keep: &[("Sh", &["command"])],
inputs: &[("Open", "path", "file_path"), ("*", "q", "query")],
creates: &[Creates {
tool: "Ed",
canonical: "Write",
when_absent: &["old"],
renames: &[("new", "content")],
}],
mcp_rule: McpKeyRule {
server_field: Some("server"),
bare_prefix: Some("MCP:"),
wrapper: Some(McpWrapper {
tool: "call_mcp",
server_key: "s",
tool_key: "t",
arguments_key: "a",
}),
joined: true,
json_string_input: true,
},
multi: MultiValueRule {
rules: &[
MultiRule {
tools: &["Open"],
from: &["files"],
element_key: Some("path"),
to: "file_path",
join: Join::Array,
},
MultiRule {
tools: &["Cmds"],
from: &["commands"],
element_key: Some("command"),
to: "command",
join: Join::Lines,
},
MultiRule {
tools: &["Sh"],
from: &["command"],
element_key: None,
to: "command",
join: Join::Argv,
},
],
},
};
#[test]
fn a_listed_tool_is_mapped_and_an_unlisted_one_is_not() {
let sh = TABLE.resolve("Sh", Some(json!({"command": "ls", "x": 1})), None);
assert_eq!(sh.name, "Bash");
assert!(sh.mapped);
assert_eq!(sh.input, Some(json!({"command": "ls"})), "keep drops x");
let other = TABLE.resolve("Mystery", Some(json!({"a": 1})), None);
assert_eq!((other.name.as_str(), other.mapped), ("Mystery", false));
assert_eq!(other.native, "Mystery");
assert_eq!(other.input, Some(json!({"a": 1})));
}
#[test]
fn inputs_are_renamed_per_tool_and_globally() {
let open = TABLE.resolve("Open", Some(json!({"path": "a", "q": "x"})), None);
assert_eq!(open.input, Some(json!({"file_path": "a", "query": "x"})));
}
#[test]
fn several_targets_are_all_kept() {
let open = TABLE.resolve(
"Open",
Some(json!({"files": [{"path": "a"}, {"path": "b"}]})),
None,
);
assert_eq!(open.input.unwrap()["file_path"], json!(["a", "b"]));
let one = TABLE.resolve("Open", Some(json!({"files": [{"path": "a"}]})), None);
assert_eq!(one.input.unwrap()["file_path"], json!("a"));
let lines = TABLE.resolve(
"Cmds",
Some(json!({"commands": ["ls", {"command": "pwd"}]})),
None,
);
assert_eq!(lines.input, Some(json!({"command": "ls\npwd"})));
}
#[test]
fn an_argv_array_is_posix_quoted_and_joined() {
let sh = TABLE.resolve(
"Sh",
Some(json!({"command": ["rm", "-rf", "my dir", "it's"]})),
None,
);
assert_eq!(
sh.input,
Some(json!({"command": "rm -rf 'my dir' 'it'\\''s'"}))
);
let line = TABLE.resolve("Sh", Some(json!({"command": "rm -rf x"})), None);
assert_eq!(line.input, Some(json!({"command": "rm -rf x"})));
}
#[test]
fn a_tool_is_its_alternative_when_the_keys_are_absent() {
let write = TABLE.resolve("Ed", Some(json!({"new": "body"})), None);
assert_eq!(write.name, "Write");
assert_eq!(write.input, Some(json!({"content": "body"})));
let edit = TABLE.resolve("Ed", Some(json!({"old": "a", "new": "b"})), None);
assert_eq!(edit.name, "Edit");
assert_eq!(edit.input, Some(json!({"old": "a", "new": "b"})));
}
#[test]
fn mcp_calls_become_the_canonical_name() {
let canonical = TABLE.resolve("mcp__s__t", None, None);
assert_eq!(
(canonical.name.as_str(), canonical.mapped),
("mcp__s__t", true)
);
let beside = TABLE.resolve("create", Some(json!("{\"a\":1}")), Some("gh"));
assert_eq!(beside.name, "mcp__gh__create");
assert_eq!(beside.input, Some(json!({"a": 1})));
let bare = TABLE.resolve("MCP:create", Some(json!({"a": 1})), None);
assert_eq!(bare.name, "mcp__unknown__create");
let wrapped = TABLE.resolve(
"call_mcp",
Some(json!({"s": "gh", "t": "create", "a": "{\"x\":1}"})),
None,
);
assert_eq!(wrapped.name, "mcp__gh__create");
assert_eq!(wrapped.input, Some(json!({"x": 1})));
assert_eq!(wrapped.native, "call_mcp");
let joined = TABLE.resolve("gh__create", Some(json!({"a": 1})), None);
assert_eq!(joined.name, "mcp__gh__create");
assert!(joined.mapped);
let broken = TABLE.resolve("call_mcp", Some(json!({"s": "gh"})), None);
assert!(!broken.mapped, "a wrapper naming no tool is not a call");
}
#[test]
fn an_event_that_is_a_tool_call_needs_its_action() {
let payload = json!({"command": "ls", "cwd": "/w", "other": 1});
let call = TABLE
.resolve_event("beforeShell", &payload)
.expect("a call");
assert_eq!(call.name, "Bash");
assert_eq!(call.input, Some(json!({"command": "ls", "cwd": "/w"})));
assert_eq!(call.native, "beforeShell");
assert!(TABLE
.resolve_event("beforeShell", &json!({"cwd": "/w"}))
.is_none());
assert!(TABLE.resolve_event("sessionStart", &payload).is_none());
}
#[test]
fn a_lane_that_is_not_a_hook_has_its_own_binding() {
assert_eq!(TABLE.binding_id(false), "fixture_hook");
assert_eq!(TABLE.binding_id(true), "fixture_plugin");
}
}