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 - registry —
McpToolRegistryfor tool lookup and optional Qdrant-backed search - tool —
McpToolwrapper with schema and metadata - prompt — MCP prompt template support
- error —
McpErrorerror types
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).
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