reasonix-mcp
Bridges MCP clients to interactive Reasonix agents, letting you spawn, steer, poll, resume, and manage agents via tools and callbacks. Enables orchestrating long-running AI agents from any MCP host.
README
reasonix-mcp — spawn and steer Reasonix agents from any MCP client
An MCP server that bridges any MCP host (Claude Code, Codex, …) to live,
interactive Reasonix agents. Your MCP client is the front-end; this project
adds the server it talks to, plus a detached daemon that owns the agents.
Each spawned agent runs reasonix acp
(Agent Client Protocol v1, NDJSON
JSON-RPC over stdio) in a subprocess, rooted in your project, under your own
Reasonix config and provider credentials.
How it works
MCP host (Claude Code, Codex, …) ──(MCP stdio)──▶ launcher.py ──▶ server.py ──(Unix socket JSON-RPC)──▶ agentd ──(ACP stdio)──▶ reasonix acp ──▶ agents
│ spawn/send/watch/poll/list/… ▲ owns the subprocesses
└─ blocking watch + elicitation ──────┘
launcher.py is the per-orchestrator MCP supervisor; server.py is a thin
MCP front-end; agentd.py is a detached daemon that
owns the agents. This split is what makes agents survive: close the MCP host
(or kill the MCP server) and the fleet keeps running in the daemon — a new
server reconnects to the same socket and reasonix_list shows everything
still there. agentd is auto-started by the server on first use
(socket: ~/.reasonix-mcp/agentd.sock, log beside it) and stops only when
shut down (killing the daemon kills its agents — they carry PDEATHSIG).
- Spawn returns a
session_idimmediately; the agent works in the daemon. - Send steers a running agent mid-turn via
_reasonix.io/session/steer; if idle it starts a new turn. - Poll returns recent output since last poll: text, turns, plan, events, permission requests, stop reason, and terminal error text when applicable.
- Cleanup can stop completed agents after an idle grace period; it is
disabled by default. Use
keep_alive=truefor interactive follow-up turns. - Resume revives a stopped/crashed session from its persisted transcript.
- Stop cancels + closes + kills; the session stays listed and resumable.
- Wake-up: keep
reasonix_watchin flight. It returns compact terminal or permission results directly, with no timeout and no follow-up poll by default. Wire notifications are diagnostics only.
Setup
cd ~/reasonix-mcp
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
Requires reasonix on PATH (verified: v1.17.20) and a configured provider
(reasonix setup).
Register with your MCP client
Project scope (this directory): the included .mcp.json is picked up
automatically when the MCP host runs in ~/reasonix-mcp.
User scope (available in every project) — example with Claude Code:
claude mcp add reasonix --scope user -- \
/home/asmodeus/reasonix-mcp/.venv/bin/python \
/home/asmodeus/reasonix-mcp/src/reasonix_mcp/launcher.py
Other MCP hosts (Codex, etc.) register stdio servers through their own
mcp add equivalents — same command + args, different client. Then restart
your MCP client and verify the server is listed.
Tools
| Tool | Purpose |
|---|---|
reasonix_spawn(task, cwd?, model?, work_mode?, tool_approval?, effort?, keep_alive?, idle_timeout?) |
Start an agent in the daemon on task; returns session_id + sandbox posture. Completed agents can be cleaned up after the idle grace period when cleanup is enabled, unless keep_alive=true. |
reasonix_resume(session_id, cwd?, keep_alive?, idle_timeout?) |
Revive a stopped/crashed session from its persisted transcript. |
reasonix_models() |
List selectable models: provider/model refs, default, per-model supported_efforts, and price hints where configured. |
reasonix_send(session_id, message, expect?) |
Forced steer: queue as mid-turn guidance, or start a new turn if idle. Never dropped. expect="steer" refuses to start a new turn. |
reasonix_poll(session_id, include_events?, exclude_events?, include_thought?, include_full?, max_events?) |
New output / status / completed turns / current plan / pending permission request. Static boilerplate is filtered and events are a recent tail by default. |
reasonix_transcript(session_id, max_tool_calls?) |
What the agent actually did: tool calls with args, files touched (write/read/bash), roles, work duration, last text — powers rebase decisions. (Reasonix does not persist token/cost usage; these are activity metrics.) |
reasonix_watch(session_ids, timeout?, detail?, …poll options) |
Primary callback: block indefinitely by default until completion, permission/question, or process death. Returns a compact result; detail=true opts into the poll-shaped result. |
reasonix_wait(session_ids, timeout?) |
Block until any watched session produces output, finishes a turn, or raises a permission request. |
reasonix_list(include_task?) |
All live sessions: id, status, cwd, compact task preview, transcript_path. Set include_task=true for full prompts. |
reasonix_respond_permission(session_id, option_id) |
Answer a tool-approval request (option_id from watch/poll's permission_request.options, or "cancel"). |
reasonix_stop(session_id) |
Cancel + close + kill the agent (tombstone: poll keeps reporting exited). |
reasonix_restart_agentd(force?) |
Explicitly reload the detached daemon. Source changes already queue a safe automatic reload; force=true may terminate live agents. |
reasonix_restart_mcp_server() |
Explicitly restart this orchestrator's MCP server through launcher.py; source changes are watched automatically. |
Completion and decision delivery
Keep reasonix_watch(session_ids) in flight while agents work. It returns when
a child finishes, exits, or needs a decision, with each compact result in
results[session_id]. By default each result contains only status,
stop reason, one capped message, permission details, plan/current work, errors,
and transcript path. Its timeout is disabled by default and it does
not wake for ordinary text chunks, so no timer loop or follow-up poll is needed.
Use one non-overlapping watch per orchestrator fleet. If another child finishes
while a result is being handled, its terminal state remains pending and the
next watch returns it immediately. Overlapping watches on the same session are
rejected to prevent duplicate permission delivery. If the MCP host or server
restarts during a watch, reconnect, recover the owned fleet with
reasonix_list, and watch it again; undelivered terminal state remains pending
in agentd.
Compact watch consumes queued events so long-running sessions stay bounded.
Use reasonix_watch(..., detail=true) when raw event/turn detail is wanted
immediately. Full accumulated text remains available afterward through
reasonix_poll(include_full=true), and tool history through
reasonix_transcript.
For diagnostics, the server also emits this custom notification on the wire:
{"method": "reasonix/agent_event", "params": {
"session_id": "…", "event": "turn_end", "status": "idle",
"stop_reason": "end_turn", "transcript_path": "…", "note": "…"}}
- Events:
status(plan/current-work update),turn_end(done/stopped/errored, withstop_reason),permission_request(a tool-approval or an agent question — see below),process_exited. - Diagnostic only: JSON-RPC clients silently ignore unknown notifications,
and even a standard MCP notification is delivered to the client application,
not injected into the model's conversation. Do not build orchestration loops
around either
reasonix/agent_eventornotifications/message.
Orchestrator loop:
reasonix_watch(session_ids)→ handle each result → answer a permission or remove a terminal id → watch the remaining ids again.reasonix_waitremains available as a legacy output-sensitive long poll; unlike watch, it requires a follow-upreasonix_poll.
- Diagnostic notifications are always emitted. The daemon and MCP server log each emit/relay result so host-side dropping is distinguishable from a server-side omission.
The Reasonix [notifications].enabled setting controls Reasonix CLI/desktop
system notifications; it is separate from this MCP transport.
Errored turns and unexpected process exits include error_text in the push
payload. reasonix_watch returns the same detail directly, so an
orchestrator can decide whether to resume or retry without inspecting the
session JSONL by hand.
- Verified at the wire level by
selftest_notifications.py(raw JSON-RPC client — the SDK client validates server notifications against known types and may drop custom ones).
Agent questions
The agent can put a structured multiple-choice question to the orchestrator
mid-task via its built-in ask tool — and YOLO does not bypass it (a
question is a genuine user decision, not a tool approval). Questions ride the
same session/request_permission channel (kind "other"), so they surface
identically:
{"permission_request": {"request_id": 7,
"tool_call": {"title": "Which approach should we take?", "kind": "other",
"toolCallId": "ask-1-q1", "rawInput": {"id":"q1","options":[…],"question":…}},
"options": [{"optionId": "q1:1", "name": "A"}, {"optionId": "q1:2", "name": "B"}, …]}}
Clients that advertise MCP elicitation receive a standard
elicitation/create form and the selected option is returned to Reasonix
automatically. If the client declines or cancels that form, Reasonix leaves
the ACP request pending; dismissal is not treated as rejection. Watch and
answer with reasonix_respond_permission(session_id, "q1:1"); the chosen label becomes
the ask tool result and the agent continues. A diagnostic agent_event is
also emitted. Verified live by selftest_question.py.
Spawn defaults
Spawned agents run at effort = max on opencode-go/deepseek-v4-flash
with tool_approval = yolo by default (the user's requested defaults). All
spawn options are per-call overridable, and the defaults themselves are
env-overridable (REASONIX_MCP_DEFAULT_MODEL, REASONIX_MCP_DEFAULT_EFFORT,
REASONIX_MCP_DEFAULT_WORK_MODE, REASONIX_MCP_DEFAULT_TOOL_APPROVAL).
Model selection: pass reasonix_spawn(model="<provider>/<model>", ...) to
pick the agent's model per call — reasonix_models() lists the valid refs and
each model's supported_efforts (effort is per-model: e.g. kimi-k3 accepts
only high/max; an unsupported value fails at spawn). Some gateway models
bake effort into the id (omniroute/codex/gpt-5.6-luna-{low,medium,high,xhigh, max}): they advertise no effort config option, so spawn skips it and
reports skipped_options in the result — pick the variant id instead.
reasonix_send is forced steer: a message is always delivered — queued as
mid-turn guidance while a turn is running, or submitted as a new turn if the
agent is idle (or the turn ended mid-race). Messages are never dropped.
expect (any default) narrows that: expect="steer" raises instead of
accidentally starting a new turn; expect="new_turn" raises if the message
was steered into a running turn.
| Option | Values |
|---|---|
model |
any configured provider/model, e.g. opencode-go/deepseek-v4-flash |
effort |
auto · disabled · high · max |
work_mode |
economy (lean tool surface) · balanced (complete default) · delivery (requires acceptance criteria + review/verification evidence) |
tool_approval |
ask · auto · yolo (default) |
Poll is lean (orchestrator-friendly)
A spawned session emits a burst of static setup events
(available_commands_update — 24 slash commands with descriptions — and
config_option_update — the full model catalogue). Unfiltered, that is
~31 KB / ~8k tokens per poll for a 4-byte reply (measured), which kills
parallel orchestration. By default reasonix_poll omits those two types
from events (they are implied by spawn and available via transcript_path);
events_filtered counts what was omitted. To change the filter:
include_events=["tool_call","tool_call_update","plan"]— only these sessionUpdate types (permission requests are always included);- Ordinary polls return a small recent event tail (50 by default; set
max_eventsto choose another value).include_eventsopts named types back in, including static setup types when explicitly named. exclude_events=[...]— drop additional types;- the orchestrator-relevant set is
tool_call,tool_call_update,plan,permission_request.
Every poll also includes current_work: the active native tool call when one
is running, or the plan step marked in_progress. Agents spawned through this
server receive a small status contract asking them to keep that plan current.
It is injected exactly once per persisted session: follow-up turns do not
repeat it, and resume detects it in session history. Explicit task restrictions
on tools take precedence.
turns in poll results gives completed turns as [{text, stop_reason}] —
clean turn boundaries (full_text alone concatenates turns).
Thought and full_* are opt-in. Reasoning is the bulk of what effort=max
models emit, so reasonix_poll does not return thought / full_thought /
full_text by default — it returns only what changed (text delta, turns,
events, status). They stay accumulated server-side and are available on demand:
include_thought=True→thought(delta) +full_thoughtinclude_full=True→full_text(whole conversation;turnsusually suffices — per-turn text + stop_reason)
Use include_thought when diagnosing a derailed agent. Defaults are
env-overridable: REASONIX_MCP_INCLUDE_THOUGHT=1, REASONIX_MCP_INCLUDE_FULL=1.
Parallel orchestration
spawn 6–8 agents (note session_ids, each spawn reports its sandbox posture)
loop:
event = reasonix_watch(all ids) # no timeout; compact terminal/permission results
handle event.results # no follow-up poll
reasonix_send(sid, msg, expect="steer") when a discovery invalidates a round
drop finished ids from the watch list; reasonix_stop() the rest when done
reasonix_list() whenever you lose track of session_ids
Caps & truncation
Poll output is capped so long gaps can't blow up the MCP host's context; the cuts are reported, never silent:
| Field | Limit (env override) |
|---|---|
events |
recent tail (50 by default; explicit cap 200) — events_dropped counts the cut |
text |
last MAX_DELTA_TEXT (100k) chars — text_truncated |
compact watch message |
first/last MAX_WATCH_MESSAGE (4k) chars — message_truncated |
thought / full_thought (only with include_thought) |
last MAX_FULL_TEXT (200k) chars — *_truncated |
full_text (only with include_full) |
last MAX_FULL_TEXT (200k) chars — full_text_truncated |
events_dropped (queue/size cap) is distinct from events_filtered
(static-type omission). The unpolled in-server event buffer is bounded at 4000
chunk events; critical events (permission, turn end, process exit) are never
dropped.
Sandbox posture
reasonix_spawn reads and returns the effective [sandbox] posture at spawn
time (sandbox, legacy bash, allow_write, network, workspace_root,
config_file) so an orchestrator knows up front whether agents can execute
commands and write outside cwd. Note Reasonix semantics: bash = "off" means
unconfined (execution allowed), while bash = "enforce" jails commands in
bubblewrap when available. The clearer posture names are sandbox = "bwrap"
or sandbox = "none"; with none, allow_write cannot be enforced for bash
and a warning is returned/logged. Changing config while an agent runs does not
change that agent; inspect the spawn response for its effective posture. Under
tool_approval = "ask", gated commands raise a
permission_request in watch/poll — answer with reasonix_respond_permission;
approving blind is not required: the request's tool_call carries the tool
name (title/kind) and rawInput (the JSON arguments).
Ask mode does not pause every shell command: Reasonix requests permission only
for commands its policy classifies as gated. Its explicit ask tool always
creates a user-decision request, including in YOLO mode.
Updating the daemon
agentd watches agentd.py, acp_bridge.py, and common.py. A source change
queues an automatic restart; active turns, pending decisions, and
keep_alive=true sessions are never interrupted. Once agents are safely idle
and their terminal output has been polled, the daemon waits a short grace
period, closes resumable idle processes, and starts a fresh daemon from current
code. No MCP reinstallation is needed.
For explicit control, call reasonix_restart_agentd(). It safely reloads the
shared daemon when no live agents remain. If live agents can be discarded, use
reasonix_restart_agentd(force=true); their persisted transcripts can be
resumed after the fresh daemon starts. The next tool call automatically starts
the new daemon, so no shell command or socket cleanup is needed. The daemon is
shared by MCP clients, so a restart disconnects other clients too; they
reconnect automatically on their next tool call.
Restarting the MCP server
launcher.py watches the package's Python sources and replaces its MCP server
automatically after a change. It proxies stdio, replays the negotiated MCP
handshake into the new child, and leaves shared agentd sessions untouched,
so an already-running orchestrator can continue without being restarted.
Every orchestrator has its own launcher and refreshes independently.
reasonix_restart_mcp_server() remains available for an explicit reload.
Registrations that point directly to server.py must be changed to
launcher.py once; after that, source updates need no MCP reinstall.
Orchestrator isolation
Sessions are scoped to the MCP orchestrator that created them. reasonix_list
and all session operations only expose that orchestrator's sessions. The scope
is stable across MCP server restarts and is derived from the MCP client's name
and workspace. If multiple instances of the same CLI run in the same workspace,
set a distinct stable value in each environment:
REASONIX_MCP_ORCHESTRATOR_ID=project-a-cli-1
The daemon is shared, but ownership is enforced by the daemon and persisted
with each session. reasonix_restart_agentd remains a global operation because
restarting the shared daemon affects every orchestrator; it refuses while
another orchestrator has live agents unless force=true.
Agent cleanup
After a terminal turn (including an errored turn), an agent remains available
for the idle grace period (REASONIX_MCP_IDLE_TIMEOUT, disabled by default, or
idle_timeout per spawn) so the orchestrator can poll its final output or send
a quick follow-up. It is then stopped and remains as an exited, resumable
tombstone. Set keep_alive=true on reasonix_spawn for an agent that needs
ongoing interactive turns; call reasonix_stop when it is no longer needed.
Use idle_timeout=-1 to disable cleanup, 0 for immediate cleanup after a
terminal turn, or a positive number of seconds for a grace period.
Safety
- Agents run under your Reasonix permissions and workspace sandbox
(writes confined to
cwd+allow_write; bash jailed where the OS sandbox is enabled and available). Withsandbox = "none"/bash = "off", bash is unjailed andallow_writeis not enforceable. - Spawn cwd is confined:
reasonix_spawn(cwd=…)is rejected unless the target is the MCP host's project dir (or a subdir) or one of the[sandbox] allow_writedirs — a prompt-injected orchestrator can't spawn agents that write anywhere (the server runs with your full permissions andyolodefault). Escape hatch:REASONIX_MCP_ALLOW_ANY_CWD=1. - Agents die with the server: PDEATHSIG guarantees a killed MCP server
can never orphan
reasonix acpprocesses. tool_approvalper spawn:yolo(default; approve except protected decisions),auto(follow configured permission rules), orask(relay every approval throughreasonix_respond_permission).reasonix_spawnreturns the effective sandbox posture — check it before assigning tasks that require running commands.cwddefaults to the MCP host's project root; pass an explicitcwdto scope an agent elsewhere.
Long-running agents
A spawned agent may legitimately run for hours — long thinking, tool loops, implementing across many files. The bridge is built for that:
- Nothing blocks.
reasonix_spawn/reasonix_send/reasonix_pollreturn immediately; the agent works in its own subprocess. The MCP host stays fully responsive while the agent grinds. - No default timeout. A turn runs until it finishes or you call
reasonix_stop(cancels the turn, closes the session, kills the process). - You can come back anytime.
reasonix_pollreportsstatus: "running"with everything new since your last poll — leave it for an hour, then check again.reasonix_sendsteers even mid-turn. - Memory is bounded. Unpolled chunk events are capped in the server; poll
results cap
text/thought/full_textand the structuredeventslist (tails kept,*_truncated/events_droppedflags report the cut) so a long gap can't blow up the MCP host's context.
One constraint, now mostly lifted: agents live in the daemon, not the MCP
server — the MCP host can close and come back and the fleet is still running
(reasonix_list). The daemon itself is the survival boundary: kill it and its
agents die (PDEATHSIG); a crashed session's work survives on disk and can be
revived with reasonix_resume.
Testing
.venv/bin/python tests/selftest.py # spawn → poll → steer → stop (real provider)
.venv/bin/python tests/selftest_daemon.py # survival across server kill + resume + 6-way concurrency (real provider)
.venv/bin/python tests/selftest_permission.py # ask-mode permission round-trip (real provider)
.venv/bin/python tests/selftest_question.py # agent asks a question via `ask` (real provider)
.venv/bin/python tests/selftest_transcript.py # transcript + plan fields (real provider)
.venv/bin/python tests/selftest_orchestrator.py # list/wait/filtering/posture (no model calls)
.venv/bin/python tests/selftest_notifications.py # diagnostic event frames (no model calls)
.venv/bin/python tests/selftest_elicitation.py # ACP decision → MCP elicitation bridge (no model calls)
.venv/bin/python tests/selftest_watch.py # watch overlap/cancellation safety (no model calls)
.venv/bin/python tests/selftest_mcp_restart.py # manual + source-change hot reload (no model calls)
.venv/bin/python tests/selftest_auto_reload.py # agentd source watcher + self-replace (no model calls)
.venv/bin/python tests/selftest_prompt_injection.py # one status contract per session (no model calls)
.venv/bin/python tests/selftest_chaos.py # cwd allowlist + dual notify + PDEATHSIG (no model calls)
.venv/bin/python tests/selftest_allow_write.py # cross-cwd write via allow_write (real provider)
The selftest runs fully isolated: it copies config.toml + .env into a
scratch REASONIX_HOME under /tmp (removed on exit), so it never touches
your live ~/.reasonix sessions and spawns the native Reasonix Go binary
directly (never the npm node shim) in its own process group that it kills on
cleanup.
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。