MCP utilities¶
Shared machinery for building MCP servers and clients used by Klea apps: metadata schemas, tool registration, tool access levels, an httpx session lifespan, path permission (discovery, the sensitive-file matcher, and the interactive-approval payload/schema), and the reusable bundled tools server.
Schemas¶
Schemas shared by MCP servers and clients.
File: klea_utils/mcp/schemas.py
Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
- class klea_utils.mcp.schemas.ToolCallSchema(*, tool: str = '', args: dict[str, ~typing.Any]=<factory>, reason: str = '', paths: list[str] = <factory>, step: int = 0)[source]¶
Bases:
BaseModel- model_config = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class klea_utils.mcp.schemas.ToolCallsSchema(*, tool_calls: list[ToolCallSchema] = <factory>)[source]¶
Bases:
BaseModel- model_config = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class klea_utils.mcp.schemas.ToolInfo(*, description: str | None = None, short_description: str | None = None, input_schema: dict[str, Any] | None = None, title: str | None = None, tags: set[str] | None = None, checkpaths: list[str] | None = None, meta: dict[str, Any] | None = None, read_only: bool | None = None, destructive: bool | None = None, idempotent: bool | None = None, open_world: bool | None = None)[source]¶
Bases:
BaseModelMetadata used to describe an MCP tool to clients and models.
The
read_only/destructive/idempotent/open_worldfields map 1:1 to the standard MCPToolAnnotationshints (readOnlyHint/destructiveHint/idempotentHint/openWorldHint); see https://fastmcp.wiki/en/servers/tools#mcp-annotations for what each hint means and how a client is expected to act on it.klea_utils.mcp.registry.register_tools()folds them onto the registered tool.- model_config = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
Errors¶
Custom error classes for the MCP tooling.
File: klea_utils/mcp/errors.py
Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
- exception klea_utils.mcp.errors.DocumentConversionError[source]¶
Bases:
ExceptionRaised when a document file cannot be converted to text.
Carries a user-facing message that tools report through their
errorresult field.
- exception klea_utils.mcp.errors.FileEditError[source]¶
Bases:
ExceptionRaised when a file cannot be read or written by the editing tools.
Carries a user-facing message that the write/edit tool implementations report through their
errorresult field (e.g. a missing file, a binary or non-UTF-8 file, or an atomic-write failure).
- exception klea_utils.mcp.errors.PermissionDeniedError[source]¶
Bases:
PermissionErrorRaised when a tool is denied access to a path.
Tool registration¶
Shared MCP tool registration helpers.
File: klea_utils/mcp/registry.py
Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
- klea_utils.mcp.registry.register_tools(mcp: fastmcp.FastMCP, modules: list[ModuleType])[source]¶
Register tools from the given modules.
A function is registered as a tool when it is decorated with
tool_meta()(which attachesToolInfometadata). The function name is used as the tool name. Helper functions in the same module that are not decorated are ignored (logged at debug level, so a forgotten decoration is easy to spot). Only functions defined in the given module are registered, so an imported decorated function is not picked up accidentally.Also installs
_DropNullOptionalsMiddleware, which tolerates weak models sending explicitnullfor optional arguments (treated as “use the default”).- Parameters:
mcp – FastMCP server to register the tools on.
modules – list of modules with tool function definitions
- klea_utils.mcp.registry.tool_meta(metadata: ToolInfo)[source]¶
Decorator that attaches
ToolInfometadata to a tool function.Usage:
@tool_meta(ToolInfo(tags={"bundled", "web"})) async def web_fetch(ctx: Context, url: str, ...): ...
The metadata is read by
register_tools()when the function is registered on a FastMCP server (it setsdescription,title,tags, andmetaon the tool if provided). A function is only registered as a tool when it carries this decoration; the function name is used as the tool name.- Parameters:
metadata –
ToolInfoto attach to the decorated function.- Returns:
The decorated function, unchanged, with
_tool_metaset.
Tool call dispatch¶
Client-side MCP tool-call dispatch with permission gating.
File: klea_utils/mcp/dispatch.py
Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
- klea_utils.mcp.dispatch.DEFAULT_TOOL_CALL_TIMEOUT_SECONDS = 900.0¶
Default per-call wall-clock cap for tool calls. A generous backstop that only fires when a tool hangs; individual tools own their semantic timeouts (e.g.
run_command), so raise this above any tool’s own maximum.
- klea_utils.mcp.dispatch.PermissionResolver¶
Resolves detected outside-path requests to allow/deny decisions. The app supplies one that pauses the run for user approval (
interrupt, ADR-0046); with no resolver (the default) every request is denied.alias of
Callable[[list[PathRequest]],PermissionResolution]
- klea_utils.mcp.dispatch.TOOL_CALL_TIMEOUT_ENV_VAR = 'KLEA_TOOL_CALL_TIMEOUT'¶
Process environment variable overriding the backstop (seconds). A value of
0or less disables it.
- async klea_utils.mcp.dispatch.dispatch_tool_calls(mcp_client: Any, tool_calls: Sequence[ToolCallSchema | tuple[str, Mapping[str, Any]]], tool_infos: dict[str, ToolInfo] | None = None, project_root: str | None = None, access_level: Literal['read_only', 'full'] = 'full', call_timeout: float | None = None, *, allowed_dirs: Sequence[str] | None = None, allowed_files: Sequence[str] | None = None, permission_resolver: Callable[[list[PathRequest]], PermissionResolution] | None = None, session_id: str | None = None, mcp_session_pool: Any | None = None, client_context: ClientContext | None = None, meta: Mapping[str, Any] | None = None) list[fastmcp.client.client.CallToolResult][source]¶
Gate and dispatch tool calls against an MCP server.
For each call a gate runs before it reaches the server, each producing a synthetic non-halting error when rejected:
an invalid tool name (empty, or – when tool_infos is given – not in the disclosed catalogue) is rejected without contacting the server, so a hallucinated name cannot be dispatched; and
the permission gate (
klea_utils.mcp.path_detect.detect_path_requests()), which finds the outside-project paths a call would touch (declaredcheckpaths, picker-declared paths, and heuristics); permission_resolver turns those into allow/deny decisions (the app pauses for user approval) and any request left unapproved blocks the call; andthe tool access level (ADR-0037): a tool the level does not permit (
read_onlypermits only explicitly read-only, non-destructive tools) is rejected bycheck_tool_access().
Allowed calls are dispatched in parallel, and the returned list stays aligned with the input tool_calls order.
- Parameters:
mcp_client – MCP client used for
call_tool. Its reentrant context is entered/exited by this helper. Typed asAnybecausefastmcp.Client.call_toolhas a complex signature (optionalarguments, keyword-onlyraise_on_error,CallToolResult | ToolTaskreturn) that a structural protocol would not cleanly match; tests substitute a fake implementing the subset used here.tool_calls – The calls to invoke, as
ToolCallSchema(so the picker’s declaredpathstravel) or legacy(name, args)pairs.tool_infos – Mapping of tool name to its
ToolInfo, carrying both the annotatedread_only/destructivecapability and (inmeta) thecheckpathsdeclarations.Nonedisables both gates (backward compatible); forread_onlya tool missing from the map is denied (fail-closed).project_root – Boundary directory for the path gate. Defaults to the current working directory.
access_level – Active tool access level (ADR-0037).
call_timeout – Per-call wall-clock timeout in seconds, as a backstop against a hung tool.
None(the default) resolves fromTOOL_CALL_TIMEOUT_ENV_VAR/DEFAULT_TOOL_CALL_TIMEOUT_SECONDS;0or less disables the backstop. A timed-out call becomes a synthetic non-halting error.allowed_dirs – Directories already approved for the session (
BaseGraphSchema.allowed_dirs), checked alongside project_root.allowed_files – Sensitive files already approved for the session (
BaseGraphSchema.allowed_files).permission_resolver – Called with the detected requests before dispatch; returns a
PermissionResolution. The app supplies one that pauses for user approval; when it isNonethe outside paths are denied (the pre-ADR-0007 behaviour) and the sensitive second round is not run.session_id – Chat-thread identifier used to select a pooled MCP client for this dispatch. When given together with mcp_session_pool the per-session client from
mcp_session_pool.get(session_id, client_context)is used instead of mcp_client, so concurrent chats do not share one MCP session.None(the default) keeps the shared mcp_client, which is what tests and non-pooled callers use.mcp_session_pool – Optional session pool exposing
get(session_id, client_context). Ignored when session_id isNone.client_context – Optional client-supplied environment forwarded to the per-session pooled client. Ignored without a pool/session id; the shared mcp_client always uses its own spawn configuration.
meta – Optional metadata forwarded to
client.call_toolas itsmetaargument. Only whether a value was supplied is logged, never the value itself.
- Returns:
One
CallToolResultper input call, in input order.
- klea_utils.mcp.dispatch.resource_key(call: ToolCallSchema, tool_infos: dict[str, ToolInfo] | None) frozenset[str][source]¶
Return the resource identifiers a tool call touches (ADR-0041).
Best-effort and deterministic: reads the tool’s declared
checkpaths(fromToolInfo.checkpathsor the foldedToolInfo.meta['checkpaths']) and normalises the string value of each declared argument lexically withos.path.normpath. The caller uses this to serialise calls that share a resource, so a missed dependency edge cannot cause a lost update.An empty set means no identifiable resource: the tool is unknown, declares no
checkpaths, or the declared argument is absent/not a string. Such a call is left to run concurrently.- Parameters:
call – The bound tool call.
tool_infos – Mapping of tool name to
ToolInfo, orNone.
- Returns:
A frozenset of normalised resource identifiers (possibly empty).
- klea_utils.mcp.dispatch.tool_call_timeout_seconds() float | None[source]¶
Return the per-call tool timeout in seconds, or
Nonewhen disabled.Reads
TOOL_CALL_TIMEOUT_ENV_VAR; an unset, non-numeric, or non-finite value falls back toDEFAULT_TOOL_CALL_TIMEOUT_SECONDS, while a non-positive value disables the backstop.- Returns:
The timeout ceiling, or
Nonewhen disabled.
Path permissions¶
Layered path discovery, the sensitive-file matcher, and the payload/response
schema for the interactive allow / deny approval loop (ADR-0007). The gate
runs in klea_utils.mcp.dispatch before a tool call reaches the server.
Layered path discovery for the client-side tool permission gate (ADR-0007).
The gate needs to know which filesystem paths a tool call would touch before
it dispatches. Declared checkpaths alone only cover Klea-authored tools,
so discovery is layered and tool-agnostic:
declared– the tool’s declaredcheckpathsargument values;expected– paths the tools picker declared for the call;guess– heuristic: argument names that look path-ish, plus path-like values (a shell command string is split withshlex.split()first).
Only paths that resolve outside the permitted roots (project_root plus any
session-approved allowed_dirs) become PathRequest objects. This
is advisory: a tool can ignore its arguments and touch any path at runtime;
only OS sandboxing confines it (ADR-0007).
See devdocs/system/mcp-permissions.md.
File: klea_utils/mcp/path_detect.py
Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
- klea_utils.mcp.path_detect.Confidence¶
Confidence that a discovered string really is a path, strongest first.
alias of
Literal[‘declared’, ‘expected’, ‘guess’]
- klea_utils.mcp.path_detect.MAX_PATH_DISPLAY_CHARS = 240¶
Display caps for permission prompts. The full path and approval key stay in the request; only the rendered labels are bounded so a pathological guessed string cannot flood the ask.
- class klea_utils.mcp.path_detect.PathRequest(*, tool: str, argument: str, raw: str, path: str, directory: str, confidence: ~typing.Literal['declared', 'expected', 'guess'], source: str, kind: ~typing.Literal['outside', 'sensitive'] = 'outside', argument_preview: str = '', covered_paths: list[str] = <factory>)[source]¶
Bases:
BaseModelOne path a tool call would touch that needs approval.
rawis the string as discovered;pathis its resolved location;directoryis the directory to approve (the path itself when it is a directory, else its parent).confidenceandsourcedescribe how it was found, so the prompt can show a guessed path differently from a declared one.kindisoutside(the path is outside the permitted roots) orsensitive(a credential-bearing file inside them).outsideis approved per directory,sensitiveper file (approval_key).argument_previewis a bounded, token-centred preview of the full argument value, used by the approval UI.pathanddirectoryremain full values for decisions and auditing; use the display helpers when rendering them.covered_pathslists every distinct resolved path that maps to this request’sapproval_key(all files in an approved directory, or the single sensitive file). It lets the ask show what one session approval covers.- property approval_key: str¶
Return the key a decision for this request is matched on.
The resolved file for a
sensitiverequest, the resolved directory for anoutsideone.
- model_config = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- klea_utils.mcp.path_detect.declared_checkpaths(tool_info: ToolInfo | None) list[str][source]¶
Return the tool’s declared path-argument names.
Reads
ToolInfo.checkpathsor the foldedToolInfo.meta['checkpaths'](set byregister_tools); empty when the tool declares none.- Parameters:
tool_info – Tool metadata, or
None.- Returns:
Declared path-argument names.
- klea_utils.mcp.path_detect.detect_path_requests(tool: str, arguments: Mapping[str, Any], tool_info: ToolInfo | None = None, *, project_root: str | PathLike | None = None, allowed_dirs: Sequence[str] | None = None, allowed_files: Sequence[str] | None = None, picker_paths: Sequence[str] | None = None, include_sensitive: bool = False) list[PathRequest][source]¶
Return the paths a tool call would touch that need approval.
Layered discovery (see the module docstring) unions declared
checkpathsvalues, picker-declared paths, and heuristics. Two rounds become requests: paths outside the permitted roots (kind="outside", approved per directory) and, when include_sensitive is set, sensitive files inside them (kind="sensitive", approved per file). Requests are de-duplicated byPathRequest.approval_key, keeping the strongest confidence tier.- Parameters:
tool – Tool name (recorded on each request).
arguments – The arguments the call would pass.
tool_info – Tool metadata (for
checkpaths), orNone.project_root – Boundary directory; defaults to the current directory.
allowed_dirs – Additional permitted directories (session approvals).
allowed_files – Sensitive files already approved for the session.
picker_paths – Paths the picker declared for this call, or
None.include_sensitive – Also request sensitive files inside the roots (the permission second round).
- Returns:
Approval requests, sorted by approval key.
- klea_utils.mcp.path_detect.platform_is_windows() bool[source]¶
Return whether arguments should be split with Windows quoting rules.
Backslashes are literal path separators on Windows, so POSIX
shlexescape processing (a\ b) must not be applied there. Kept as a function so tests can pin either behaviour on any platform.- Returns:
Truewhen running on Windows.
- klea_utils.mcp.path_detect.resolve_path_arguments(arguments: Mapping[str, Any], tool_info: ToolInfo | None, *, project_root: str | PathLike | None = None) dict[str, Any][source]¶
Return arguments with relative filesystem paths made absolute.
The tools run in the MCP subprocess, whose working directory is fixed at spawn, so a per-session project root cannot reach them via
chdir. Resolving relative path arguments to absolute against the session root here lets any local tool operate under the root regardless of its own cwd, and works for third-party tools too (it does not depend on the tool being Klea-authored).Rules:
a declared
checkpathsargument is always resolved (string orlist[str]value);other arguments are resolved heuristically when the argument name is path-ish (
klea_utils.tool_args.is_path_argument_name()) and the value is a single-token, non-URL/non-flag/non-glob string (multi-token shell command strings are never rewritten);working_directory(therun_commandconvention) defaults to the session root when unset;absolute paths and URLs are left unchanged.
- Parameters:
arguments – The call’s bound arguments.
tool_info – The tool’s metadata (for
checkpaths), orNone.project_root – Session root; defaults to the process working directory.
- Returns:
A copy of arguments with relative path values made absolute.
- klea_utils.mcp.path_detect.split_arguments(text: str, *, windows: bool | None = None) list[str][source]¶
Split text into shell-like argument tokens for the current platform.
On POSIX this is
shlex.split()unchanged: quotes group tokens, backslash escapes are honoured, and unbalanced quotes raiseValueError. On Windows the split is non-POSIX, so backslashes stay literal (C:\Users\me\a.txtis a single token); one enclosing quote pair is stripped from each token, and unbalanced quotes still raiseValueError.- Parameters:
text – The argument string to split.
windows – Force Windows (
True) or POSIX (False) rules; defaults toplatform_is_windows().
- Returns:
The argument tokens.
- klea_utils.mcp.path_detect.split_shell_tokens(text: str, *, windows: bool | None = None) list[str][source]¶
Split a shell command into tokens, separating control operators.
Unlike
split_arguments()(used for/commandarguments, where a;may be part of a value), this usesshlex’s punctuation mode so;,|,&,<and>are separate tokens instead of clinging to a path. Quotes still group a token, so a quoted path with spaces or shell characters stays intact. Comments are disabled (as inshlex.split()):#is a literal character, not a comment marker.whitespace_splitis enabled so=,:and#do not split a token; only whitespace and the punctuation characters do.- Parameters:
text – The shell command string.
windows – Force Windows (
True) or POSIX (False) rules; defaults toplatform_is_windows().
- Returns:
The argument and operator tokens.
- klea_utils.mcp.path_detect.truncate_middle(text: str, max_chars: int = 240) str[source]¶
Truncate text in the middle, keeping its start and end.
Used for display only. The start (root/directory) and the end (filename) are the useful parts of a path; the omitted span is replaced with an explicit marker so a pathological guessed value is recognisable.
- Parameters:
text – The text to bound.
max_chars – Maximum number of visible characters to keep.
- Returns:
The text unchanged when short, otherwise a middle-truncated preview with an omission marker.
Session-scoped path-approval helpers.
Pure helpers for the allowed_dirs / allowed_files state used by the
path gate (ADR-0007) and the /paths command (ADR-0047): resolve and
validate directory approvals, normalise the edited lists, find the entry that
covers a path, and format the user-facing listing. Command-specific parsing
and messages stay in the app command handler.
See devdocs/system/mcp-permissions.md and system/session-commands.md.
File: klea_utils/mcp/path_approval.py
Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
- klea_utils.mcp.path_approval.entry_matches_path(entry: str, resolved: Path) bool[source]¶
Return whether an approval entry resolves to resolved.
- klea_utils.mcp.path_approval.find_covering_dir(allowed_dirs: list[str], resolved: Path) str | None[source]¶
Return the allowed directory that covers resolved, if any.
- klea_utils.mcp.path_approval.find_matching_entry(entries: list[str], resolved: Path) str | None[source]¶
Return the exact approval entry matching resolved, if any.
- klea_utils.mcp.path_approval.format_allowed_paths(project_root: str | PathLike | None, allowed_dirs: list[str], allowed_files: list[str]) str[source]¶
Return the user-facing permitted-path listing.
- klea_utils.mcp.path_approval.is_relative_to(path: Path, parent: Path) bool[source]¶
Return whether path is inside parent.
- klea_utils.mcp.path_approval.normalise_allowed_dirs(project_root: str | PathLike | None, values: list[Any]) tuple[list[str], list[str]][source]¶
Validate edited directory values.
- Parameters:
project_root – The session project root.
values – The submitted directory values.
- Returns:
(accepted, ignored); ignored holds the validation errors.
- klea_utils.mcp.path_approval.normalise_allowed_files(current: list[str], values: list[Any]) list[str][source]¶
Keep only existing sensitive-file approvals that remain selected.
The editor is additive-disabled for sensitive files: new values are ignored, removals are honoured.
- Parameters:
current – The current
allowed_filesentries.values – The submitted values.
- Returns:
The remaining entries, in their original order.
- klea_utils.mcp.path_approval.normalise_directory(project_root: str | PathLike | None, value: str | PathLike) tuple[str | None, str][source]¶
Validate one directory approval.
The directory must exist, must not be inside the project root (which is always permitted), and is returned resolved.
- Parameters:
project_root – The session project root.
value – The user-supplied directory.
- Returns:
(resolved, "")on success, or(None, error).
- klea_utils.mcp.path_approval.resolve_project_root(project_root: str | PathLike | None = None) Path[source]¶
Return the resolved session project root.
Falls back to the current directory when project_root is unset or cannot be resolved.
- Parameters:
project_root – The session root, or
None.- Returns:
A resolved directory path.
- klea_utils.mcp.path_approval.resolve_user_path(value: str | PathLike, base: str | PathLike) Path[source]¶
Resolve value relative to base without requiring it to exist.
- Parameters:
value – The user-supplied path.
base – The directory relative paths resolve against.
- Returns:
A resolved path.
Sensitive-file matcher for the client-side permission gate (ADR-0007).
The path-boundary check (path_detect) only asks about paths outside the
permitted roots. This module is the second round: a curated, best-effort
matcher for files that commonly hold credentials (env files, private keys,
cloud config) so the gate can ask before a tool reads one that is inside the
project root.
It is advisory: a renamed secret, a secret inline in source, or a directory
scan (grep -r) can slip past a filename matcher. Operators can extend the
list with the comma-separated KLEA_SENSITIVE_PATTERNS env var (extra
fnmatch globs on the basename).
See devdocs/system/mcp-permissions.md.
File: klea_utils/mcp/sensitive.py
Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
- klea_utils.mcp.sensitive.SENSITIVE_ENV_VAR = 'KLEA_SENSITIVE_PATTERNS'¶
Env var with extra comma-separated basename globs to treat as sensitive.
- klea_utils.mcp.sensitive.is_sensitive(path: str | PathLike) bool[source]¶
Return whether path is a well-known credential-bearing file.
Matches on the basename (exact names and globs), the suffix, and any directory in the path (
_SENSITIVE_DIRS), plus the operator’s extra patterns fromSENSITIVE_ENV_VAR.- Parameters:
path – Path to test (need not exist).
- Returns:
Truewhen the path looks sensitive.
HITL payload and response schema for interactive path approval (ADR-0007).
The client-side permission gate collects outside-project PathRequest
objects, pauses the run with an interrupt carrying
build_permission_payload(), and resolves the user’s per-path
allow/deny decision with resolve_decisions(). The interrupt mechanism
itself is the shared LangGraph one (ADR-0046); this module owns only the
permission-specific payload, response schema, and decision mapping.
See devdocs/system/mcp-permissions.md.
File: klea_utils/mcp/permission_hitl.py
Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
- klea_utils.mcp.permission_hitl.DEFAULT_QUESTION = 'The agent wants to access paths outside the project directory. Allow or deny each one.'¶
Default question shown with a permission ask.
- klea_utils.mcp.permission_hitl.Decision¶
allow for this dispatch only, allow for the session (persisted in
allowed_dirs), deny, or dismiss the request as “not a path” (a heuristic false positive; the call proceeds for this dispatch and nothing is remembered).- Type:
A per-path decision
alias of
Literal[‘once’, ‘session’, ‘deny’, ‘not_a_path’]
- class klea_utils.mcp.permission_hitl.PathDecision(*, key: str, decision: Literal['once', 'session', 'deny', 'not_a_path'] = 'deny')[source]¶
Bases:
BaseModelA user’s decision for one requested path.
keymatches a request’sapproval_key(the directory for anoutsiderequest, the file for asensitiveone);decisionisonce(this dispatch),session(also persisted for the thread),deny, ornot_a_path(dismiss a guessed false positive for this dispatch; the call proceeds).- model_config = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class klea_utils.mcp.permission_hitl.PermissionResolution(*, allowed_now: list[str] = <factory>, allowed_session: list[str] = <factory>, denied: list[str] = <factory>, not_a_path: list[str] = <factory>)[source]¶
Bases:
BaseModelResolved decisions for one permission ask.
Fields hold approval keys (a directory for an
outsiderequest, a file for asensitiveone).allowed_nowis allowed for this dispatch only;allowed_sessionis also persisted for the thread;deniedis refused (the gate turns each into a non-halting denial);not_a_pathis dismissed for this dispatch only – the request was a heuristic false positive (the user is not approving it), so the call proceeds without gating it, and no memory is kept.- model_config = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class klea_utils.mcp.permission_hitl.PermissionResponse(*, action: Literal['answer', 'cancel']='answer', decisions: list[PathDecision] = <factory>)[source]¶
Bases:
BaseModelResume payload for a permission ask (ADR-0007).
action="cancel"denies every request.action="answer"carries onePathDecisionper requested directory; a requested directory with no decision is denied (fail closed).- model_config = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- klea_utils.mcp.permission_hitl.build_permission_payload(requests: Sequence[PathRequest], *, question: str = '') dict[str, Any][source]¶
Build the interrupt payload for a list of path requests.
The payload is a plain mapping (the stream/interrupt convention):
kindis"permission",questionis the prompt text, andrequestsis the list of serializedPathRequestobjects for the client to render. Each request carries the fullpathanddirectoryfor decisions plus boundedpath_display,directory_display,raw_previewandargument_previewfields for the UI; the payload’srawvalue is capped toraw_preview.- Parameters:
requests – The outside-path requests to ask about.
question – Custom prompt text; defaults to
DEFAULT_QUESTION.
- Returns:
The interrupt payload dict.
- klea_utils.mcp.permission_hitl.resolve_decisions(requests: Sequence[PathRequest], response: Any) PermissionResolution[source]¶
Resolve a resume value into allow-now / allow-session / deny key sets.
Uses each request’s
approval_key(the directory for anoutsiderequest, the file for asensitiveone). A cancel, an invalid response, or a request with no matching decision denies that request (fail closed).- Parameters:
requests – The requests that were asked about.
response – The value returned by the interrupt (a
PermissionResponse, a raw mapping, orNone).
- Returns:
The
PermissionResolution.
Tool access levels¶
Tool access levels for MCP tool invocation (ADR-0037).
Classification is derived from the standard MCP ToolAnnotations
(readOnlyHint / destructiveHint) carried on ToolInfo, with an
optional operator override (ToolAccessOverride). Levels are
read_only (only explicitly read-only, non-destructive tools) and
full (every tool); an unannotated tool fails closed in read_only.
See devdocs/system/mcp-permissions.md and
devdocs/adr/0037-tool-access-levels.md.
File: klea_utils/mcp/access.py
Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
- klea_utils.mcp.access.AccessLevel¶
Tool invocation access levels (ADR-0037).
alias of
Literal[‘read_only’, ‘full’]
- klea_utils.mcp.access.DEFAULT_ACCESS_LEVEL: Literal['read_only', 'full'] = 'full'¶
every tool is permitted (the pre-ADR-0037 behaviour).
- Type:
Default level
- class klea_utils.mcp.access.ToolAccessOverride(*, read_only: bool | None = None, destructive: bool | None = None)[source]¶
Bases:
BaseModelOperator declaration of a tool’s capability (ADR-0037).
Configured per tool under
general.tool_accessand applied over the tool’s own MCP annotations. Either field may be leftNoneto leave the annotated value unchanged; the operator who writes the config is the trust root for the declaration.- model_config = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- klea_utils.mcp.access.check_tool_access(tool: str, read_only: bool | None, destructive: bool | None, access_level: Literal['read_only', 'full']) str | None[source]¶
Return a denial message when tool is not permitted, else
None.Mirrors
tool_permits()but returns a human-readable message for the non-halting synthetic error result the caller produces (the ADR-0007 error contract) instead of raising.- Parameters:
tool – Tool name, used in the message.
read_only – Tool’s read-only capability.
destructive – Tool’s destructive capability.
access_level – The active access level.
- Returns:
A denial message, or
Nonewhen the tool is permitted.
- klea_utils.mcp.access.filter_tools_info(tools_info: dict[str, dict[str, ToolInfo]], access_level: Literal['read_only', 'full']) dict[str, dict[str, ToolInfo]][source]¶
Return tools_info with tools disallowed by access_level removed.
Domains are preserved as keys even when all their tools are filtered out.
fullreturns the input unchanged; otherwise a new mapping is built.- Parameters:
tools_info – Per-domain
{name: ToolInfo}mapping.access_level – The active access level.
- Returns:
A filtered mapping (the input is not mutated).
- klea_utils.mcp.access.resolve_access_level(value: Any) Literal['read_only', 'full'][source]¶
Coerce a value to a valid access level, defaulting to
full.Used for untrusted inputs (env var strings); an unrecognised value is logged and replaced with
DEFAULT_ACCESS_LEVEL.- Parameters:
value – Candidate value (e.g. from a request component or env).
- Returns:
The matching level, or
DEFAULT_ACCESS_LEVEL.
- klea_utils.mcp.access.resolve_capability(read_only: bool | None, destructive: bool | None, override: ToolAccessOverride | None = None) tuple[bool | None, bool | None][source]¶
Apply an operator override over a tool’s annotated capability.
Precedence is explicit
tool_accessoverride > tool annotation >None(which fails closed downstream). ANoneoverride field leaves the annotated value unchanged.- Parameters:
read_only – Annotated read-only hint, or
None.destructive – Annotated destructive hint, or
None.override – Operator override, or
None.
- Returns:
The effective
(read_only, destructive)pair.
- klea_utils.mcp.access.tool_permits(read_only: bool | None, destructive: bool | None, access_level: Literal['read_only', 'full']) bool[source]¶
Return whether access_level permits invoking this tool.
fullpermits every tool.read_onlypermits a tool only when it is explicitly read-only and not destructive; an unannotated tool (None) is not permitted (fail-closed).- Parameters:
read_only – Tool’s read-only capability.
destructive – Tool’s destructive capability.
access_level – The active access level.
- Returns:
True when the tool may be invoked.
Strict-safe call schema¶
Strict-safe tool-call schema builder for the tools picker.
Builds the picker’s structured-output schema dynamically from the disclosed
tools, so each tool’s parameters become real, typed fields instead of a
free-form args object. A free-form object is closed by every
provider’s strict structured-output mode (additionalProperties: false),
which silently forces args to {}; a discriminated union of per-tool
call models avoids that and enforces each tool’s required parameters.
The generated schema is:
ToolPickerOutput-{tool_calls: list[ToolCall]}ToolCall-{step, reason, call: <union>}(nested)one
<tool>_callmodel per tool -{tool: Literal["<name>"], <params>}a
NoToolbranch -{tool: Literal["no_tool"], reason}for the “no suggested tool can carry out this step” signal.
Type mapping (strict-safe): string/integer/number/boolean
map directly; anyOf (including T | null) maps to a union; arrays of
supported types map to list[...]; object/unknown parameters fall back to
a JSON-encoded str (documented). No generated field is an open object.
File: klea_utils/mcp/call_schema.py
Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
- klea_utils.mcp.call_schema.NO_TOOL_TAG = 'no_tool'¶
Discriminator tag for the “no suggested tool fits” branch.
_update_statemaps it to the picker’s empty-tool failure signal.
- klea_utils.mcp.call_schema.build_tool_call_schema(tools: Mapping[str, ToolInfo]) type[BaseModel][source]¶
Build the picker’s structured-output schema for a tool set.
The schema is a nested discriminated union over the given tools plus a
NoToolbranch. Tool order is deterministic (sorted by name) so the serialized schema - and therefore the prompt’sOutput schemablock - is stable for a given tool set (prompt caching).- Parameters:
tools – The disclosed tools (
{name: ToolInfo}), already filtered by access level and, for RAG, by the run’s domains.- Returns:
A pydantic model class with a
tool_callsfield.
Privilege guard¶
Privilege guard for Klea-authored MCP tools.
Klea-authored tools (the bundled server and the NeuroML server) refuse to run when the server process has root privileges, unless the operator explicitly opts in via an environment variable. This is defense-in-depth for deployments that accidentally run the server as uid 0 (including containers, which default to root): a tool running as root can read/write/execute far beyond the project. Third-party MCP servers do not use this guard; their privileges are the operator’s choice (ADR-0007 trust model). See ADR-0038.
File: klea_utils/mcp/privilege.py
Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
- klea_utils.mcp.privilege.ALLOW_ROOT_ENV_VAR = 'KLEA_ALLOW_ROOT_TOOLS'¶
Process environment variable that allows Klea-authored tools to run as root. Must be truthy (
1/true/yes/on); anything else (or unset) refuses root execution. A process env var, so it must reach the MCP server subprocess (it is inherited from the app’s environment).
- klea_utils.mcp.privilege.allow_root_tools() bool[source]¶
Return whether root execution is explicitly allowed.
Reads
ALLOW_ROOT_ENV_VAR; only a truthy value (1/true/yes/on, case-insensitive) opts in.- Returns:
True when the override is set to a truthy value.
- klea_utils.mcp.privilege.root_denial_message() str[source]¶
Return the non-halting denial message for a root-refused tool call.
- Returns:
A message naming the override variable.
- klea_utils.mcp.privilege.running_as_root() bool[source]¶
Return whether the current process has root (effective uid 0).
Windows-safe: non-POSIX systems have no
geteuidand are treated as non-root.- Returns:
True when
os.geteuid() == 0.
- klea_utils.mcp.privilege.warn_if_root(logger: Logger) None[source]¶
Log a warning when the current process runs as root.
Called once at app/server startup so operators are aware that tools will either be refused or execute with full root privileges.
- Parameters:
logger – Logger to emit the warning on.
Tool results¶
Helper to map Klea dict results with an error field to MCP isError.
File: klea_utils/mcp/tool_result.py
Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
- klea_utils.mcp.tool_result.to_result(result: dict[str, Any]) fastmcp.tools.ToolResult[source]¶
Return a
ToolResultwithis_errorderived from theerrorfield.Klea tool implementations are framework-agnostic and signal failure via a non-empty
error(orErrorfor legacy repository tools) string in their returned dict. The MCP spec requires such failures to be reported asisError: truein theCallToolResult(seemcp/types.py:CallToolResultand the spec sectionserver/tools#Error Handling); FastMCP only sets this when the tool explicitly returnsToolResult(is_error=True)or raisesToolError(seefastmcp/tools/base.py:convert_resultandmcp/server/lowlevel/server.py:_make_error_result).This helper bridges the two: it preserves the dict as
structured_content(so LLM remediation keeps full context) while markingis_errorwhen theerror/Errorfield is truthy. It intentionally preserves the structured payload rather than raisingToolError, which would strip it.- Parameters:
result – Tool return dict containing at least an
errorkey; legacy tools may useError(capitalised).- Returns:
ToolResultwithis_errorreflecting the error field.
HTTP session lifespan¶
Shared FastMCP lifespan helpers.
See devdocs/adr/0005-httpx-single-stack.md.
File: klea_utils/mcp/lifespan.py
Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
- klea_utils.mcp.lifespan.make_http_session_lifespan(session_key: str = 'http_session')[source]¶
Create a FastMCP lifespan that provides a shared httpx session.
Tools that need an HTTP session (e.g. klea_utils.mcp.tool_impls.web_fetch) read it from
ctx.lifespan_context[<session_key>]in their MCP wrapper. Lifespans are composable with the|operator.- Parameters:
session_key – Lifespan context key under which the session is stored.
- Returns:
A FastMCP
@lifespan-decorated function.
Paper search sources¶
Framework-agnostic sources that search scholarly APIs and return normalised
PaperRecord objects.
klea_utils.mcp.tool_impls.papers.search.PaperSearchResolver rotates
and falls back across them, and the search_papers bundled tool wraps it.
Academic paper search across several scholarly APIs.
Peer reviewed sources are preferred. The general sources (OpenAlex,
Crossref, and Semantic Scholar when SEMANTIC_SCHOLAR_API_KEY is set) are
queried in round-robin order across calls, like
klea_utils.biblio.doi.DoiResolver, so no single API takes every
request; when the primary is rate limited, fails or finds nothing, the next
one is tried. Life sciences searches try Europe PMC, then PubMed, before the
general sources. Preprints (arXiv, Europe PMC preprints) are only searched
when asked for, and are listed after the peer reviewed results.
File: klea_utils/mcp/tool_impls/papers/search.py
Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
- class klea_utils.mcp.tool_impls.papers.search.PaperSearchResolver(rotation: Iterator[int] | None = None)[source]¶
Bases:
objectSearch academic papers across a pool of scholarly APIs.
Peer reviewed sources are preferred. The general sources (OpenAlex, Crossref, and Semantic Scholar when
SEMANTIC_SCHOLAR_API_KEYis set) are queried in round-robin order across calls, likeklea_utils.biblio.doi.DoiResolver, so no single API takes every request; when the primary is rate limited, fails or finds nothing, the next one is tried. Life sciences searches try Europe PMC, then PubMed, before the general sources. Preprints (arXiv, Europe PMC preprints) are only searched when asked for, and are listed after the peer reviewed results.- async search(session: SessionLike | None, query: str, domain: str = 'general', preprints: bool = False, max_results: int = 10) dict[str, Any][source]¶
Search academic papers by keywords.
- Parameters:
session – HTTP session to use for the requests.
query – Keywords to search for.
domain –
"general"or"life-sciences".preprints – Also search preprints, listed after peer reviewed results.
max_results – Maximum results from the peer reviewed sources, and separately from the preprint sources.
- Returns:
Dictionary with query, domain, preprints, results, error and note.
notelists sources that failed, if any.
- async klea_utils.mcp.tool_impls.papers.search.search_papers(session: SessionLike | None, query: str, domain: str = 'general', preprints: bool = False, max_results: int = 10) dict[str, Any][source]¶
Search academic papers by keywords.
Thin wrapper over the shared
PaperSearchResolver; this is the entry point the bundled tool calls.- Parameters:
session – HTTP session to use for the requests.
query – Keywords to search for.
domain –
"general"or"life-sciences".preprints – Also search preprints, listed after peer reviewed results.
max_results – Maximum results from the peer reviewed sources, and separately from the preprint sources.
- Returns:
Dictionary with query, domain, preprints, results, error and note.
notelists sources that failed, if any.
Normalised paper record returned by every paper search source.
File: klea_utils/mcp/tool_impls/papers/record.py
Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
- klea_utils.mcp.tool_impls.papers.record.MAX_ABSTRACT_CHARS = 2000¶
Abstracts longer than this are cut, so a page of results stays small enough for the model’s context.
- class klea_utils.mcp.tool_impls.papers.record.PaperRecord(*, title: str | None = None, authors: list[str] = [], year: int | None = None, journal: str | None = None, abstract: str | None = None, doi: str | None = None, url: str | None = None, peer_reviewed: bool = True, source: str = '')[source]¶
Bases:
BiblioRecordA search hit: the DOI resolver’s record plus search-specific fields.
- model_config = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- klea_utils.mcp.tool_impls.papers.record.clean_doi(raw: str | None) str | None[source]¶
Strip a
https://doi.org/ordoi:prefix from an API-provided DOI.Unlike
klea_utils.biblio.normalize.normalize_doi()this keeps everything after the prefix: DOI suffixes may contain/(for example10.1088/0957-4484/24/38/383001).
- klea_utils.mcp.tool_impls.papers.record.from_biblio(record: BiblioRecord, *, url: str | None, peer_reviewed: bool, source: str, doi: str | None = None) PaperRecord[source]¶
Build a
PaperRecordfrom aBiblioRecord.- Parameters:
record – Record produced by one of the DOI resolver normalisers.
url – Link to the paper’s landing page.
peer_reviewed – Whether the paper is peer reviewed.
source – Name of the service that returned the record.
doi – DOI to use instead of
record.doi.
- klea_utils.mcp.tool_impls.papers.record.is_preprint(record: BiblioRecord) bool[source]¶
Whether record comes from a preprint server, by its venue.
Some sources tag preprints as journal articles (Semantic Scholar, and SSRN in Crossref), so their peer reviewed filters alone are not enough. The DOI is not checked: Semantic Scholar merges a preprint with its published version and can keep the preprint DOI, while the venue is then the journal.
- klea_utils.mcp.tool_impls.papers.record.truncate_abstract(abstract: str | None) str | None[source]¶
Cut abstract to
MAX_ABSTRACT_CHARS, marking the cut.
Errors for the academic paper search layer.
File: klea_utils/mcp/tool_impls/papers/errors.py
Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
- exception klea_utils.mcp.tool_impls.papers.errors.PaperSourceError[source]¶
Bases:
ExceptionRaised when a paper source cannot be queried (HTTP error, 429, bad body).
The search falls back to the next source when this is raised.
Shared HTTP helpers for the paper search sources.
File: klea_utils/mcp/tool_impls/papers/http.py
Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
- class klea_utils.mcp.tool_impls.papers.http.Throttle(min_interval: float)[source]¶
Bases:
objectKeep at least
min_intervalseconds between requests to one service.Used for services that ask for a gap between requests (arXiv, Semantic Scholar and PubMed); each module explains its own gap.
- async klea_utils.mcp.tool_impls.papers.http.get_json(session: SessionLike | None, url: str, params: dict[str, Any] | None = None, headers: dict[str, str] | None = None) Any[source]¶
GET url and return the decoded JSON body.
- Raises:
PaperSourceError – on any failure or an unreadable body.
- async klea_utils.mcp.tool_impls.papers.http.get_text(session: SessionLike | None, url: str, params: dict[str, Any] | None = None, headers: dict[str, str] | None = None) str[source]¶
GET url and return the body as text (for the XML APIs).
- Raises:
PaperSourceError – on any failure.
OpenAlex paper search source.
File: klea_utils/mcp/tool_impls/papers/openalex.py
Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
- klea_utils.mcp.tool_impls.papers.openalex.PEER_REVIEWED_FILTER = 'type:article,primary_location.source.type:journal'¶
OpenAlex search also returns preprints, datasets etc.; this keeps only journal articles.
- async klea_utils.mcp.tool_impls.papers.openalex.search(session: SessionLike | None, query: str, limit: int) list[PaperRecord][source]¶
Search OpenAlex for peer reviewed journal articles.
- Raises:
PaperSourceError – when the request fails or is rate limited.
Crossref paper search source.
File: klea_utils/mcp/tool_impls/papers/crossref.py
Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
- klea_utils.mcp.tool_impls.papers.crossref.PEER_REVIEWED_FILTER = 'type:journal-article'¶
Crossref search also returns preprints (“posted-content”), datasets etc.; this keeps journal articles.
- async klea_utils.mcp.tool_impls.papers.crossref.search(session: SessionLike | None, query: str, limit: int) list[PaperRecord][source]¶
Search Crossref for peer reviewed journal articles.
- Raises:
PaperSourceError – when the request fails or is rate limited.
Semantic Scholar paper search source.
The search endpoint answers HTTP 429 to nearly every request without an API
key, so this source is only used when SEMANTIC_SCHOLAR_API_KEY is set.
File: klea_utils/mcp/tool_impls/papers/semantic_scholar.py
Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
- klea_utils.mcp.tool_impls.papers.semantic_scholar.api_key() str | None[source]¶
Return the Semantic Scholar API key, or
Nonewhen unset.
- async klea_utils.mcp.tool_impls.papers.semantic_scholar.search(session: SessionLike | None, query: str, limit: int) list[PaperRecord][source]¶
Search Semantic Scholar for peer reviewed journal articles.
- Raises:
PaperSourceError – when no key is set, or the request fails or is rate limited.
Europe PMC paper search source (life sciences).
Europe PMC indexes PubMed (SRC:MED) and preprint servers such as bioRxiv,
medRxiv and Research Square (SRC:PPR), so one free, keyless API covers
both peer reviewed papers and preprints.
File: klea_utils/mcp/tool_impls/papers/europepmc.py
Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
- async klea_utils.mcp.tool_impls.papers.europepmc.search(session: SessionLike | None, query: str, limit: int, preprints: bool = False) list[PaperRecord][source]¶
Search Europe PMC for PubMed papers, or preprints when preprints.
- Raises:
PaperSourceError – when the request fails or is rate limited.
PubMed paper search source (life sciences), via NCBI E-utilities.
Used as the fallback for Europe PMC: esearch finds the PubMed IDs, then
efetch returns the records (with abstracts) as XML.
File: klea_utils/mcp/tool_impls/papers/pubmed.py
Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
- klea_utils.mcp.tool_impls.papers.pubmed.PEER_REVIEWED_FILTER = 'NOT preprint[pt]'¶
PubMed also indexes some preprints; this keeps only peer reviewed records.
- async klea_utils.mcp.tool_impls.papers.pubmed.search(session: SessionLike | None, query: str, limit: int) list[PaperRecord][source]¶
Search PubMed for peer reviewed papers.
- Raises:
PaperSourceError – when a request fails, is rate limited, or returns an unreadable body.
arXiv paper search source (general preprints).
File: klea_utils/mcp/tool_impls/papers/arxiv.py
Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
- async klea_utils.mcp.tool_impls.papers.arxiv.search(session: SessionLike | None, query: str, limit: int) list[PaperRecord][source]¶
Search arXiv preprints.
- Raises:
PaperSourceError – when the request fails, is rate limited, or the body is not valid Atom XML.
Repository sources¶
Framework-agnostic functions that list the versions and files of archival
repositories (GitHub, FigShare, DANDI Archive, BioModels). The returned
file lists carry direct download_url values that can be fed to
klea_utils.mcp.tool_impls.download_file.download_files() (or the
single-file download_file implementation), e.g. by wrapping them into
MCP tools.
Shared helpers for the repository source implementations.
File: klea_utils/mcp/tool_impls/repositories/sources.py
Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
- klea_utils.mcp.tool_impls.repositories.sources.REQUEST_TIMEOUT = Timeout(timeout=30.0)¶
Request timeout for the JSON API calls, in seconds.
Error classes for the repository source implementations.
File: klea_utils/mcp/tool_impls/repositories/errors.py
Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
- exception klea_utils.mcp.tool_impls.repositories.errors.RepositorySourceError[source]¶
Bases:
ExceptionRaised when a repository source cannot be queried.
Carries a user-facing message that the public functions report through their
errorresult field.
GitHub repository source implementation.
File: klea_utils/mcp/tool_impls/repositories/github.py
Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
- klea_utils.mcp.tool_impls.repositories.github.GITHUB_API_BASE = 'https://api.github.com/repos'¶
GitHub REST API base for the
/reposcollection.
- klea_utils.mcp.tool_impls.repositories.github.PAGE_SIZE = 100¶
Page size for the branches/tags listing endpoints.
- klea_utils.mcp.tool_impls.repositories.github.RAW_BASE = 'https://raw.githubusercontent.com'¶
Base URL for direct raw file downloads.
- async klea_utils.mcp.tool_impls.repositories.github.github_list_files(session: SessionLike | None, url: str, version: str | None = None) dict[str, Any][source]¶
List the files in a GitHub repository at a given version.
The
versionis a git branch or a tag. When a name exists as both a branch and a tag, a branch is assumed (git ref resolution precedence). Ifversionis omitted, the repository’s default branch is used.- Use when:
Getting the file list of a GitHub repository so files can be downloaded.
- Parameters:
url – GitHub repository URL (https://github.com/<owner>/<repo>).
version – Branch or tag to list. Defaults to the default branch.
- Returns:
Dictionary with source, url, version, files (path, name, download_url, size), and error.
- async klea_utils.mcp.tool_impls.repositories.github.github_list_versions(session: SessionLike | None, url: str) dict[str, Any][source]¶
List the available versions (branches and tags) of a GitHub repository.
A GitHub version is a git branch or a tag; both are merged into a single list. When a name exists as both a branch and a tag, it is listed once.
- Use when:
Discovering which branches/tags a GitHub repository offers before listing its files.
- Parameters:
url – GitHub repository URL (https://github.com/<owner>/<repo>).
- Returns:
Dictionary with source, url, versions, and an empty files list.
File: klea_utils/mcp/tool_impls/repositories/figshare.py
Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
FigShare API v2 base URL. Even for institutional FigShare instances (e.g.
rdr.ucl.ac.uk) the article IDs and the API endpoint are shared.
Cap on the number of pages fetched, to bound runaway loops.
Page size for the article files endpoint (the API maximum is 1000).
List the files of a FigShare article.
FigShare serves the same file list for every version of an article (the files endpoint is not versioned); the
versionargument is accepted for a uniform API but only labels the result. Whenversionis omitted, the article’s current version is reported.- Use when:
Getting the file list of a FigShare article so files can be downloaded.
- Parameters:
url – FigShare article URL (e.g. https://figshare.com/articles/dataset/<title>/<article_id>).
version – Version label for the result. Defaults to the article’s current version.
- Returns:
Dictionary with source, url, version, files (path, name, download_url, size), and error.
List the available versions of a FigShare article.
- Use when:
Discovering which versions a FigShare article offers before listing its files.
- Parameters:
url – FigShare article URL (e.g. https://figshare.com/articles/dataset/<title>/<article_id>).
- Returns:
Dictionary with source, url, versions, and an empty files list.
DANDI Archive repository source implementation.
File: klea_utils/mcp/tool_impls/repositories/dandi.py
Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
- klea_utils.mcp.tool_impls.repositories.dandi.DANDI_API_BASE = 'https://api.dandiarchive.org/api'¶
DANDI Archive REST API base URL.
- klea_utils.mcp.tool_impls.repositories.dandi.DANDI_HOSTS = ('dandiarchive.org', 'www.dandiarchive.org')¶
Hosts that serve DANDI Archive.
- klea_utils.mcp.tool_impls.repositories.dandi.DRAFT_VERSION = 'draft'¶
The default version label, used when none is specified.
- klea_utils.mcp.tool_impls.repositories.dandi.MAX_FILES = 10000¶
Cap on the total number of files collected, to bound runaway recursion.
- klea_utils.mcp.tool_impls.repositories.dandi.MAX_PAGES = 100¶
Cap on the number of pages fetched per folder, to bound runaway loops.
- klea_utils.mcp.tool_impls.repositories.dandi.PAGE_SIZE = 100¶
Page size for the
assets/pathsendpoint.
- async klea_utils.mcp.tool_impls.repositories.dandi.dandi_list_files(session: SessionLike | None, url: str, version: str | None = None) dict[str, Any][source]¶
List the files of a DANDI dandiset at a given version.
The file tree is walked recursively via the
assets/pathsendpoint: entries with anassetare files, entries without one are folders that are descended into. Whenversionis omitted, thedraftversion is used.Use when: - Getting the file list of a DANDI dandiset so files can be downloaded.
- Parameters:
url – DANDI dandiset URL (https://dandiarchive.org/dandiset/<id>).
version – Version to list (e.g.
draftor a published version). Defaults todraft.
- Returns:
Dictionary with source, url, version, files (path, name, download_url, size), and error.
- async klea_utils.mcp.tool_impls.repositories.dandi.dandi_list_versions(session: SessionLike | None, url: str) dict[str, Any][source]¶
List the available versions of a DANDI dandiset.
The list includes the working
draftversion as well as published versions.- Use when:
Discovering which versions a DANDI dandiset offers before listing its files.
- Parameters:
url – DANDI dandiset URL (https://dandiarchive.org/dandiset/<id>).
- Returns:
Dictionary with source, url, versions, and an empty files list.
BioModels repository source implementation.
File: klea_utils/mcp/tool_impls/repositories/biomodels.py
Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
- klea_utils.mcp.tool_impls.repositories.biomodels.BIOMODELS_API_BASE = 'https://www.biomodels.org'¶
Canonical BioModels API base. The legacy
www.ebi.ac.uk/biomodelshost redirects here, so all API calls go to this base regardless of the host used in the model URL.
- klea_utils.mcp.tool_impls.repositories.biomodels.BIOMODELS_HOSTS = ('biomodels.org', 'www.biomodels.org')¶
Hosts that serve BioModels model pages.
- async klea_utils.mcp.tool_impls.repositories.biomodels.biomodels_list_files(session: SessionLike | None, url: str, version: str | None = None) dict[str, Any][source]¶
List the files of a BioModels model at a given revision.
The file list combines the
mainandadditionalfile groups of the model record. Whenversionis omitted, the latest revision is used.Use when: - Getting the file list of a BioModels model so files can be downloaded.
- Parameters:
url – BioModels model URL (e.g. https://www.biomodels.org/MODEL0912160000).
version – Revision number (as a string) to list. Defaults to the latest revision.
- Returns:
Dictionary with source, url, version, files (path, name, download_url, size), and error.
- async klea_utils.mcp.tool_impls.repositories.biomodels.biomodels_list_versions(session: SessionLike | None, url: str) dict[str, Any][source]¶
List the available revisions (versions) of a BioModels model.
- Use when:
Discovering which revisions a BioModels model offers before listing its files.
- Parameters:
url – BioModels model URL (e.g. https://www.biomodels.org/MODEL0912160000).
- Returns:
Dictionary with source, url, versions, and an empty files list.
Bundled tools server¶
The bundled tools server (auto-launched by apps as a stdio subprocess and
exposed standalone via the klea-mcp CLI) and its configuration.
- class klea_utils.mcp.server.config.BundledToolsConfig(*, enabled: bool = True, include_tags: set[str] = <factory>, exclude_tags: set[str] = <factory>)[source]¶
Bases:
BaseModelConfiguration for the shared bundled tools server.
Read by the app orchestrators (
BaseLangGraph) to decide whether the bundled tools server is wired into the graph and, when it is, which of its tools are exposed. Tools are filtered by tag (seeklea_utils.mcp.server.bundled_toolsfor the bundled tag vocabulary); the tags are applied to the stdio config entry so fastmcp’sTransformingStdioMCPServerenforces them.Pydantic-only on purpose: config loads must not pull in fastmcp.
- model_config = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
Bundled tools server for Klea.
Provides the common Klea tools (web fetch, file list/read, download) as an
MCP server. Apps auto-launch this module over stdio (see
BaseLangGraph._bundled_server_config) so users get the common tools with
no extra setup; the same server can also be run standalone over HTTP via the
klea-mcp CLI for remote deployments.
Tool implementations live in klea_utils.mcp.tool_impls and the FastMCP
wrappers in klea_utils.mcp.server.bundled_tools; this module wires them
onto a FastMCP server.
File: klea_utils/mcp/server/bundled.py
Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
- klea_utils.mcp.server.bundled.bundle_server = 'KleaBundled'¶
The bundled FastMCP server instance. Apps embed this module as a stdio subprocess (
python -m klea_utils.mcp.server.bundled); tests and theklea-mcpCLI use it directly.
- klea_utils.mcp.server.bundled.main(transport: str = <typer.models.OptionInfo object>, port: int = <typer.models.OptionInfo object>) None[source]¶
Run the bundled tools server.
Accessed via the
klea-mcpentry point.--transport httpserves the same pre-registered tools over HTTP so a remote client (e.g. a RAG deployment that runs Klea and the bundled server on different hosts) can point itsmcp_serversconfig at this server’s URL.
FastMCP tool wrappers for the shared Klea bundled tools.
File: klea_utils/mcp/server/bundled_tools.py
Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
- klea_utils.mcp.server.bundled_tools.BUNDLED_TAG = 'bundled'¶
Common tags carried by every bundled tool, so “enable the common set” is a single include_tags: [“bundled”] in the app config.
- klea_utils.mcp.server.bundled_tools.STORES_TAG = 'stores'¶
Tag marking the tool as searching the deployment’s document stores.
- async klea_utils.mcp.server.bundled_tools.download_file(ctx: fastmcp.Context, url: Annotated[str, FieldInfo(annotation=NoneType, required=True, metadata=[MinLen(min_length=1)])], file_path: Annotated[str, FieldInfo(annotation=NoneType, required=True, metadata=[MinLen(min_length=1)])]) fastmcp.tools.ToolResult[source]¶
Download a URL to a local file.
Use this tool to fetch binary or text resources from the web and save them to disk for later reading or processing.
Use when: - Downloading a file such as a PDF, dataset, or archive. - Saving remote content locally before inspecting it.
Do not use for: - Reading a web page as text (use the web fetch tool instead).
Example: download_file(url=”https://example.com/paper.pdf”, file_path=”paper.pdf”)
- Parameters:
url – HTTP or HTTPS URL to download.
file_path – Destination path, relative to the working directory. Existing files are overwritten.
- Returns:
Dictionary with the saved path, or an error on failure.
- async klea_utils.mcp.server.bundled_tools.edit_file(path: Annotated[str, FieldInfo(annotation=NoneType, required=True, metadata=[MinLen(min_length=1)])], old_string: Annotated[str, FieldInfo(annotation=NoneType, required=True, description='Exact text to replace', metadata=[MinLen(min_length=1)])], new_string: Annotated[str, FieldInfo(annotation=NoneType, required=True, description='Replacement text')], replace_all: Annotated[bool, FieldInfo(annotation=NoneType, required=True, description='Replace every occurrence instead of requiring one')] = False) fastmcp.tools.ToolResult[source]¶
Replace an exact span of text in an existing file.
Use this tool for targeted changes. To supply
old_string, first read the file withread_file(line_numbers=false)so the text matches exactly, including whitespace and indentation.Use when:
Changing a small part of an existing file.
Updating one occurrence, or every occurrence with
replace_all.
Do not use for:
Creating a file or replacing its entire content (use the write file tool instead).
Reading a file (use the read file tool instead).
Example: edit_file(path=”a.py”, old_string=”x = 1”, new_string=”x = 2”)
- Parameters:
path – File path to edit, relative to the project directory.
old_string – Exact text to replace; must occur once unless
replace_allis set.new_string – Replacement text.
replace_all – Replace every occurrence (default: require a unique match).
- Returns:
Dictionary with path, replacements, additions, deletions, matcher, diff, error. On a missing/not-a-file error,
nearby(entries in the nearest existing directory) andnoteare populated so the caller can see what exists instead.
- async klea_utils.mcp.server.bundled_tools.find_files(pattern: Annotated[str, FieldInfo(annotation=NoneType, required=True, description="Glob pattern matched against file paths, e.g. '*.py' or '**/*.py'. Use '*' to list every file", metadata=[MinLen(min_length=1)])] = '*', path: Annotated[str, FieldInfo(annotation=NoneType, required=True, description="Directory to search. Must be relative to the current working directory and cannot contain '..' for security", metadata=[MinLen(min_length=1)])] = '.', include_ignored: Annotated[bool, FieldInfo(annotation=NoneType, required=True, description='Also list files ignored by .gitignore. Default false, which skips ignored files (and skips .git/cache directories always)')] = False, max_results: Annotated[int, FieldInfo(annotation=NoneType, required=True, description='Maximum number of file paths to return', metadata=[Ge(ge=1), Le(le=10000)])] = 100) fastmcp.tools.ToolResult[source]¶
Find files by path pattern anywhere under a directory.
Use this tool to locate files by name or extension when you do not know where they are in the tree. It returns file paths only, without contents or directory entries.
Use when:
Finding files by name or extension across a project tree.
Discovering where a file lives before reading it.
Do not use for:
Listing the contents of a specific directory, including its subdirectories (use the list files tool instead).
Searching inside file contents (use the grep tool instead).
Example:
find_files(pattern="**/*.py")- Parameters:
pattern – Glob pattern matched against file paths;
"*"lists all.path – Directory to search, relative to the project directory.
include_ignored – Also list .gitignore-ignored files.
max_results – Maximum number of file paths to return.
- Returns:
Dictionary with files (paths relative to the search directory), truncated, error.
- async klea_utils.mcp.server.bundled_tools.grep(pattern: Annotated[str, FieldInfo(annotation=NoneType, required=True, metadata=[MinLen(min_length=1)])], path: Annotated[str, FieldInfo(annotation=NoneType, required=True, description="Directory to search. Must be relative to the current working directory and cannot contain '..' for security", metadata=[MinLen(min_length=1)])] = '.', include: Annotated[str | None, FieldInfo(annotation=NoneType, required=True, description="Space separated glob patterns to restrict which files are searched, e.g. '*.py' or '*.py *.md'. Omit to search all files")] = None, case_sensitive: Annotated[bool, FieldInfo(annotation=NoneType, required=True, description='Whether the search is case sensitive')] = True, include_ignored: Annotated[bool, FieldInfo(annotation=NoneType, required=True, description='Also search files ignored by .gitignore. Default false, which skips ignored files (and skips .git/cache directories always)')] = False, context_lines: Annotated[int, FieldInfo(annotation=NoneType, required=True, description='Lines of context before and after each match (0-7). The default of 2 usually shows enough surrounding code to avoid a follow-up read_file; raise it for denser blocks', metadata=[Ge(ge=0), Le(le=7)])] = 2, max_results: Annotated[int, FieldInfo(annotation=NoneType, required=True, description='Maximum number of matching lines to return', metadata=[Ge(ge=1), Le(le=1000)])] = 100) fastmcp.tools.ToolResult[source]¶
Search file contents for lines matching a regular expression.
Use this tool to find where a symbol, string, or pattern occurs in the files under a directory.
Use when:
Locating a definition, call site, or error message in the codebase.
Searching the contents of many files without reading each one.
Do not use for:
Listing files by name without searching their contents (use the list files tool instead).
Reading a whole file (use the read file tool instead).
Example:
grep(pattern="def main", include="*.py")- Parameters:
pattern – Regular expression to search for.
path – Directory to search, relative to the project directory.
include – Space separated glob patterns restricting which files are searched.
case_sensitive – Whether the search is case sensitive.
include_ignored – Also search .gitignore-ignored files.
context_lines – Lines of context before and after each match (0-7).
max_results – Maximum number of matching lines to return.
- Returns:
Dictionary with pattern, path, match hunks, truncated, files_scanned, error. Each hunk is
{path, start_line, end_line, lines}with one{line_number, line, match}entry per line, so callers get the surrounding code without a separate read.
- async klea_utils.mcp.server.bundled_tools.list_files(path: Annotated[str, FieldInfo(annotation=NoneType, required=True, description="Directory path to list. Must be relative to current working directory and cannot contain '..' for security. Defaults to '.'", metadata=[MinLen(min_length=1)])] = '.', max_depth: Annotated[int | None, FieldInfo(annotation=NoneType, required=True, description="Maximum directory depth to traverse. 'None' for unlimited")] = None, pattern: Annotated[str, FieldInfo(annotation=NoneType, required=True, description="\n Space separated file patterns to filter based on files type.\n Correct: '*.py'\n Correct: '*.md'\n Correct: '*.py *.md'\n ")] = '*', include_files: Annotated[bool, FieldInfo(annotation=NoneType, required=True, description='Whether to include files in results')] = True, include_directories: Annotated[bool, FieldInfo(annotation=NoneType, required=True, description='Whether to include directories in results')] = True, recursive: Annotated[bool, FieldInfo(annotation=NoneType, required=True, description='If True, traverse subdirectories recursively')] = False, max_results: Annotated[int, FieldInfo(annotation=NoneType, required=True, description='Maximum number of entries to return', metadata=[Ge(ge=1), Le(le=10000)])] = 100) fastmcp.tools.ToolResult[source]¶
List files and directories with filtering and metadata.
Use this tool to explore the local file system structure and find specific files.
Use when:
Discovering what files exist in the working directory.
Finding files by name, type, or location.
Do not use for:
Reading a file’s contents (use the read file tool instead).
Finding a file anywhere under a directory tree by path pattern (use the find files tool instead).
If the
patternor theinclude_*filters match nothing but the directory is not empty, the unfiltered listing is returned instead andnoteexplains why, so the response still shows what is there.Example:
list_files(path=".", pattern="*.py", recursive=True)- Parameters:
path – Directory path to list. Must be relative to the current working directory and cannot contain ‘..’ for security. Defaults to the current directory (‘.’).
max_depth – Maximum directory depth to traverse. ‘None’ for unlimited.
pattern – Space separated file patterns to filter files by type.
include_files – Whether to include files in results.
include_directories – Whether to include directories in results.
recursive – If True, traverse subdirectories recursively.
max_results – Maximum number of entries to return.
- Returns:
Dictionary with list of files, truncated flag, error, and note (empty unless a filter matched nothing and the unfiltered listing was substituted).
- async klea_utils.mcp.server.bundled_tools.read_file(path: Annotated[str, FieldInfo(annotation=NoneType, required=True, description="File path to read. Must be relative to current working directory and cannot contain '..' for security. Defaults to '.'", metadata=[MinLen(min_length=1)])] = '.', offset: Annotated[int, FieldInfo(annotation=NoneType, required=True, description='1-indexed line to start reading from. Negative values count from the end: -1 is the last line, -N is the Nth line from the end')] = 1, char_offset: Annotated[int, FieldInfo(annotation=NoneType, required=True, description='0-indexed character position within the start line; only needed to continue inside a line longer than the character cap', metadata=[Ge(ge=0)])] = 0, limit: Annotated[int | None, FieldInfo(annotation=NoneType, required=True, description='Maximum number of lines to return (>=1). None removes the line limit, but the server character budget still caps the response', metadata=[Ge(ge=1)])] = 500, line_numbers: Annotated[bool, FieldInfo(annotation=NoneType, required=True, description='Prefix each returned line with its line number (default true). Set false to get raw line text, e.g. to copy into an edit')] = True) fastmcp.tools.ToolResult[source]¶
Read a file and return a slice of its text content.
Use this tool to inspect source files, logs, or documents as plain text. Document formats (PDF, office files) are converted to Markdown first.
Use when:
You need to see the contents of a file in the project.
You want to page through a large file by line numbers.
You need the end of a large file, such as the tail of a build log: use a negative offset (
offset=-100) instead of paging from the start.
Read file chunks instead of complete files. Use the grep tool to locate the region you need first, then read around it with
offset/limit.Do not use for:
Listing a directory (use the list files tool instead).
Fetching remote content (use the web fetch tool instead).
Examples:
read_file(path="README.md", offset=1, limit=100)read_file(path="build.log", offset=-100)
- Parameters:
path – File path to read. Must be relative to the current working directory and cannot contain ‘..’ for security. Defaults to the current directory (‘.’), which is not a file, so an omitted path yields the nearby-entries fallback instead of a validation error.
offset – 1-indexed line to start reading from. A negative value counts from the end: -1 is the last line, -N is the Nth line from the end.
char_offset – 0-indexed character position within the start line; used only to continue inside a single line longer than the character cap (see
next_char_offsetin the result).limit – Maximum number of lines to return.
Noneremoves only the line limit; the server character budget still caps the response, so aNoneread can still be truncated withnext_offset. For a negative offset, this counts forward from the tail start.line_numbers – Prefix each line with its line number; set false for raw text to copy into an edit.
The response size is always capped by a server-side character budget, reported as
char_budget; a large file is read by paging (offset/limit) or from the end rather than returned in one call.- Returns:
Dictionary with content, line range, total_lines, truncated, truncation_reason, char_budget, next_offset, next_char_offset, error and note.
line_start/line_endandnext_offsetare negative for a tail read, with -1 as the last line.total_linesisnullwhen a streamed read stops before the relevant end (EOF for a forward read, the file start for a tail read); a tail that reaches past the start clamps to the first line and reports the exact total. A partial tail that reaches EOF hastruncated: false,next_offset: nullandline_end: -1.char_budgetis the effective server character cap, andtruncation_reasonischar_budget,line_limitorscan_capwhen truncated (empty otherwise), so alimit=Noneread shows why it was cut. A truncated read is a success (emptyerror) that normally ends on a line boundary and carriesnext_offset(and an explanatorynote) so the caller can continue. A single line longer than the character cap is paged by characters: continue with the sameoffsetand the returnednext_char_offset. On a missing/not-a-file error,nearby(entries in the nearest existing directory) andnoteare populated so the caller can see what exists instead.
- async klea_utils.mcp.server.bundled_tools.run_command(command: Annotated[str, FieldInfo(annotation=NoneType, required=True, description="Shell command string to run. Pipes, '&&' and redirection are supported. Scope the command to the requested target; do not run a broader command or suite than asked (for example run the one named test file, not the whole suite).", metadata=[MinLen(min_length=1)])], working_directory: Annotated[str | None, FieldInfo(annotation=NoneType, required=True, description='Directory to run the command in; must be inside the project directory. Defaults to the project directory.')] = None, timeout_seconds: Annotated[float, FieldInfo(annotation=NoneType, required=True, description='Seconds before the command is killed. Defaults to 30; the maximum is set by the KLEA_RUN_COMMAND_MAX_TIMEOUT environment variable (default 600).', metadata=[Ge(ge=1)])] = 30.0, max_output_chars: Annotated[int, FieldInfo(annotation=NoneType, required=True, description='Maximum characters captured per output stream', metadata=[Ge(ge=1)])] = 100000) fastmcp.tools.ToolResult[source]¶
Run a shell command and return its exit status and output.
Use this tool to inspect or act on the workspace: print the working directory, list processes, run a build or test, or invoke a script.
Use when: - You need a fact about the environment (working directory, host, date). - You need to run a build, test, or script in the project.
Do not use for: - Reading a file (use the read file tool instead). - Listing a directory (use the list files tool instead).
A non-zero exit code is a normal result, not an error: many tools signal conditions with it (
diff/grepreturn 1 for differences/no match, a failing test returns non-zero). Judge the outcome fromreturncodeandstdout;erroris set only when the command could not be run or was killed.A timed-out or cancelled command has its process group killed, but a command that detaches its children (for example daemonises or calls
setsid) may leave processes running; check withps/pgrepand clean up if needed.Example: run_command(command=”pwd && ls”, working_directory=”.”)
- Parameters:
command – Shell command string to run.
working_directory – Directory to run in; must be inside the project.
timeout_seconds – Seconds before the command is killed.
max_output_chars – Maximum characters captured per output stream.
- Returns:
Dictionary with command, working_directory, returncode, stdout, stderr, truncated, error.
- async klea_utils.mcp.server.bundled_tools.search_papers(ctx: fastmcp.Context, query: Annotated[str, FieldInfo(annotation=NoneType, required=True, description="Keywords to search for, e.g. 'hodgkin huxley'", metadata=[MinLen(min_length=1)])], domain: Annotated[Literal['general', 'life-sciences'], FieldInfo(annotation=NoneType, required=True, description="'general' for all fields, or 'life-sciences' to search biomedical literature (Europe PMC, PubMed) first")] = 'general', preprints: Annotated[bool, FieldInfo(annotation=NoneType, required=True, description='Also search preprints (not peer reviewed). Set only when the user asks for preprints, or the peer reviewed results are not enough')] = False, max_results: Annotated[int, FieldInfo(annotation=NoneType, required=True, description='Maximum number of peer reviewed results, and separately of preprint results', metadata=[Ge(ge=1), Le(le=25)])] = 10) fastmcp.tools.ToolResult[source]¶
Search academic papers by keywords.
Use this tool to find published research papers on a topic, with their title, authors, year, journal, DOI, link and abstract. Peer reviewed papers are searched first; preprints are only included when
preprintsis true, and are marked withpeer_reviewed: false.Use when: - Finding papers or references on a scientific topic. - Looking for the source of a model, method or result.
Do not use for: - Reading a paper’s full text (use the web fetch or download tool). - General web pages, news or docs (use the web search tool instead).
When you cite a preprint, tell the user it has not been peer reviewed.
Example: search_papers(query=”hodgkin huxley”, domain=”life-sciences”)
- Parameters:
query – Keywords to search for.
domain – ‘general’ or ‘life-sciences’.
preprints – Also search preprints, listed after the peer reviewed results.
max_results – Maximum results from the peer reviewed sources and preprints.
- Returns:
Dictionary with query, domain, preprints, results (title, authors, year, journal, abstract, doi, url, peer_reviewed, source), error and note.
- async klea_utils.mcp.server.bundled_tools.search_stores(ctx: fastmcp.Context, query: Annotated[str, FieldInfo(annotation=NoneType, required=True, description='Search string.')] = '', domain: Annotated[str, FieldInfo(annotation=NoneType, required=True, description='Domain to search, or empty to list the catalogue.')] = '', mode: Annotated[str, FieldInfo(annotation=NoneType, required=True, description="One of 'hybrid', 'vector', 'bm25' or 'catalogue'.")] = 'hybrid', top_k: Annotated[int, FieldInfo(annotation=NoneType, required=True, description='Documents fetched per store', metadata=[Ge(ge=1), Le(le=50)])] = 5, rerank: Annotated[bool, FieldInfo(annotation=NoneType, required=True, description='Re-rank the fused results with the cross-encoder')] = True, metadata_filter: Annotated[dict[str, Any] | None, FieldInfo(annotation=NoneType, required=True, description='Optional metadata filter in the store filter DSL')] = None, max_chars: Annotated[int, FieldInfo(annotation=NoneType, required=True, description='Character budget for the returned reference material', metadata=[Ge(ge=1), Le(le=200000)])] = 20000) fastmcp.tools.ToolResult[source]¶
Search the deployment’s configured document stores for one domain.
Use this tool to answer a question from the deployment’s document collections: it queries the domain’s vector and BM25 stores, fuses their results, re-ranks them and returns character-budgeted reference material.
Use when: - Answering a question from the deployment’s document collections. - Looking up material held in a vector store or BM25 corpus.
Do not use for: - Reading a local file (use the read file tool instead). - General web pages (use the web search tool instead).
Call it with mode ‘catalogue’ (or an empty domain) to list the domains with the filter fields each accepts.
- Parameters:
query – Search string; required unless mode is ‘catalogue’.
domain – Domain to search, or empty to list the catalogue.
mode – One of ‘hybrid’, ‘vector’, ‘bm25’ or ‘catalogue’.
top_k – Documents fetched per store (1-50).
rerank – Re-rank the fused results with the cross-encoder.
metadata_filter – Optional metadata filter in the store filter DSL.
max_chars – Character budget for the returned reference material.
- Returns:
Dictionary with query, domain, mode, top_k, results, count, content, catalogue, truncated, error and note.
- async klea_utils.mcp.server.bundled_tools.web_fetch(ctx: fastmcp.Context, url: Annotated[str, FieldInfo(annotation=NoneType, required=True, metadata=[MinLen(min_length=1)])], timeout: Annotated[float, FieldInfo(annotation=NoneType, required=True, metadata=[Ge(ge=1.0), Le(le=120.0)])] = 30.0, max_chars: Annotated[int, FieldInfo(annotation=NoneType, required=True, metadata=[Ge(ge=1), Le(le=1000000)])] = 100000) fastmcp.tools.ToolResult[source]¶
Fetch a URL and return its text content.
Use this tool to read web pages, docs, or other HTTP resources.
Use when: - Reading a page or document from the web. - Checking a URL that a user or another tool referenced.
Do not use for: - Downloading a file to disk (use the download file tool instead).
Example: web_fetch(url=”https://example.com”)
- Parameters:
url – HTTP or HTTPS URL to fetch.
timeout – Request timeout in seconds.
max_chars – Maximum number of characters of content to return.
- Returns:
Dictionary with url, status_code, content_type, content, truncated, error.
- async klea_utils.mcp.server.bundled_tools.web_search(ctx: fastmcp.Context, query: Annotated[str, FieldInfo(annotation=NoneType, required=True, metadata=[MinLen(min_length=1)])], max_results: Annotated[int, FieldInfo(annotation=NoneType, required=True, metadata=[Ge(ge=1), Le(le=20)])] = 8) fastmcp.tools.ToolResult[source]¶
Search the web and return ranked results with snippets.
Use this tool to discover current information, documentation, or sources online, then read a chosen result with the web fetch tool.
Use when: - You need to find information on the web (discovery). - You want links and snippets before fetching a page.
Do not use for: - Reading the content of a specific URL (use the web fetch tool instead). - Downloading a file to disk (use the download file tool instead).
Example: web_search(query=”NeuroML specification”)
- Parameters:
query – Free-text search query.
max_results – Maximum number of results to return (1-20).
- Returns:
Dictionary with query, provider, results (list of title, url, snippet, published, score), and error.
- async klea_utils.mcp.server.bundled_tools.write_file(path: Annotated[str, FieldInfo(annotation=NoneType, required=True, metadata=[MinLen(min_length=1)])], content: Annotated[str, FieldInfo(annotation=NoneType, required=True, description='Complete content to write to the file')]) fastmcp.tools.ToolResult[source]¶
Create a new file or overwrite an existing one with the given content.
Use this tool to create a file, or to replace a file’s entire contents. The write is atomic and an existing file’s mode is preserved.
Use when:
Creating a new file.
Replacing the whole content of a small file.
Do not use for:
Reading a file (use the read file tool instead).
Listing a directory (use the list files tool instead).
Making a small change to an existing file (use the edit file tool instead).
Example: write_file(path=”notes.txt”, content=”hello world”)
- Parameters:
path – File path to write, relative to the project directory. Missing parent directories are created.
content – Complete file content.
- Returns:
Dictionary with path, created, bytes_written, additions, deletions, diff, error.