MCP servers

Klea acts as an MCP client: MCP servers supply the tools its graphs can call, from the bundled common tools to domain-specific servers such as nml-mcp. This page describes how Klea uses and configures those servers.

How Klea uses MCP

Klea acts as the MCP client: it uses MCP servers to give its LLMs access to external tools (validation, file handling, model lookup, code execution, etc.). A domain in a RAG or agent config can declare one or more MCP servers; the tools they expose are fetched at startup and made available to the graph’s tool binder/picker. The agent’s binder completes the arguments for the Planner’s chosen tools; the RAG picker chooses tools for a query.

Configuring MCP servers

Each domain lists the MCP servers it can use under mcp_servers. Each entry maps a server name to the URL of a streamable HTTP MCP endpoint:

{
    "domains": {
        "NeuroML": {
            "mcp_servers": {
                "NeuroML": {
                    "url": "http://127.0.0.1:8542/mcp"
                }
            }
        }
    }
}

See Create and use a RAG system for a full example and the Deploying Klea RAG on HuggingFace Spaces guide for a deployed setup.

Adding your own MCP servers

Klea ships two reference servers to copy from:

  • the bundled common tools server in klea_utils.mcp.server (web fetch, file handling, command execution), which applications auto-launch as a stdio subprocess; and

  • the NeuroML server in the mcp_pkg package (neuroml_mcp), exposed by the nml-mcp CLI.

To add your own, implement an MCP server (any language or framework works), start it, and point a domain’s mcp_servers entry at it. The tool modules in mcp_pkg and the bundled server are the worked examples of the pattern Klea expects. Tag filters (below) let you expose only part of a server’s toolset.

Filtering tools by tag

Klea lets a deployment enable or disable the tools of a configured MCP server without editing any server code. Each tool carries tags; the RAG/agent config selects which tags to expose through fastmcp’s include_tags / exclude_tags fields on a server entry. A RAG domain that only wants the query tools of a server (not its local file / code tools) can filter by tag:

{
    "domains": {
        "NeuroML": {
            "mcp_servers": {
                "NeuroML": {
                    "url": "http://127.0.0.1:8542/mcp",
                    "include_tags": ["web", "neuroml"],
                    "exclude_tags": ["code"]
                }
            }
        }
    }
}

include_tags exposes only tools carrying at least one listed tag; exclude_tags hides any tool carrying a listed tag. Both are optional and can be combined. When only exclude_tags is set, everything except the excluded tags is enabled. These fields also work for stdio servers (the bundled tools server below).

The tag vocabulary

Tags are used for Klea’s own config filtering – which domain’s tools to expose, and whether to allow local or web-facing tools. Two groups:

  • Scope tracks where a tool operates: local (filesystem / process on the host) or web (interacts with external URLs / web APIs).

  • Domain / functional groups tools by purpose, for example files, code, download, search, neuroml, neuroml-db, osb.

Every tool also carries the bundled tag when it comes from the common bundled server, so enabling the whole common set is a single include_tags: ["bundled"]. Specific current assignments:

bundled  web_fetch, web_search, search_papers, list_files, find_files, read_file, grep, write_file, edit_file, download_file, run_command (each also has its scope + functional tags)

Web scope:   web_fetch (bundled), search_papers (bundled), web_search (bundled, search), download_file (bundled, download)
Local scope: list_files / find_files / read_file / grep (bundled, files),
             write_file / edit_file (bundled, files, destructive),
             run_command (bundled, code),
             run_python_code / run_lems_simulation (neuroml, code),
             create_new_NeuroML_model (neuroml)

Behavioral intent (whether a tool is read-only or destructive) is not tagged. That is carried by standard MCP tool annotations (readOnlyHint / destructiveHint), which any compliant client can enforce without knowing Klea’s tag vocabulary. Tags answer “which tools to expose”; annotations answer “what effects the tool may have”.

run_command executes a shell command and is therefore marked destructive + open_world: it is full-mode only (never offered or run under read_only, see below), and its optional working_directory argument is gated like any other path (see Path permissions below) but does not confine the command (a shell can cd elsewhere or use absolute paths). Commands default to a 30-second timeout with a 600-second ceiling, overridable via KLEA_RUN_COMMAND_MAX_TIMEOUT. See ADR-0038 for the design and its limits.

Klea-authored tools (this one included) also refuse to run when the server process has root privileges (uid 0), unless the KLEA_ALLOW_ROOT_TOOLS environment variable is set. This catches accidental root deployments (containers default to root); third-party MCP servers are not covered and run with whatever privileges they are given.

grep and find_files search file contents and file paths and are marked read-only, so they are available under both access levels. When the optional search extra is installed (a pinned ripgrep binary built by Astral and installed as rg), they use it; otherwise they fall back to an in-house walker. The ripgrep backend honours .gitignore inside a git repository by default; a tool call can pass include_ignored: true to search gitignored files too. The in-house fallback has no .gitignore support, so it searches those files regardless. Both backends skip version-control internals and tool caches (.git, .venv, node_modules, __pycache__, …) and never follow symlinks out of the project. grep returns grouped per-file hunks with two lines of context by default (context_lines, 0-7); overlapping windows are merged and context lines count toward the result character budget.

