zeph-mcp
MCP client with multi-server lifecycle and Qdrant tool registry for Zeph.
Overview
Implements the Model Context Protocol client for Zeph, managing connections to multiple MCP servers, discovering their tools at startup, and routing tool calls through a unified executor. Built on rmcp 0.17.
Key Modules
- client — low-level MCP transport and session handling;
ToolListChangedHandlerreceivestools/list_changednotifications, appliessanitize_tools()(rate-limited to once per 5 s per server, capped at 100 tools), and forwards the sanitized list toMcpManagervia a refresh channel - manager —
McpManager,McpTransport,ServerEntryfor multi-server lifecycle; command allowlist validation (npx, uvx, node, python3, docker, mcpls, etc.), env var blocklist (LD_PRELOAD, DYLD_*, NODE_OPTIONS, etc.), and path separator rejection; statically configured servers (from[[mcp.servers]]) bypass SSRF validation to allow connections tolocalhostand private IPs — dynamically added servers retain full SSRF protection - sanitize —
sanitize_tools()applied to all tool definitions at registration time and again on everytools/list_changedrefresh; strips 17 injection-detection patterns, Unicode Cf-category characters, and caps descriptions at 1024 bytes; fields triggering a pattern are replaced with"[sanitized]"— tool registration is never blocked - executor —
McpToolExecutorbridging MCP tools into theToolExecutortrait; propagatescaller_idfrom sub-agent dispatches to the audit log - registry —
McpToolRegistryfor tool lookup and optional Qdrant-backed search - tool —
McpToolwrapper with schema and metadata - prompt — MCP prompt template support
- error —
McpErrorerror types with typedMcpErrorCodefor retry classification (Transient,RateLimited,InvalidInput,AuthFailure,ServerError,NotFound,PolicyBlocked)
MCP Roots protocol
The MCP client implements the roots/list handler, exposing configured project roots to MCP servers. Roots are declared via [mcp.roots] in config and passed to each server connection at initialization time. Servers that support roots/list can use this information to scope their file system access to the declared directories.
Semantic tool discovery
SemanticToolIndex indexes all registered MCP tool definitions as embedding vectors in Qdrant (or the SQLite vector backend). On each LLM turn, only the top-K most relevant tools — ranked by cosine similarity to the current query — are included in the tools array sent to the model. This keeps the tools payload small for models with narrow context windows and reduces prompt injection surface area.
[]
= true
= 20 # max tools sent per request (0 = all tools, disables discovery)
= 0.55 # minimum similarity threshold
= "zeph_mcp_tools"
Note:
Tool discovery requires an embedding model. Configure
[llm.orchestrator] embedding_modelor set a dedicatedembedding_providerfor the mcp subsystem. When Qdrant is unavailable the index falls back to BM25 keyword matching.
Per-message pruning cache
PruningCache tracks which tool set was sent in the previous LLM request. If the ranked tool list for the current turn is identical, the cache returns the pre-serialized JSON blob directly, skipping re-serialization and re-ranking.
Cache invalidation triggers on: new tool registered, tool removed, tools/list_changed notification, or config reload. No manual configuration is required; the cache is always active when [mcp.tool_discovery] enabled = true.
Tool attestation
expected_tools in a server config entry declares the tool names that server is authorised to expose. If a tool name appears in tools/list that is not in expected_tools, it is logged as a security warning and excluded from the registry.
[[]]
= "filesystem"
= "npx"
= ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
= ["read_file", "write_file", "list_directory"]
Important:
Leave
expected_toolsempty (or omit it) to allow all tools from a server. Setting it to an empty list[]blocks all tools from that server.
Elicitation
MCP servers can request structured user input via the elicitation/create method. When enabled, Zeph presents a phishing-prevention header before displaying the server's form and routes the response back over a bounded channel.
| Config field | Type | Default | Description |
|---|---|---|---|
elicitation_enabled |
bool | false |
Enable elicitation globally (opt-in) |
elicitation_timeout |
u64 (secs) | 120 |
Seconds to wait for user input before timing out |
elicitation_queue_capacity |
usize | 16 |
Bounded channel capacity for pending elicitation requests |
elicitation_warn_sensitive_fields |
bool | true |
Warn when field names suggest sensitive input (password, token, key, etc.) |
A per-server elicitation_enabled override takes precedence over the global setting. Sandboxed servers (trust level Sandboxed) can never use elicitation regardless of config.
[]
= true
= 120
Security hardening
- Tool collision detection — when two servers expose tools with the same
sanitized_id, a warning is emitted at registration time. The first-registered tool wins. - Tool-list snapshot locking — set
lock_tool_list = trueon a server entry to reject anytools/list_changedrefresh after the initial snapshot. Prevents malicious servers from injecting new tools mid-session. - Per-server stdio env isolation —
env_isolation = true(ordefault_env_isolation = trueglobally) strips the inherited process environment before spawning stdio MCP servers, preventing accidental secret leakage viaPATH,HOME, and similar variables. Explicitly declaredenvkeys are still passed through. - Intent-anchor nonce boundaries — tool output from MCP servers is wrapped with per-call nonce delimiters before entering the LLM context, reducing prompt injection surface.
[]
= true # strip env for all stdio servers by default
[[]]
= "untrusted"
= "npx"
= ["-y", "some-mcp-server"]
= true # reject tool list changes after startup
= true # explicit per-server override
MCPShield trust calibration
MCPShield assigns a per-server trust score that starts at 1.0 and degrades on anomalous events: tool definition mutations between tools/list_changed cycles, sanitization hits, unexpected tool names, and tool execution errors. When the trust score drops below shield.quarantine_threshold, the server is quarantined and its tools are excluded from the registry until the score recovers (exponential half-life decay).
[]
= true
= 0.4 # score below which a server is quarantined
= 3600 # half-life for trust score recovery
Tip:
View per-server trust scores in the TUI with
mcp:listfrom the command palette — the trust column shows the current score and a coloured indicator (green ≥ 0.7, yellow ≥ 0.4, red < 0.4).
Structured error codes
Every McpError::ToolCall carries a typed McpErrorCode that the agent uses to decide whether to retry:
| Code | Retryable | When |
|---|---|---|
Transient |
Yes | Temporary failure; connection drops, timeouts |
RateLimited |
Yes | Server asked to back off |
ServerError |
Yes | Internal server error |
InvalidInput |
No | Bad parameters — retrying unchanged will fail again |
AuthFailure |
No | Token invalid or expired |
NotFound |
No | Tool or resource does not exist |
PolicyBlocked |
No | Blocked by policy rule or OAP authorization |
Errors that do not carry an explicit code (timeouts, connection failures, SSRF blocks) are mapped automatically. McpErrorCode::is_retryable() is the authoritative retry gate used by the agent loop.
OAP authorization
Tool calls can be authorized declaratively via [tools.authorization] in config. Rules are appended after [tools.policy] rules using first-match-wins semantics. OAP is disabled by default.
[]
= true
[[]]
= "allow"
= ["read_file", "list_directory"]
[[]]
= "deny"
= ["shell"]
Denied calls return McpErrorCode::PolicyBlocked and are not retried.
Tool call quota
Limit the total number of tool calls per agent session:
[]
= 100 # None = unlimited (default)
Only the first attempt counts against the quota — retries of a failed call are free.
Configuration
[[]]
= "filesystem"
= "npx"
= ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
= {}
[[]]
= "fetch"
= "uvx"
= ["mcp-server-fetch"]
Note:
Statically configured servers (from
[[mcp.servers]]) bypass SSRF validation to allow connections tolocalhostand private IPs. Dynamically added servers retain full SSRF protection.
Features
| Feature | Description |
|---|---|
mock |
Enables MockMcpClient for downstream tests |
Installation
Documentation
Full documentation: https://bug-ops.github.io/zeph/
License
MIT