From 8b4bd595723a1cf58871d6653abb8fe1cdcd87ec Mon Sep 17 00:00:00 2001 From: Richard Zink Date: Thu, 12 Nov 2026 12:12:19 +0100 Subject: [PATCH] mcp_search also points to built-in tools and their availability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Models looked for generate_3d with mcp_search, which only searched MCP servers, and concluded it did not exist (the chat had been in plan mode, then switched to build). The search now lists matching built-in tools first: "available – call it directly" or "not in the read-only mode plan – ask the user to switch to build mode". Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 9 +++++++++ server/src/mcp.rs | 43 +++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e7652f..b831cb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to CodeCraft. Versions follow [Semantic Versioning](https://semver.org/); until 1.0, minor versions may contain breaking changes. +## [Unreleased] + +### Fixed + +- **Models did not find built-in tools via `mcp_search`** (e.g. `generate_3d` in a chat that + had just been switched from plan to build): the search only looked at MCP servers. It now also + lists matching built-in tools and says whether the current mode has them ("available – call it + directly", or "not in the read-only mode plan – ask the user to switch to build"). + ## [v202611-00024] – 2026-11-12 ### Added diff --git a/server/src/mcp.rs b/server/src/mcp.rs index 67b840e..28cffec 100644 --- a/server/src/mcp.rs +++ b/server/src/mcp.rs @@ -558,6 +558,42 @@ impl McpManager { /// Search the on-demand MCP tools by keywords. pub struct McpSearch; +/// Built-in tools matching the search words, with their availability in the current mode. +fn builtin_matches(ctx: &ToolCtx, words: &[&str]) -> String { + if words.is_empty() { + return String::new(); + } + let app = &ctx.app; + let owner_id = app.session(&ctx.session_id).ok().and_then(|s| s.owner_id); + let owner_user = owner_id.as_deref().and_then(|id| app.store.user(id).ok().flatten()); + let root = app.root_for(owner_id.as_deref(), &ctx.cwd); + let agent = crate::prompts::find_agent_for(app, &root, &ctx.cwd, &ctx.agent, owner_id.as_deref()); + let available: std::collections::HashSet = crate::runner::available_tools(app, &agent, owner_user.as_ref()).iter().map(|t| t.name().to_string()).collect(); + let mut hits: Vec<(usize, String)> = crate::tools::builtin_tools() + .iter() + .map(|t| { + let name = t.name().to_lowercase(); + let desc = t.description().to_lowercase(); + let score: usize = words.iter().map(|w| if name.contains(w) { 3 } else if desc.contains(w) { 1 } else { 0 }).sum(); + let state = if available.contains(t.name()) { + "available – call it directly".to_string() + } else if agent.read_only && !t.read_only() { + format!("exists, but not in the read-only mode \"{}\" – ask the user to switch to build mode", agent.name) + } else { + format!("exists, but not available in mode \"{}\" (or disabled in the settings)", agent.name) + }; + (score, format!("- {}: {state}", t.name())) + }) + .filter(|(s, _)| *s >= 2 || words.len() == 1 && *s >= 1) + .collect(); + hits.sort_by(|a, b| b.0.cmp(&a.0)); + if hits.is_empty() { + return String::new(); + } + let list: Vec = hits.into_iter().take(4).map(|(_, l)| l).collect(); + format!("Built-in tools (not MCP – call them directly by name, not via mcp_call):\n{}\n\n", list.join("\n")) +} + #[async_trait] impl Tool for McpSearch { fn name(&self) -> &str { @@ -602,10 +638,13 @@ impl Tool for McpSearch { .filter(|(s, _)| *s > 0 || words.is_empty()) .collect(); scored.sort_by(|a, b| b.0.cmp(&a.0)); + // Models also look here for built-in tools (e.g. generate_3d): say that they exist, + // whether this mode has them, and that they are called directly. + let builtin = if server.is_none() { builtin_matches(ctx, &words) } else { String::new() }; if scored.is_empty() { - return Ok(ToolOutput::text("No matching tools. Try other keywords or another server.")); + return Ok(ToolOutput::text(if builtin.is_empty() { "No matching tools. Try other keywords or another server.".to_string() } else { builtin })); } - let mut out = String::new(); + let mut out = builtin; for (_, t) in scored.iter().take(8) { let desc: String = t.info.description.chars().take(300).collect(); out.push_str(&format!("## {}\n{}\nInput schema: {}\n\n", t.full_name, desc, t.info.input_schema));