write_file and edit_file modify files and are therefore marked destructive: like run_command they are full-mode only, and never offered or run under read_only. write_file creates a file or replaces a whole file (creating parent directories); edit_file replaces an exact span of an existing file, requiring the match to be unique unless replace_all is set. When an exact match fails, edit_file tries a bounded replacer chain (line-trimmed, block-anchor, whitespace-normalised, indentation-flexible, context-aware) before refusing, and reports which matcher fired. Both tools write atomically and preserve the file’s mode, line endings and BOM; read_file accepts line_numbers: false to return raw text suitable for an edit_file old_string. See ADR-0039.

The file tools are also defensive about missing or over-filtered input. list_files returns the unfiltered listing (with a note) when a pattern / include_* filter matches nothing but the directory is not empty, and read_file / edit_file return the nearest existing directory’s entries (nearby) plus a note when the target is missing. This keeps an empty filtered result from being mistaken for an empty directory, or a typo for a missing project.

web_search performs web discovery over a keyless-first provider pool (Tavily, Exa, Parallel and Firecrawl hosted MCP endpoints need no API key; Brave and Serper are used only when their key is set). It is read-only and open-world. Results are normalised to title, url, snippet, published and score, with the serving provider named in the response, and a rate-limited or failing provider falls through to the next. A key only raises that provider’s limits; keys are optional and read from the process environment (see docs/install.rst). See ADR-0048.

The bundled tools server

Klea ships a set of common tools (web fetch/search, file list/read/search/edit, download) as a shared MCP server in klea_utils.mcp.server. The individual bundled tools are indexed on the Tools page. Applications auto-launch it as a stdio subprocess by default, so users get the common tools with no extra setup; the same server can be run standalone over HTTP via the klea-mcp CLI for remote deployments.

Whether the bundled server is used, and which of its tools are exposed, is configured under general.bundled_tools:

{
    "general": {
        "bundled_tools": {
            "enabled": true,
            "include_tags": ["web"],
            "exclude_tags": ["download"]
        }
    }
}

The agent enables the bundled server by default (batteries included); the RAG leaves it disabled by default, since each RAG deployment is domain specific and should wire in only the tools it needs.

How Klea uses the tools

At startup, the graph connects to each configured MCP server, lists its tools, and stores per-domain metadata. The tool binder/picker then completes the planned calls (agent) or selects the tools relevant to the current query (RAG), and the calls are dispatched during graph execution. When several MCP servers are configured, fastmcp prefixes tool names with the server name (e.g. NeuroML_list_files) so tools from different servers stay distinct; Klea keeps these prefixed names unchanged.

Each dispatched call also has a wall-clock backstop so a hung tool cannot stall the graph: the default is 900 seconds, overridable with the KLEA_TOOL_CALL_TIMEOUT environment variable (0 disables it). A call that exceeds it returns a non-halting error and is cancelled; tools should still enforce their own, tighter timeouts.

Tool access levels

Klea can restrict which tools may be invoked. The level is either full (every tool) or read_only (only tools explicitly annotated readOnlyHint: true and not destructiveHint: true; a tool with no annotation is not permitted). Disallowed tools are never shown to the model, and a call to one is rejected before it reaches the MCP server with a non-halting error the model can adapt to. See ADR-0037 for the design and its trust limits.

The default level is set in the app config:

{
    "general": {
        "access_level": "read_only"
    }
}

The agent defaults to full and accepts a per-request access_level override in the chat API; the RAG is fixed at read_only (it only retrieves). In the agent’s web UI the level is a per-chat selector in the status pane (next to the operating mode), so it can be changed without editing the request payload; the effective level is projected back through the graph’s context event.

Because annotations are self-reported, tools that declare nothing are hidden under read_only. A deployment can declare the capability of a trusted tool explicitly, which takes precedence over its annotations:

{
    "general": {
        "tool_access": {
            "my_server_search": {"read_only": true},
            "my_server_fetch": {"read_only": false, "destructive": true}
        }
    }
}

This is a least-privilege guard for trustworthy tools, not a sandbox: a server that misreports its annotations cannot be confined this way. For servers Klea does not author, run them under OS-level isolation (see the MCP permissions notes in the development documentation).

Path permissions

The agent gates filesystem paths before a tool call is dispatched. A call that would touch a path outside the project directory (the directory the server runs in), or a sensitive file inside it (env files, private keys, cloud config), pauses the run and asks the user, per path, to allow it once, allow it for the session, or deny it. The ask is shown in the web UI and terminal REPL; a session approval is remembered for the chat, and a denied path returns a non-halting error so the run continues. Path discovery is layered – a tool’s declared checkpaths, the paths the tool binder/picker declares, and heuristics (including the tokens of a shell command) – with each request labelled by its confidence. The set of sensitive filenames can be extended with the comma-separated KLEA_SENSITIVE_PATTERNS environment variable.

This is a consent mechanism, not a sandbox: a tool can ignore its arguments and touch any path at runtime, and a standalone klea-mcp server used by a non-Klea client has no gate. See ADR-0007 for the design and its limits.

See also