MCP (Model Context Protocol)
Python dependency commands on this page use a PM-prepared source checkout. After a dependency change, reactivate the checkout and restart Nedura.
MCP lets Nedura connect to external tool servers so the agent can use tools that live outside Nedura itself — GitHub, databases, file systems, browser stacks, internal APIs, and more.
If you have ever wanted Nedura to use a tool that already exists somewhere else, MCP is usually the cleanest way to do it.
The mcpServers block in your ~/.claude.json maps to mcp_servers in Nedura's config.yaml — and nedura import-agent claude-code (from the agent-importers plugin) migrates it, along with skills and instructions. See Import from Other Agents.
What MCP gives you
- Access to external tool ecosystems without writing a native Nedura tool first
- Local stdio servers and remote HTTP MCP servers in the same config
- Automatic tool discovery and registration at startup
- Utility wrappers for MCP resources and prompts when supported by the server
- Per-server filtering so you can expose only the MCP tools you actually want Nedura to see
Quick start
-
MCP support ships with the standard install — no extra step needed.
-
Add an MCP server to
~/.nedura/config.yaml:
mcp_servers:
filesystem:
command: "npx"
args: ["-y", "@modelcontextprotocol/server-filesystem", "/home/user/projects"]
- Start Nedura:
nedura chat
- Ask Nedura to use the MCP-backed capability.
For example:
List the files in /home/user/projects and summarize the repo structure.
Nedura will discover the MCP server's tools and use them like any other tool.
Two kinds of MCP servers
Stdio servers
Stdio servers run as local subprocesses and talk over stdin/stdout.
mcp_servers:
github:
command: "npx"
args: ["-y", "@modelcontextprotocol/server-github"]
env:
GITHUB_PERSONAL_ACCESS_TOKEN: "***"
Use stdio servers when:
- the server is installed locally
- you want low-latency access to local resources
- you are following MCP server docs that show
command,args, andenv
HTTP servers
HTTP MCP servers are remote endpoints Nedura connects to directly.
mcp_servers:
remote_api:
url: "https://mcp.example.com/mcp"
headers:
Authorization: "Bearer ***"
Use HTTP servers when:
- the MCP server is hosted elsewhere
- your organization exposes internal MCP endpoints
- you do not want Nedura spawning a local subprocess for that integration
HTTP and SSE servers honor the standard proxy settings: HTTPS_PROXY / HTTP_PROXY / ALL_PROXY (a socks:// alias is normalized to socks5://), then the OS proxy (Windows registry, macOS system settings), with NO_PROXY hosts — including CIDR ranges and *.example.com patterns — connecting directly.
OAuth-authenticated HTTP servers
Most hosted MCP servers (Cloudflare, Linear, Sentry, Atlassian, Asana, Figma, Stripe, …) require OAuth 2.1 instead of a static bearer token. Set auth: oauth and Nedura handles discovery, client identification, PKCE, token exchange, refresh, and step-up auth via the MCP Python SDK.
Nedura identifies itself with a Client ID Metadata Document on servers that support one, and falls back to Dynamic Client Registration on those that don't. Both are automatic; there is nothing to configure.
Figma's hosted endpoint (https://mcp.figma.com/mcp) allowlists Dynamic Client Registration by exact client_name — bare "Nedura" 403s, while "Claude Code" and "Codex" succeed. Nedura auto-sets oauth.client_name: "Claude Code" for mcp.figma.com so install/login works without a special trick:
mcp_servers:
figma:
url: "https://mcp.figma.com/mcp"
auth: oauth
Or: nedura mcp install figma, then nedura mcp login figma.
mcp_servers:
linear:
url: "https://mcp.linear.app/mcp"
auth: oauth
On first connect, Nedura prints an authorize URL, opens your browser when possible, and waits for the OAuth callback on a local loopback port. Tokens are cached at ~/.nedura/mcp-tokens/<server>.json with 0o600 perms; subsequent runs reuse them silently until refresh fails.
Refresh tokens are bound to the authorization server that granted them: Nedura records the discovered issuer alongside the cached tokens and, if a server's advertised authorization server ever changes (server migration, metadata edit, or hijack), the stored refresh token is dropped instead of being sent to the new issuer. The current access token keeps working until it expires, then a normal re-authorization runs against the new issuer.
The redirect back from the authorization server is checked against RFC 9207: when the server's metadata advertises authorization_response_iss_parameter_supported, a redirect without a matching iss is rejected. Figma's authorization server (https://api.figma.com) advertises that support and then omits iss; Nedura fills the missing value from the discovered issuer for that one issuer and logs a warning, so nedura mcp login figma completes. A present-but-different iss is still rejected, and no other server gets the exemption.
The authorization server's metadata document must name the server the resource advertised (RFC 8414 §3.3); a document for a different server is rejected before any registration or login. One shape is accepted without an exact match: a server advertised with a path (https://host/path) whose document, fetched from https://host/.well-known/oauth-authorization-server/path, names the origin https://host as its issuer — Strava's MCP connector publishes exactly that pair. Only the origin's operator controls that well-known location, so the document is treated as the advertised server's own; a document naming another origin or another path, or one reached only through a redirect or a fallback location, still fails with Authorization server metadata issuer mismatch.
Google-hosted servers (Gmail, Calendar). Google only issues a refresh token when the authorization request carries access_type=offline, which MCP discovery never advertises. Nedura adds it (plus prompt=consent, so a repeat login is re-granted one) whenever the discovered authorization server is accounts.google.com, so the connection persists across restarts and works from nedura gateway. Other issuers' requests are untouched.
Remote / headless hosts. When Nedura runs on a different machine than your browser, the loopback callback can't reach your laptop. Ways to complete the flow:
- Paste-back (no setup): on an interactive terminal Nedura prints "Or paste the redirect URL here…" alongside the authorize URL. Open the URL in your browser, approve, copy the full URL the browser ends up on (the redirect will show a connection error — that's expected), paste it at the prompt. Bare
?code=…&state=…query strings work too. - Device-code login (no callback at all): if the server's authorization server advertises a device authorization endpoint, run
nedura mcp login <server> --flow deviceon the machine running Nedura. It prints a verification URL and a short code; open the URL on any device, enter the code, and Nedura polls for approval. No browser is launched on the host and no callback listener is needed. Setoauth.flow: deviceon the server to makeloginandreauthuse it by default. Details: Device-code login. - SSH port forward:
ssh -N -L <port>:127.0.0.1:<port> user@hostin a separate terminal, then let the redirect flow normally. - Proxied callback (
redirect_uri): when a public HTTPS endpoint forwards to the host (e.g. a Tailscale Funnel or reverse proxy pointed at the callback port), setoauth.redirect_uriand the browser redirect reaches Nedura on its own — no tunnel or paste needed:
mcp_servers:
myserver:
url: "https://mcp.example.com/mcp"
auth: oauth
oauth:
redirect_port: 8765 # fixed port for the proxy to target
redirect_uri: "https://oauth.example.ts.net/callback"
For fully headless gateways (messaging bot, no interactive terminal at all), the optional mcp-oauth-remote-gateway skill walks the agent through completing the flow manually and writing tokens where Nedura expects them.
Pitfall — WAF rejects 127.0.0.1 redirect URIs. A few providers front their authorization server with a WAF that 403s any authorize request whose query string contains a literal 127.0.0.1 (Reclaim.ai's AWS API Gateway is a known example — every attempt returns {"message":"Forbidden"} before reaching the OAuth app). Set oauth.redirect_host: localhost to use http://localhost:<port>/callback instead; the callback listener still binds 127.0.0.1 either way.
See OAuth over SSH / Remote Hosts for the full walkthrough, including DCR-less servers (e.g. Slack), pre-registered client_id/client_secret, scope customization, and re-auth via nedura mcp login <server>.
Pitfall — providers that don't support automatic registration (Google Drive, Atlassian). Some servers reject the dynamic client registration step (RFC 7591) that bare auth: oauth relies on — Google's official Drive server (https://drivemcp.googleapis.com/mcp/v1) returns a 400 Bad Request, so no OAuth client is created and no token is acquired. The symptom is subtle: these servers also serve tools/list without auth, so nedura mcp login can list the tools and look like it worked, but every real tool call later times out. nedura mcp login now detects this (it checks that a token actually landed on disk) and tells you to supply your own OAuth client. Create one in the provider's console and add it to config:
mcp_servers:
googledrive:
url: "https://drivemcp.googleapis.com/mcp/v1"
auth: oauth
oauth:
client_id: "<your-oauth-client-id>"
client_secret: "<your-oauth-client-secret>"
Then run nedura mcp login googledrive — with the pre-registered client, Nedura skips registration and runs the normal browser authorization flow.
Pitfall — config auto-reload race. When you edit ~/.nedura/config.yaml from inside a running Nedura session, the CLI auto-reloads MCP connections with a 30s timeout. That's not enough for an interactive OAuth flow. Add the entry, then run nedura mcp login <server> from a fresh terminal — it waits the full 5 minutes for you to complete auth.
Need longer than 5 minutes to approve? Set oauth.timeout on the server entry (seconds). nedura mcp login, the dashboard and Desktop re-auth all wait oauth.timeout + 15 s (or the entry's connect_timeout, whichever is longer); a login that still runs out of time reports Connecting to MCP server '<name>' timed out after Ns naming both knobs instead of a blank failure line.
mTLS / client certificates
Remote HTTP MCP servers that require mutual TLS (client-certificate authentication) are supported via client_cert / client_key. Nedura passes the resolved certificate to the underlying HTTP client for the TLS handshake.
client_cert accepts three shapes:
- A single combined PEM path — one file holding both the certificate and the private key:
mcp_servers:
internal_api:
url: "https://mcp.internal.example.com/mcp"
client_cert: "~/.certs/mcp-client.pem"
- A
[cert, key]2-tuple — certificate and key in separate files (equivalent to settingclient_cert+client_key):
mcp_servers:
internal_api:
url: "https://mcp.internal.example.com/mcp"
client_cert: ["~/.certs/mcp-client.crt", "~/.certs/mcp-client.key"]
- A
[cert, key, password]3-tuple — when the private key is encrypted, the third element is the key passphrase:
mcp_servers:
internal_api:
url: "https://mcp.internal.example.com/mcp"
client_cert: ["~/.certs/mcp-client.crt", "~/.certs/mcp-client.key", "${MCP_KEY_PASSWORD}"]
You can also keep the cert and key fully separate via client_cert (combined PEM) plus an explicit client_key. Paths support ~ expansion; a missing file raises a clear, server-scoped error rather than an opaque TLS handshake failure.
Per-user identity header
Remote HTTP/SSE MCP servers that key behavior on a caller identity (per-user rate limits, audit trails, multi-tenant routing) can be sent an identity header on every request via identity_header:
mcp_servers:
team_api:
url: "https://mcp.team.example.com/mcp"
identity_header:
name: "X-User-Id"
value_from: "static" # "static" (default) or "profile"
value: "alice" # required for static
value_from: staticsends the literalvaluefrom config.yaml.value_from: profilesends the active Nedura profile name, resolved once at connect time — useful when multiple profiles on one machine talk to the same server and it needs to tell them apart.
An explicit entry in the server's headers mapping with the same name (any casing) always wins; the identity header never overrides your own header config. Invalid identity_header blocks are warned about and ignored — they never block the server from connecting. On stdio servers the key is ignored with a warning (stdio transports have no headers).
Basic configuration reference
Nedura reads MCP config from ~/.nedura/config.yaml under mcp_servers.
Common keys
| Key | Type | Meaning |
|---|---|---|
command | string | Executable for a stdio MCP server |
args | list | Arguments for the stdio server |
env | mapping | Environment variables passed to the stdio server |
cwd | string | Working directory for the stdio server process. Default: the session working directory when one is pinned (ACP/gateway sessions, terminal.cwd), else the Nedura process directory |
url | string | HTTP MCP endpoint |
headers | mapping | HTTP headers for remote servers |
client_cert | string | list | Client certificate for mTLS — a combined PEM path, or [cert, key] / [cert, key, password] |
client_key | string | Client private-key PEM path (when separate from client_cert) |
identity_header | mapping | Optional per-user identity header for HTTP/SSE servers — {name, value_from: static|profile, value} |
timeout | number | Tool call timeout |
connect_timeout | number | Initial connection timeout (also bounds the MCP initialize handshake) |
lazy | bool | If true, register the server's tools from the schema cache at startup and only start/connect it on the first tool call (default false). Needs one prior live connect to fill the cache. |
idle_timeout_seconds | number | Recycle a stdio server after this many seconds without a tool call (0 = never, default). The server restarts transparently on the next tool call. |
max_lifetime_seconds | number | Recycle a stdio server after this total age (0 = never, default). Restarts transparently on next use. |
enabled | bool | If false, Nedura skips the server entirely |
supports_parallel_tool_calls | bool | If true, tools from this server may run concurrently |
events | bool | If true, register the experimental MCP events tools for this server (default false). See MCP Events |
tools | mapping | Per-server tool filtering and utility policy |
Minimal stdio example
mcp_servers:
filesystem:
command: "npx"
args: ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/allowed/dir"]
Recycling memory-heavy stdio servers
Browser-based MCP servers (e.g. @playwright/mcp) keep a full Chromium
resident after their first tool call — hundreds of MB that never get
released. Opt in to automatic recycling and the server is torn down after
the idle/lifetime limit, then restarted transparently the next time one of
its tools is called (its tools stay registered the whole time):
mcp_servers:
playwright:
command: "npx"
args: ["-y", "@playwright/mcp@latest", "--headless"]
idle_timeout_seconds: 900 # recycle after 15 min without a tool call
max_lifetime_seconds: 86400 # and at least once a day regardless
Minimal HTTP example
mcp_servers:
company_api:
url: "https://mcp.internal.example.com"
headers:
Authorization: "Bearer ***"
Built-in presets
For well-known MCP servers, nedura mcp add accepts a --preset flag that fills in the transport details so you don't have to look up the command and args. The preset only supplies defaults — anything else (env vars, headers, filtering) you pass on the same command line still wins.
| Preset | What it wires up |
|---|---|
codex | The Codex CLI's MCP server (codex mcp-server over stdio). Requires the codex CLI on PATH. |
# Add Codex CLI as an MCP server in one line
nedura mcp add codex --preset codex
That writes the equivalent of:
mcp_servers:
codex:
command: "codex"
args: ["mcp-server"]
You can pick any local name (nedura mcp add my-codex --preset codex is fine); the preset only provides the command/args defaults.
How Nedura registers MCP tools
Nedura prefixes MCP tools so they do not collide with built-in names:
mcp_<server_name>_<tool_name>
Examples:
| Server | MCP tool | Registered name |
|---|---|---|
filesystem | read_file | mcp_filesystem_read_file |
github | create-issue | mcp_github_create_issue |
my-api | query.data | mcp_my_api_query_data |
In practice, you usually do not need to call the prefixed name manually — Nedura sees the tool and chooses it during normal reasoning.
Tool-result sanitization and _meta
Two behaviors apply to every MCP tool result before the model sees it:
- Invisible Unicode TAG characters are stripped. Characters in the U+E0000–U+E007F range render as nothing in terminals and chat UIs but are fully visible to the model — a classic prompt-injection smuggling channel for a malicious or compromised server. Nedura strips them from tool results, resource content, and tool descriptions. Legitimate emoji tag sequences (regional flags like 🏴) are preserved.
- Vendor
_metais surfaced; protocol-reserved keys are not. When a server attaches a_metamapping to a tool result (vendor namespaces likecom.example/handoff), Nedura passes it through to the model alongside the result content. Keys under protocol-reserved prefixes — amodelcontextprotocolormcplabel followed by another label, e.g.modelcontextprotocol.io/...ortools.mcp.com/...— are dropped, matching the MCP spec's key-name rules. If nothing model-facing remains, the_metafield is omitted entirely.
Image results
An ImageContent block (a screenshot, a rendered chart, a page capture) is saved to Nedura's image cache and its path is reported in the result as MEDIA:/path/to/image.png, so you can ask for the file on any attachment-capable surface. When the main model can see images inside tool results, the image itself is attached to the result too, so the model reads the pixels directly. MCP results follow the same rule as vision_analyze: agent.image_input_mode, an explicit auxiliary.vision backend and the model's vision capability all apply (see Vision). Attached images are downscaled to vision.embed_target_bytes (up to 4 per result) because they are re-sent on every later turn. Text-only models get the path only.
MCP utility tools
When supported, Nedura also registers utility tools around MCP resources and prompts:
list_resourcesread_resourcelist_promptsget_prompt
These are registered per server with the same prefix pattern, for example:
mcp_github_list_resourcesmcp_github_get_prompt
Important
These utility tools are now capability-aware:
- Nedura only registers resource utilities if the MCP session actually supports resource operations
- Nedura only registers prompt utilities if the MCP session actually supports prompt operations
So a server that exposes callable tools but no resources/prompts will not get those extra wrappers.
Per-server filtering
You can control which tools each MCP server contributes to Nedura, allowing fine-grained management of your tool namespace.
Disable a server entirely
mcp_servers:
legacy:
url: "https://mcp.legacy.internal"
enabled: false
If enabled: false, Nedura skips the server completely and does not even attempt a connection.
Whitelist server tools
mcp_servers:
github:
command: "npx"
args: ["-y", "@modelcontextprotocol/server-github"]
env:
GITHUB_PERSONAL_ACCESS_TOKEN: "***"
tools:
include: [create_issue, list_issues]
Only those MCP server tools are registered.
Entries in include/exclude may also be glob patterns (*, ?, [...],
matched case-sensitively): include: ["*_dns_*"] registers every tool whose
name contains _dns_. Plain entries without metacharacters stay exact-match.
Globs are the practical way to filter servers that expose thousands of
auto-generated endpoint tools by product family.
Blacklist server tools
mcp_servers:
stripe:
url: "https://mcp.stripe.com"
tools:
exclude: [delete_customer]
All server tools are registered except the excluded ones.
Glob patterns
Both lists accept fnmatch-style globs alongside exact names — essential for
huge flat surfaces like Cloudflare's API MCP (?codemode=false, ~3,300
tools) where excluding product areas one endpoint at a time is impractical:
mcp_servers:
cloudflare:
url: "https://mcp.cloudflare.com/mcp?codemode=false"
auth: oauth
tools:
exclude: ["*_radar_*", "*_accounts_dlp_*", "*_zones_web3_*"]
Entries without glob metacharacters (*, ?, [) match exactly — docs
excludes only the tool named docs, never docs_search.
Precedence rule
If both are present:
tools:
include: [create_issue]
exclude: [create_issue, delete_issue]
include wins.
Filter utility tools too
You can also separately disable Nedura-added utility wrappers:
mcp_servers:
docs:
url: "https://mcp.docs.example.com"
tools:
prompts: false
resources: false
That means:
tools.resources: falsedisableslist_resourcesandread_resourcetools.prompts: falsedisableslist_promptsandget_prompt
Full example
mcp_servers:
github:
command: "npx"
args: ["-y", "@modelcontextprotocol/server-github"]
env:
GITHUB_PERSONAL_ACCESS_TOKEN: "***"
tools:
include: [create_issue, list_issues, search_code]
prompts: false
stripe:
url: "https://mcp.stripe.com"
headers:
Authorization: "Bearer ***"
tools:
exclude: [delete_customer]
resources: false
legacy:
url: "https://mcp.legacy.internal"
enabled: false
What happens if everything is filtered out?
If your config filters out all callable tools and disables or omits all supported utilities, Nedura does not create an empty runtime MCP toolset for that server.
That keeps the tool list clean.
Runtime behavior
Discovery time
Nedura discovers MCP servers at startup and registers their tools into the normal tool registry.
Servers are connected at most 4 at a time per discovery pass (startup, /reload-mcp, config
watcher). Every stdio server spawns its own child-process tree, so an unbounded pass with many servers
used to launch them all in the same instant — a CPU/RAM spike and, on multi-profile fleets, a burst of
simultaneous provider calls. Tune it in config.yaml:
mcp:
discovery_concurrency: 4 # max simultaneous server connects; 0 = unlimited
A pass with more servers than the cap runs in waves; each wave keeps the usual 120 s budget (whole pass capped at 300 s), so a slow fleet finishes later rather than timing out.
Lazy start
A server with lazy: true is registered from the on-disk schema cache instead: its tools appear in the registry immediately, and the process is spawned (or the HTTP endpoint connected) on the first tool call. The cache is written on every live connect, so the first run of a new or changed server is always eager. The banner and the TUI session panel show such a server as lazy with its cached tool count (3 tool(s) (lazy, starts on first use)) — it is a working server, not a failed one — and the startup discovery summary counts it as N lazy, not spawned yet.
Dynamic Tool Discovery
MCP servers can notify Nedura when their available tools change at runtime by sending a notifications/tools/list_changed notification. When Nedura receives this notification, it automatically re-fetches the server's tool list and updates the registry — no manual /reload-mcp required.
This is useful for MCP servers whose capabilities change dynamically (e.g. a server that adds tools when a new database schema is loaded, or removes tools when a service goes offline).
The refresh is lock-protected so rapid-fire notifications from the same server don't cause overlapping refreshes. Prompt and resource change notifications (prompts/list_changed, resources/list_changed) are received but not yet acted on.
Reloading
If you change MCP config, use:
/reload-mcp
This reloads MCP servers from config and refreshes the available tool list. It is also the explicit way to re-probe availability-gated tools (Docker, plugin credentials such as HASS_TOKEN, OAuth…): a session's tool set is otherwise frozen, so a credential or daemon that appears mid-session is only picked up on /reload-mcp, /new, or context compaction. For runtime tool changes pushed by the server itself, see Dynamic Tool Discovery above.
A running messaging gateway (nedura gateway run) also watches config.yaml on its own: within about a minute of you removing an mcp_servers entry or setting enabled: false, that server's connection is torn down; a newly added entry is connected. A server whose first connect failed (an unreachable host, or an OAuth server on a headless box that had no token yet) is retried automatically on its connect cooldown schedule (30 s, doubling up to 10 min) once you fix the cause. No restart or /reload-mcp needed for the edit to take effect.
Expired OAuth tokens in the background. The gateway, /reload-mcp, and the periodic self-probe of a parked server never open a browser — nobody is there to complete the flow. When a refresh token dies, the server parks with a warning in gateway.log and you re-authorize once with nedura mcp login <server> (or the Desktop/dashboard Authorize button); the parked server picks the new token up on its next probe. Until the token files change, starting new sessions does not retry the rejected token: only the timed self-probe does, so a dead grant cannot hammer the provider's token endpoint.
Toolsets
Each configured MCP server also creates a runtime toolset when it contributes at least one registered tool:
mcp-<server>
That makes MCP servers easier to reason about at the toolset level.
Security model
Stdio env filtering
For stdio servers, Nedura does not blindly pass your full shell environment.
Only explicitly configured env plus a safe baseline are passed through. This reduces accidental secret leakage.
Config-level exposure control
The new filtering support is also a security control:
- disable dangerous tools you do not want the model to see
- expose only a minimal whitelist for a sensitive server
- disable resource/prompt wrappers when you do not want that surface exposed
Example use cases
GitHub server with a minimal issue-management surface
mcp_servers:
github:
command: "npx"
args: ["-y", "@modelcontextprotocol/server-github"]
env:
GITHUB_PERSONAL_ACCESS_TOKEN: "***"
tools:
include: [list_issues, create_issue, update_issue]
prompts: false
resources: false
Use it like:
Show me open issues labeled bug, then draft a new issue for the flaky MCP reconnection behavior.
Stripe server with dangerous actions removed
mcp_servers:
stripe:
url: "https://mcp.stripe.com"
headers:
Authorization: "Bearer ***"
tools:
exclude: [delete_customer, refund_payment]
Use it like:
Look up the last 10 failed payments and summarize common failure reasons.
Filesystem server for a single project root
mcp_servers:
project_fs:
command: "npx"
args: ["-y", "@modelcontextprotocol/server-filesystem", "/home/user/my-project"]
Use it like:
Inspect the project root and explain the directory layout.
Troubleshooting
MCP server not connecting
Check:
# Verify MCP deps are installed (already included in standard install)
cd ~/.nedura/hermes-agent && python -c "import pm; pm.sync_venv(['mcp'], explicit=True)"
node --version
npx --version
Then verify your config and restart Nedura.
The startup summary in agent.log names every server that did not register, with the recorded
connect error, so you never have to work out the failing one by elimination:
MCP: registered 116 tool(s) from 4 server(s) (2 failed: github (Connection closed); notion (HTTP 401 from POST https://mcp.notion.com/mcp))
A server that was skipped this pass because it is still inside its retry cooldown from an earlier
failure is listed as not attempted (in retry cooldown).
Remote (HTTP) server rejects the connection
nedura mcp test <name> reports what the server actually answered. When the MCP SDK can only say
Server returned an error response (a 4xx/5xx whose body is not a JSON-RPC error), Nedura appends
the HTTP status, the URL it requested and the start of the response body:
Streamable HTTP: Server returned an error response (HTTP 400 from POST http://host:27200/mcp:
{"jsonrpc":"2.0","error":{"code":-32020,"message":"Unsupported MCP-Protocol-Version"}})
Read the status and body first: a 400/405 on the initialize POST usually means the endpoint
speaks SSE only (set transport: sse) or a proxy in front of it rejects the request; a 401/403
means the token or OAuth grant is wrong; an HTML body means the URL points at a web page, not an MCP
endpoint. nedura logs --level debug additionally shows the exact endpoint each connect attempt used.
A server's response is too large
Nedura accepts up to 10 MiB in one HTTP response, or in one SSE event of a streamed response, so a
large tools/list catalog or tool result (an editor screenshot, say) fits. On the legacy SSE
transport (transport: sse) the MCP SDK caps each event at 1 MiB and offers no way to raise it; if
the server also serves Streamable HTTP, remove transport: sse from its entry.
Tools not appearing
Possible causes:
- the server failed to connect
- discovery failed
- your filter config excluded the tools
- the utility capability does not exist on that server
- the server is disabled with
enabled: false
If you are intentionally filtering, this is expected.
Why didn't resource or prompt utilities appear?
Because Nedura now only registers those wrappers when both are true:
- your config allows them
- the server session actually supports the capability
This is intentional and keeps the tool list honest.
Parallel Tool Calls
By default, MCP tools run sequentially — one at a time. If your MCP server exposes tools that are safe to run concurrently (e.g. read-only queries, independent API calls), you can opt-in to parallel execution:
mcp_servers:
docs:
command: "docs-server"
supports_parallel_tool_calls: true
When supports_parallel_tool_calls is true, Nedura may execute multiple tools from that server at the same time within a single tool-call batch, just like it does for built-in read-only tools (web_search, read_file, etc.).
Only enable parallel calls for MCP servers whose tools are safe to run at the same time. If tools read and write shared state, files, databases, or external resources, review the read/write race conditions before enabling this setting.
MCP Sampling Support
MCP servers can request LLM inference from Nedura via the sampling/createMessage protocol. This allows an MCP server to ask Nedura to generate text on its behalf — useful for servers that need LLM capabilities but don't have their own model access.
Sampling is enabled by default for all MCP servers (when the MCP SDK supports it). Configure it per-server under the sampling key:
mcp_servers:
my_server:
command: "my-mcp-server"
sampling:
enabled: true # Enable sampling (default: true)
model: "openai/gpt-4o" # Override model for sampling requests (optional)
max_tokens_cap: 4096 # Max tokens per sampling response (default: 4096)
timeout: 30 # Timeout in seconds per request (default: 30)
max_rpm: 10 # Rate limit: max requests per minute (default: 10)
max_tool_rounds: 5 # Max tool-use rounds in sampling loops (default: 5)
allowed_models: [] # Allowlist of model names the server may request (empty = any)
log_level: "info" # Audit log level: debug, info, or warning (default: info)
The sampling handler includes a sliding-window rate limiter, per-request timeouts, and tool-loop depth limits to prevent runaway usage. Metrics (request count, errors, tokens used) are tracked per server instance.
To disable sampling for a specific server:
mcp_servers:
untrusted_server:
url: "https://mcp.example.com"
sampling:
enabled: false
Tool costs and spend limits
Some MCP servers spend real money or credits on every call (image, video and 3D generation, paid APIs). A server entry can declare what each tool costs and a budget per chat:
mcp_servers:
renders:
command: "render-mcp"
trust: untrusted
tool_costs:
create_image: 20 # about 20 credits per call
animate: {credits: [5, 20], note: "per animation"} # a range, with a short note
get_status: 0 # free; same as leaving it out
spend_limit: 100 # per chat; 0 or absent = no limit
cost_unit: credits # label in prompts (default: credits)
- The approval prompt names the cost:
Costs about 20 credits.orCosts 5-20 credits (per animation)., plusThis chat has used about 40 credits on 'renders' (limit 100).once the chat has spent something. - Nedura keeps a running total per chat and per server, counting the declared figure (the top of a range) for every costed call it approved and started. Declined or blocked calls add nothing; a call whose server dies before receiving it still counts, which errs toward asking sooner. The total lives in memory: it is a budget for one conversation, not the vendor's bill.
- The total follows the conversation when context compression or
/branchcontinues it under a new session id. Starting a new chat (/newin the CLI, a new Desktop chat) starts a new total; on messaging platforms the total belongs to the chat, so it survives/newthere. Each cron run has its own total. Restarting Nedura resets every total. - With
spend_limit, a costed call that would take the chat past the limit asks first, even on atrust: fullserver that otherwise never prompts, and every further costed call in that chat asks again: that prompt offers only Allow Once and Deny, and an earlier Session or Always answer for the tool does not silence it. Where nobody can answer (cron jobs,nedura chat -q, a gateway with no approver), the call is refused without running and the model is told not to retry. - Below the limit, a
trust: fullserver still runs costed tools without asking; on atrust: untrustedserver every write-capable call already asks and the prompt now carries the cost. Tools annotatedreadOnlyHint: truestay unprompted unless the budget requires it. - YOLO (
/yolo,--yolo) andapprovals.mode: offskip thetrust: untrustedprompt (/yolofor that chat,--yolofor that process,approvals.mode: offfor that profile), but not the spend limit: below the limit a costed call runs without asking, and a call that would cross it still asks (or is refused where nobody can answer, such as a cron job withapprovals.mode: off). - A malformed
tool_costs,spend_limitorcost_unitskips the server with an error naming the key, so a budget you declared is never silently dropped. Atool_costskey the server does not offer (a typo, or Nedura'smcp_<server>_display prefix copied in) is named in a warning at connect time: use the server's own tool names.
A portable plugin package can declare the same table for its server. If you add a same-name mcp_servers entry (for example to set trust: full), the package's tool_costs, spend_limit and cost_unit still apply unless your entry sets those keys itself; spend_limit: 0 turns the package's limit off and tool_costs: {} drops its costs.
MCP Elicitation Support
MCP servers can ask the user for structured input mid-tool-call via the elicitation/create protocol (mcp Python SDK ≥ 1.11.0). Nedura routes form-mode elicitations through its existing approval surface — an interactive prompt in the CLI/TUI, or approval buttons on gateway platforms like Telegram and Slack — so the request reaches you wherever the session lives. Each answer covers that one request, so the prompt offers only Allow Once and Deny. YOLO and approvals.mode: off do not answer these for you: the server is asking you a question, not asking permission. URL-mode elicitations (where a server points you at an external URL) are declined as unsupported.
Elicitation is enabled by default per server. Configure it under the elicitation key:
mcp_servers:
my_server:
command: "my-mcp-server"
elicitation:
enabled: true # default: true
timeout: 300 # seconds to wait for your answer (default: 300)
The 5-minute default timeout mirrors the gateway approval default so users on async surfaces have time to respond before the server gives up. Per-server metrics (requests, accepted, declined, errors) are tracked on the handler.
MCP Events (experimental)
Some MCP servers can notify an agent when something happens upstream (a new email, an incident, a
new item in a feed) through the events/* methods of the
MCP events design sketch.
The proposal is still a working-group draft. Nedura implements its webhook delivery mode: the
server POSTs each event to the gateway's webhook listener, and every
event starts an agent run like any other webhook route.
Turn it on per server, and tell Nedura the public HTTPS address of the webhook listener (servers reject plain-HTTP callbacks, so put a TLS-terminating reverse proxy or tunnel in front of port 8644):
mcp_servers:
ambush:
url: "https://example.com/mcp"
events: true # registers the event utility tools for this server
platforms:
webhook:
enabled: true
extra:
public_url: "https://hermes.example.com"
The server then gets three more utility tools:
list_event_types: the event types the server offers, and your current subscriptions to themsubscribe_event: subscribe with the event's arguments, what to do when it arrives (instructions, up to 2000 characters) and where each run's result goes (deliver,deliver_chat_id). You are asked to approve every subscription, since it starts unattended agent runs; the approval shows the instructions and the destination in full.unsubscribe_event: cancel a subscription
Each subscription is stored as a dynamic webhook route (nedura webhook list shows it) with its own
whsec_ signing secret; the grant and the replay cursor are kept beside it in
mcp_event_subscriptions.json, so the route itself only changes when you resubscribe with different
instructions or a different destination. Nedura answers the server's signed verification challenge, deduplicates
events by eventId, and refreshes the subscription before the lifetime the server granted runs out
while the MCP connection is up (and again whenever it reconnects). With lazy: true or
idle_timeout_seconds the connection is not always up, so a subscription can lapse until the next
connection resubscribes it. A refresh the server refuses
for good (access revoked, event type removed, arguments no longer valid) removes the route; other
failures are retried with back-off. Turning the route off in the webhook dashboard lets the
subscription lapse, and turning it back on resubscribes it. If the server ends a subscription,
Nedura removes the route and tells the deliver target. Events only arrive while
nedura gateway run is running. The event payload reaches the run whole, up to 32,000 characters
of JSON, with a note when it is cut. Event payloads are external data: event runs use the same
constrained webhook toolset as other webhook routes.
The listener's per-route rate limit (platforms.webhook.extra.rate_limit, 30 a minute by default)
applies to event deliveries too; raise it for a busy event type, since a server may not retry a
rejected delivery.
Not supported yet: the poll and push delivery modes, and subscriptions made from a secondary
profile under gateway.multiplex_profiles.
Running Nedura as an MCP server
Nedura can also be an MCP server, so Claude Code, Claude Desktop, Cursor, Codex or any MCP client can list and read your messaging conversations, wait for new messages and send messages across every platform Nedura is connected to, and optionally call Nedura's own web, browser, vision, image generation, skills and TTS tools. This ships as the official mcp-serve plugin:
nedura plugins install mcp-serve
{
"mcpServers": {
"nedura": {
"command": "nedura",
"args": ["mcp-serve"]
}
}
}
It used to be built in as nedura mcp serve. An MCP client configured with "args": ["mcp", "serve"] changes them to ["mcp-serve"]; the tool names and parameters are unchanged, and nedura mcp serve now prints the new spelling and the install line. Tools, the --tools mode and its safety limits are documented in the plugin's README.