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; andthe NeuroML server in the
mcp_pkgpackage (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) orweb(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
Writing MCP tools – conventions for authoring Klea’s own MCP tools