# MCP local clones + tool profiles (local LLM friendly) This documents the MCP/tool-profile work done to make MCP servers usable with local LLM providers that have **low tool limits** (e.g. ~20 tools) and/or unreliable native function-calling. ## What we changed - **Local-clone policy for MCP servers** - MCP servers are cloned/built locally under `./mcp-servers/` (not installed from npm/PyPI at runtime). - Repo gitignore keeps the clones out of git while keeping `mcp-servers/README.md` and `.gitkeep`. - Added `scripts/setup-mcp-servers.sh` to clone/build the Gitea MCP server (`mcp-servers/gitea-mcp/`) into a runnable binary. - **Docker mounts for local MCP servers** - Multi-bot compose files mount `./mcp-servers` into containers at `/app/mcp-servers:ro`. - This makes config entries like `/app/mcp-servers/gitea-mcp/gitea-mcp` work inside the container. - **MCP env var expansion** - `nanobot/agent/tools/mcp.py` expands `$VARS` in MCP server `env` using the container environment, so configs can safely reference secrets without duplicating them: - Example: `"GITEA_ACCESS_TOKEN": "$NANOBOT_GITLE_TOKEN"` - **Local-provider tool calling reliability** - `nanobot/agent/context.py` now includes a strict JSON tool-call protocol for providers that don’t do native function calling. - `nanobot/providers/custom_provider.py` was improved to parse tool calls when the model returns: - Embedded JSON tool calls in content, or - A message that is *only* a JSON object (`{"name": "...", "parameters": {...}}`) - **Tool profile routing improvements** - `nanobot/agent/tool_routing.py` includes a heuristic fast-path to pick an MCP-capable profile (e.g. `workspace_mcp`) when the user intent includes PRs/issues/repos/Gitea terms. - The LLM router still applies for general cases; this heuristic prevents obvious “forge” intents from being routed to a no-MCP profile. - **LLM empty-response retry** - `nanobot/agent/loop.py` retries once if the provider returns an empty final message, nudging the model to either call a tool or respond with text. ## Where MCP servers live (host vs container) - **Host repo path**: `./mcp-servers/` - Example: `./mcp-servers/gitea-mcp/` - **Inside the nanobot container**: `/app/mcp-servers/` (mounted read-only) - Example: `/app/mcp-servers/gitea-mcp/gitea-mcp` ## Minimal Gitea MCP setup 1. Build the local server (host): ```bash ./scripts/setup-mcp-servers.sh gitea ``` 2. Add to per-bot config (host) `~/.nanobot-user1/config.json`: ```jsonc { "tools": { "mcpServers": { "gitea": { "command": "/app/mcp-servers/gitea-mcp/gitea-mcp", "args": ["-t", "stdio", "--host", "http://10.0.30.169:3000", "-r"], "env": { "GITEA_ACCESS_TOKEN": "$NANOBOT_GITLE_TOKEN" } } } } } ``` 3. Ensure your compose file mounts the local MCP servers directory: - `./mcp-servers:/app/mcp-servers:ro` ## Tool profiles: what they do (and what they don’t) - **Built-in tools** are registered once at gateway startup and do not “shut down”; profiles only control **exposure to the LLM per turn**. - **MCP tools/servers** are connected lazily and can be **connected/disconnected per turn** based on the selected profile. ### When MCP servers disconnect - **On a later message**, if a new profile is selected whose `mcpServers` set does not include a previously-connected server, nanobot disconnects that MCP server. - On **gateway shutdown**, nanobot disconnects all MCP servers. - `/new` **does not** disconnect MCP servers; it only clears session history and triggers memory consolidation. ## Common gotchas - **Router picked `workspace` (no MCP)** - If the router selects a profile with `"mcpServers": []`, MCP tools are hidden even if the servers are configured. - Either improve routing, or (if acceptable) allowlist the specific MCP server in that profile (`"mcpServers": ["gitea"]`). - **Tool count limits on local models** - A single MCP server can register 30+ tools (Gitea MCP: ~30), which can exceed local providers’ tool limits. - If you must stay under ~20 tools, prefer: - Narrow profiles, or - A “dispatcher tool” approach (one tool per MCP server) instead of registering every MCP tool individually. - **Repo owner/repo mismatch** - Errors like `GetUserByName` commonly mean the `owner` string doesn’t exist or the token cannot see it. - Resolve the canonical `owner/repo` first (search/list repos), then call list PRs with the correct pair.