Hmbown/CodeWhale · error · anyhow::Error

tool '{tool_name}' on MCP server '{server_name}' is blocked

Error message

tool '{tool_name}' on MCP server '{server_name}' is blocked by the tool filter

What it means

McpRegistry::call_tool() enforces the server's ToolFilter at invocation time, not just at listing time: if the filter denies the tool (deny-list hit, or allow-list that does not include it), calling it by bare or qualified name is refused. This makes the listing-time filter authoritative for execution too, so a tool hidden from listings cannot be reached by guessing its name.

Source

Thrown at crates/mcp/src/lib.rs:388

            }
        }
        Ok(out)
    }

    /// Call a tool on a specific server by name.
    ///
    /// The server's [`ToolFilter`] is enforced on invocation, not just at
    /// listing time: a denied (or not-allowed) tool cannot be executed by
    /// addressing the server directly, whether by bare or qualified name.
    pub fn call_tool(&self, server_name: &str, tool_name: &str, arguments: Value) -> Result<Value> {
        let client = self
            .clients
            .get(server_name)
            .with_context(|| format!("MCP server '{server_name}' not available"))?;
        if let Some((_, filter)) = self.configs.get(server_name)
            && !allowed_by_filter(tool_name, filter)
        {
            bail!("tool '{tool_name}' on MCP server '{server_name}' is blocked by the tool filter");
        }
        client.call_tool(tool_name, arguments)
    }

    /// Call a tool using its fully qualified name (e.g., `mcp__server__tool`).
    pub fn call_qualified_tool(
        &self,
        qualified_tool_name: &str,
        arguments: Value,
    ) -> Result<Value> {
        let parsed = parse_qualified_tool_name(qualified_tool_name)
            .with_context(|| format!("invalid qualified MCP tool name: {qualified_tool_name}"));

        // An exact registration is the answer. Whatever the tool returns —
        // including an error — is returned as-is: falling through to the scan
        // below on a *call* failure would re-execute the same tool, and for a
        // file write, a commit, or a paid API call that second invocation is a
        // second real side effect. Only a failed *lookup* falls through.

View on GitHub (pinned to 0c42157ee5)

Solutions

  1. Update the ToolFilter for that server: remove the tool from the deny list, or add it to the allow list
  2. Use list_tools() on the registry to see exactly which tools the filter currently permits
  3. Verify you are targeting the right server — filters are per-server, and the same tool name may be allowed elsewhere

Example fix

// before
let filter = ToolFilter::Allow(vec!["search_code".into()]);
registry.register_server(config, filter, client)?;
let result = registry.call_tool("github", "create_issue", args)?; // blocked

// after
let filter = ToolFilter::Allow(vec!["search_code".into(), "create_issue".into()]);
let result = registry.call_tool("github", "create_issue", args)?;
Defensive patterns

Strategy: validation

Validate before calling

// Consult the filtered listing before calling:
let permitted: Vec<String> = registry
    .list_tools()?
    .into_iter()
    .map(|t| t.tool_name)
    .collect();
if !permitted.contains(&"create_issue".to_string()) {
    bail!("tool not permitted by filter; update the ToolFilter first");
}

Try / catch

match registry.call_tool(server, tool, args) {
    Ok(v) => Ok(v),
    Err(err) if err.to_string().contains("blocked by the tool filter") => {
        Err(anyhow!("'{tool}' is filtered on '{server}'; adjust the ToolFilter config"))
    }
    Err(err) => Err(err),
}

Prevention

When it happens

Trigger: Calling call_tool("github", "create_issue", ...) while the github server's filter has "create_issue" in its deny list; calling a tool by its qualified name (mcp__github__create_issue) hoping to bypass the filter; an allow-list configured before the server added new tools, so every newly added tool is blocked until the allow-list is updated.

Common situations: Least-privilege setups that allow read-only tools and later need a write tool; deny-lists added after an incident; filters written against old tool names that the server renamed in a new version.

Understand the failure class

Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@0c42157ee5 (2026-08-20). Data as JSON: /api/errors/ac71d01ddba32dfd. Report an issue: GitHub.