hanzo-mcp 1.1.31: read-only tools say so
Hanzo CI/CD / test (linux-amd64) (push) Skipped
Hanzo CI/CD / test (linux-arm64) (push) Skipped
Hanzo CI/CD / test-advisory (push) Skipped
publish / publish (push) Successful in 2m18s
Hanzo CI/CD / test-amd64 (push) Successful in 8m5s
Hanzo CI/CD / cicd (push) Successful in 3m54s
CI/CD / cicd (push) Successful in 12m20s

Codex, Hanzo Dev and Open Interpreter ask before any MCP tool that is not
marked read-only, and a run that never asks refuses it, so every hand-off
to Kai or Enso in a headless run was refused. The tools that read or hand
work to Hanzo's models and indexes (kai_decide, llm, research, web_*,
code_search, code_context, code_ask, vision, think) are now listed with
readOnlyHint. kai, which runs actions, and code_index, which writes the
index, are not.

Claude-Session: https://claude.ai/code/session_01M8RoubcPN71KgJuZ8LdBUL
This commit is contained in:
2026-10-15 03:28:17 -07:00
parent b8436102e1
commit 17a914dc2e
3 changed files with 30 additions and 3 deletions
+1 -1
View File
@@ -1265,7 +1265,7 @@ dependencies = [
[[package]]
name = "hanzo-mcp"
version = "1.1.30"
version = "1.1.31"
dependencies = [
"anyhow",
"async-trait",
+1 -1
View File
@@ -3,7 +3,7 @@ members = ["trigram", "actions", "shell"]
[package]
name = "hanzo-mcp"
version = "1.1.30"
version = "1.1.31"
repository = "https://github.com/hanzoai/mcp"
homepage = "https://hanzo.ai"
keywords = ["mcp", "ai", "tools", "hanzo", "llm"]
+28 -1
View File
@@ -118,6 +118,15 @@ pub struct ToolRegistry {
hanzo: Arc<RwLock<HanzoTool>>,
}
/// The tools that read or hand work to Hanzo's models and indexes and change nothing on this
/// machine, listed with `readOnlyHint`. A client that asks before a tool that may write calls these
/// without asking: Codex, Hanzo Dev and Open Interpreter refuse every unannotated call in a run
/// that never asks. `llm`'s one write is the rating `feedback` gives an answer it returned. `kai`
/// runs actions and `code_index` writes the index, so neither is here.
pub const READ_ONLY: &[&str] =
&["kai_decide", "llm", "research", "web_search", "web_read", "code_search", "code_context", "code_ask", "vision", "think"];
impl ToolRegistry {
pub fn new() -> Self {
Self {
@@ -305,7 +314,7 @@ impl ToolRegistry {
}
}
/// Get tool definitions for MCP protocol
/// Get tool definitions for MCP protocol, each read-only tool annotated as one.
pub fn get_definitions(&self) -> Vec<Value> {
let mut definitions = vec![
json!({
@@ -371,6 +380,11 @@ impl ToolRegistry {
}
definitions.retain(|d| d["name"].as_str().is_some_and(|n| self.allowed(n)));
for d in &mut definitions {
if d["name"].as_str().is_some_and(|n| READ_ONLY.contains(&n)) {
d["annotations"] = json!({ "readOnlyHint": true, "openWorldHint": true });
}
}
definitions
}
@@ -487,6 +501,19 @@ mod tests {
}
}
#[test]
fn read_only_tools_say_so_and_the_rest_do_not() {
let defs = ToolRegistry::with_defaults().get_definitions();
for t in READ_ONLY {
let d = defs.iter().find(|d| d["name"] == *t).unwrap_or_else(|| panic!("{t} is not listed"));
assert_eq!(d["annotations"]["readOnlyHint"], true, "{t}");
}
for t in ["exec", "fs", "git", "kai", "code_index", "memory"] {
let d = defs.iter().find(|d| d["name"] == t).unwrap_or_else(|| panic!("{t} is not listed"));
assert!(d.get("annotations").is_none(), "{t} must not claim to be read-only");
}
}
#[tokio::test]
async fn test_proc_execute() {
let registry = ToolRegistry::new();