odek speaks Model Context Protocol in both directions, in one binary:
- Server —
odek mcpexposes odek's built-in tools over stdio to Claude Code, Cursor, or any MCP client. - Client —
mcp_serversin config spawns external MCP servers and registers their tools on the agent as<server>__<tool>.
The odek-extension/v1 contract is optional on top of plain MCP: per-server limits, artifact refs, events, and budgets. A server that only implements MCP 2025-03-26 still works.
odek mcpJSON-RPC 2.0 on stdin/stdout. Logs go to stderr.
Claude Code — user scope in ~/.claude.json, or project scope in .mcp.json at the repo root (not .claude/settings.json):
{
"mcpServers": {
"odek": {
"command": "odek",
"args": ["mcp"]
}
}
}Cursor — the same mcpServers object in ~/.cursor/mcp.json (all projects) or .cursor/mcp.json (this repo).
odek mcp exposes the built-in tool set through tools/list / tools/call, after tools.enabled / tools.disabled. Two tools are never served: delegate_tasks and memory. Surface-only tools (clarify, send_message, bg_*) are not on this path either.
| Always (default config) | Conditional |
|---|---|
shell, patch |
web_search — only when web_search.base_url is set |
read_file, write_file, search_files, glob, file_info |
plan — only when planning is enabled (on by default) |
browser, http_request |
|
diff, tree, head_tail, checksum, base64, json_query, math_eval |
|
session_search, transcribe, vision |
|
skill_load, skill_list, artifact_read, list_subagent_profiles |
|
config_view, list_tools |
transcribe and vision always register; a missing binary fails at call time, not at tools/list.
Configured mcp_servers are loaded into odek mcp too, so a client talking to odek also sees those <server>__<tool> names. tools.enabled / tools.disabled apply after that load.
odek mcp --sandboxSandbox is opt-in here (--sandbox or config sandbox: true). That is not the same as odek run, which defaults the sandbox on. The project-sandbox approval gate still runs. Shell then uses Docker with --cap-drop ALL, --security-opt no-new-privileges, resource limits, and noexec tmpfs.
Same DangerousConfig as odek run. There is no TTY, so prompt-class operations use non_interactive (built-in default read_only: inspection proceeds, writes / exec / egress deny). "deny" blocks prompted work including reads; "allow" runs everything.
{
"dangerous": {
"non_interactive": "read_only"
}
}Stdio JSON-RPC 2.0. Default version 2026-07-28; also accepts 2025-11-25, 2025-03-26, and 2024-11-05.
| Method | Role |
|---|---|
server/discover |
Stateless protocol / capability discovery |
initialize / initialized |
Handshake (legacy clients) |
tools/list, tools/call |
Tool catalog and invocation |
ping |
Health check (empty object) |
resources/*, prompts/* |
Compatibility; odek registers none, so lists are empty |
Requests are capped at 10 MiB. Malformed JSON-RPC is rejected. Tool-handler panics return tool handler panicked with no panic value on the wire or stderr.
A stateless modern tools/call must send protocol 2026-07-28 and client capabilities in _meta. Metadata-free calls are accepted only after a successful initialize.
odek spawns each configured server as a subprocess, sends initialize with protocol 2025-03-26, then tools/list, and registers each tool as <server>__<tool>.
| Surface | MCP client |
|---|---|
odek run, continue, repl, serve, mcp |
Yes. Connect / discover failure aborts the process. |
odek schedule daemon / schedule run |
Yes. Same fail-closed. |
| Trusted / capped sub-agents | Yes. |
| Untrusted sub-agents | No. Adapters do not danger-classify MCP tools. |
odek telegram chat agent |
No. |
Scheduler embedded in odek telegram |
Yes, but a connect failure is non-fatal (the bot keeps running without those tools). |
Put mcp_servers in ~/.odek/config.json (operator-trusted) or ./odek.json (project; extra approval). The command / args / env shape matches Claude Code's mcpServers object. odek does not expand ${VAR} in mcp_servers.*.env.
A server entry may instead set url to use the Streamable HTTP transport — no subprocess. JSON-RPC messages are posted to the endpoint; responses arrive as plain JSON or as an SSE stream (multi-event streams are handled: notification frames are skipped, and the frame whose id matches the request is used). The notifications/initialized handshake notice is sent after initialize (best-effort). Per-server limits, tool-name validation, and per-tool approvals apply identically. Set token_env to the name of an environment variable (resolved from the operator environment, e.g. ~/.odek/secrets.env) holding the Bearer token; the variable must be uppercase MCP_-prefixed (e.g. MCP_REMOTE_TOKEN) — no other secret can be reached through this field — and the token itself never enters config files. Project-level servers get no SSRF host exemption: their URLs may only target public addresses and always require the operator approval prompt, which displays the URL.
{
"mcp_servers": {
"playwright": {
"command": "npx",
"args": ["@playwright/mcp"]
},
"fetch": {
"command": "uvx",
"args": ["mcp-server-fetch"],
"env": { "LOG_LEVEL": "debug" },
"timeout_seconds": 120,
"max_response_bytes": 2097152,
"max_result_chars": 100000,
"artifact_roots": ["/var/ci-artifacts"]
},
"remote": {
"url": "https://mcp.example.com/rpc",
"token_env": "MCP_REMOTE_TOKEN",
"timeout_seconds": 60
}
}
}| Field | Default | Notes |
|---|---|---|
command |
required* | Executable to spawn. *Required unless url is set; when url is set, command/args/env are ignored. |
url |
— | http(s) endpoint for the Streamable HTTP transport. Only absolute http/https URLs with no embedded credentials are accepted. Operator-configured (global) servers' hosts are dialed through odek's SSRF guard with the host allow-listed (internal services reachable); project-configured servers get no host exemption — internal-IP refusal applies — and always require the approval prompt. |
args |
[] |
|
env |
{} |
Overrides; empty string unsets. Secret-looking keys are stripped even here. |
timeout_seconds |
30 |
Per-request; clamped to 3600 (warning). |
max_response_bytes |
10 MiB | One JSON-RPC response line. Config above 64 MiB is rejected and the server is not started. Oversized line: drop it and close the connection. |
max_result_chars |
200000 |
Model-facing result text; clamped to 1_000_000 (warning); values below the floor (truncation notice length + 64 chars) are raised to it (warning). Oversized valid results get a structured truncation notice, never a silent cut. |
artifact_roots |
[] |
Directories that may host file:// artifact refs. Empty rejects every ref. |
auto_approve |
false |
Skip server and per-tool prompts. Honored only from ~/.odek/config.json. |
The four limit fields are odek-extension/v1; semantics live in EXTENSIONS.md.
Environment. Children get a small allowlist (PATH, HOME, USER, LOGNAME, SHELL, TMPDIR, LANG, the listed LC_* locale variables, TZ, TERM) plus env overrides. Names matching secret patterns (APIKEY, TOKEN, SECRET, PASSWORD, PASSWD, PASSPHRASE, CREDENTIAL, CREDS, PRIVATEKEY, ACCESSKEY, COOKIE, AUTHORIZATION after uppercasing and stripping -/_) are removed even from env. Pass auth via the server's own config file or argv, not the parent environment. Child stderr is inherited, so server crashes show up in odek's log. Details: SECURITY.md — MCP hardening.
Registered name is <server>__<tool> (playwright__navigate). Server and tool names must be 1–64 ASCII letters, digits, _, or -, and must not contain __. Invalid names or a collision with a built-in tool (shell, clarify, …) abort startup — they are not skipped with a warning.
Two layers, both fail closed when no TTY and nothing else grants trust.
1. Server spawn — project-level servers only (./odek.json). Global servers in ~/.odek/config.json skip this. Stored in ~/.odek/mcp_approvals.json (0600). The key hashes project directory, server name, command, args, env, and the four extension limit fields (timeout_seconds, max_response_bytes, max_result_chars, artifact_roots); for URL-configured servers it hashes url and token_env instead of command/args/env. Schema and description are not in this key. Fields are length-prefixed, so no byte sequence in a command or argument (including NUL) can make two different launch vectors share a key. Upgrading from a release that used the older key layout asks once to re-approve each persisted project server.
2. Per-tool register — every server, including global. Stored in ~/.odek/mcp_tool_approvals.json (0600). The key hashes project directory, server name, tool name, command, args, env, the four limit fields (or url/token_env for HTTP servers), the canonical-JSON SHA-256 of inputSchema, and the full description. The TTY prompt shows the (sanitized) description plus schema: sha256:… (N bytes) — not the env map. Env values are shown on the server prompt.
Ways to approve:
- Interactive
Approve? [y/N]on a TTY. ODEK_APPROVE_MCP=1for that invocation (all project servers and all tools).- A matching persisted key (any hashed field change re-prompts; older odek keys that omitted the limit fields re-prompt once after upgrade).
auto_approve: truein the global config (see below).
Declining a single tool skips that tool; the rest still load. If server approval is required and cannot be obtained, odek aborts before spawning any MCP server.
Removes both prompts. Schema scans, size caps, name checks, env sanitization, and artifact-ref validation still run.
~/.odek/config.json— honored, and only when the execution fingerprint matches: same command, args, env, timeout, response/result caps, andartifact_roots../odek.json— stripped with a warning. A cloned repo cannot pre-approve its own servers.- A command-less global
{ "auto_approve": true }is not a name wildcard. It is dropped (not a connectable server) and does not bless a project-defined command of the same name. If a project entry swaps command/args/env/limits, globalauto_approvedoes not follow.
Before a tool is registered, every string in inputSchema is injection-scanned (mcp_schema scope). A hit skips the tool (stderr warning). Serialized schemas over 256 KiB are skipped. Description hits withhold the description (placeholder) but keep the tool callable by name. Passing descriptions are still wrapped as untrusted data.
On startup: spawn → initialize (2025-03-26) → tools/list → approve → register. On exit, stdin is closed, the process is given 5s, then killed. Default per-request timeout is 30s when neither the caller nor timeout_seconds sets one.
Connect or discover errors are fatal on run / continue / repl / serve / mcp / standalone schedule — odek shuts down servers already started and aborts rather than running with a partial set. The Telegram-embedded scheduler is the exception (soft-fail).
Stderr from MCP children is shown on odek's stderr. There is no odek: connected MCP server "…" success line.
A server can return an odek.tool-result/v1 envelope with file:// refs instead of bulk content. Validation is fail-closed (schema, absolute path, symlink-resolved containment in artifact_roots, hash/size, 64 MiB / 64 refs). The model sees metadata only — never the path or file bytes. Empty artifact_roots (or one holding only blank strings) rejects every ref. Rendered metadata (media_type, summary) has control, bidi and line-separator characters flattened to spaces. See EXTENSIONS.md.
Any stdio MCP server that implements tools/list and tools/call works (Playwright, Fetch, GitHub, filesystem, …).