Install
$ agentstack add mcp-jtianling-cross-agent-teams-mcp ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ● Network access Used
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
cross-agent-teams-mcp
[中文说明](./README.zh-CN.md)
A local MCP daemon that lets multiple AI coding agents (Claude Code, Codex, opencode) running on the same machine talk to each other. Agents register, send 1-to-1 messages, broadcast to a team or role, and wake each other up — all over a single daemon, no external services.
Why not just use Claude Code's agent teams?
Claude Code already ships its own agent teams feature. cross-agent-teams overlaps with it on the surface, but solves a different problem. Three concrete reasons to reach for this project:
Cross-agent support. Claude Code's agent teams are tied to Claude Code itself — every member is a Claude Code sub-agent. cross-agent-teams lets you mix different agents in the same team: a Claude Code agent, a Codex agent, an opencode agent, a Cursor agent, etc., all coordinating through one daemon. Use the agent that's best suited for each role instead of being locked to one harness.
Better persistence and controllability. In this design, each agent process is started and stopped manually. That's more cumbersome than implicit spawn-on-demand, but it's also much more controllable and persistent — agents keep their own long-running context, memory, and conversation state instead of being recreated from scratch every time the orchestrator decides it needs them. You can leave a specialist agent running for hours or days and keep talking to the same session.
Cross-device / cross-user collaboration. The daemon recently grew support for building teams across physical machines (see [section 4](#4-cross-host--cross-device-collaboration)). That means you can coordinate with agents running on a teammate's laptop, where different people may own different specialized agents or workflows — something a single-process in-harness teams feature can't reach.
Quick start
Recommended: let a code agent set it up
The whole device setup (zshrc launchers, daemon token, codex/opencode config) is written as an agent-readable runbook: [README.agent.md](README.agent.md). Paste this to any code agent that can fetch URLs and run shell commands:
Read https://raw.githubusercontent.com/jtianling/cross-agent-teams-mcp/HEAD/README.agent.md
and follow it to set up xats on this device.
The agent will confirm a device label and the ~/.zshrc changes with you, auto-generate the daemon token on first start-xats, and wire up the free-xats-codex / xats-codex / free-xats-opencode / xats-opencode launchers plus start-xats / stop-xats. Prefer doing it by hand? Continue below.
Claude Code
# 1. Start the daemon (run once, keep it alive)
npx -y cross-agent-teams-mcp@latest daemon --port 9100 &
# 2. In your project, install the MCP config
npx mcpsmgr add jtianling/cross-agent-teams-mcp -a claude-code
# 3. Start Claude Code with the channel loader (manual permission prompt expected)
claude --dangerously-load-development-channels server:cross-agent-teams-channel
Other agents (Codex, opencode, ...)
# 1. Start the daemon (run once, keep it alive)
npx -y cross-agent-teams-mcp@latest daemon --port 9100 &
# 2. In your project, install the MCP config (interactive picker)
npx mcpsmgr add jtianling/cross-agent-teams-mcp
# 3. Start your coding agent as usual
Note: Claude Code gets push wake out of the box. Codex and opencode get real push wake too — each after a one-time launcher setup (see section 2 below): Codex over its --remote app-server transport, opencode over its HTTP prompt_async transport. cursor / other custom agents only receive pokes when running inside a tmux pane. If push wake isn't wired up for an agent, ask it to check its inbox manually ("check my xats inbox").
Then talk to your agent in plain language:
# In agent A:
Register me to xats as backend on team default.
# In agent B:
Register me to xats as frontend on team default.
Send backend a message: the API has changed.
That's it. Sections below cover the details — daemon flags, manual MCP config, codex --remote setup, more usage patterns.
1. Start the daemon
Run this once on your machine and keep the process alive (dedicated terminal, tmux, screen, launchd — your call):
npx -y cross-agent-teams-mcp@latest daemon --port 9100
The daemon listens on 127.0.0.1:9100. MCP endpoint is http://127.0.0.1:9100/mcp, health endpoint is http://127.0.0.1:9100/health.
Common flags:
--port(default9100)--host(default127.0.0.1)--device(default: hostname-derived label)--token(Bearer auth)--db(default~/.cross-agent-teams-mcp/data.db)--pid-file(default~/.cross-agent-teams-mcp/daemon.pid)
For multi-host / multi-device setups (LAN, tailscale, etc.), see [section 4](#4-cross-host--cross-device-collaboration) below.
2. Configure your agent's MCP client
Recommended: mcpsmgr (shown in Quick start)
mcpsmgr reads this repo's mcpsmgr.json and writes the right MCP entries into your agent's config in one shot — including the Claude Code stdio channel proxy entry, the Codex experimental_use_rmcp_client toggle, and the streamable-http MCP entry.
To override the daemon port:
npx mcpsmgr add jtianling/cross-agent-teams-mcp -a claude-code --port 9300
Manual config
If you don't want mcpsmgr (private fork, custom token, custom stdio args, or you just prefer hand-edited config), the raw per-agent configs are below.
Claude Code (needs both entries — HTTP for tools, stdio for channel wake)
.mcp.json (or ~/.claude.json):
{
"mcpServers": {
"cross-agent-teams": {
"type": "http",
"url": "http://127.0.0.1:9100/mcp"
},
"cross-agent-teams-channel": {
"command": "npx",
"args": [
"-y",
"-p",
"cross-agent-teams-mcp@latest",
"cross-agent-teams-channel",
"--daemon-url",
"http://127.0.0.1:9100/mcp"
]
}
}
}
Then start Claude Code with the experimental channel loader so it subscribes to the proxy's wake notifications:
claude --dangerously-load-development-channels server:cross-agent-teams-channel
The server: suffix MUST equal the MCP server key in .mcp.json (cross-agent-teams-channel above). If your daemon uses --token , add "headers": { "Authorization": "Bearer " } to the HTTP entry, and add --token to the channel proxy args.
Codex CLI
Codex talks to the daemon over Streamable HTTP. Wake-ups go through Codex's own app-server WebSocket transport — there is no channel proxy involved.
Minimum config (mailbox only, no push wake)
~/.codex/config.toml:
experimental_use_rmcp_client = true
[mcp_servers.cross-agent-teams-mcp]
type = "streamable-http"
url = "http://127.0.0.1:9100/mcp"
experimental_use_rmcp_client = true MUST sit at the top level — without it, streamable-http MCP servers fail to load.
When the daemon was started with --token : export XATS_TOKEN= in the shell that launches codex, then add bearer_token_env_var = "XATS_TOKEN" to the [mcp_servers.cross-agent-teams-mcp] block. (Codex 0.130+ silently ignores the older [mcp_servers.X.headers] form — its accepted keys are http_headers and bearer_token_env_var, and bearer_token_env_var is preferred so the token never lands in a checked-in config.)
In this minimum mode, send_message to this Codex still drops a row in its mailbox, but you have to call get_inbox yourself to read it — no push wake.
Let other agents wake you (codex-appserver poke)
To let other agents wake this Codex thread (not just mail it), you need codex-appserver delivery. The setup has one non-obvious gotcha worth calling out:
> In codex --remote mode, MCP servers are loaded by the app-server, NOT by the TUI. The MCP entry above must therefore live in the CODEX_HOME that the app-server reads at startup — usually the global ~/.codex/config.toml. Setting CODEX_HOME on the TUI alone does nothing for MCP under --remote.
Start order:
# 1) Long-lived codex app-server somewhere (its CODEX_HOME decides the MCP set).
codex app-server --listen ws://127.0.0.1:8799
# 2) Codex TUI in a separate terminal, connected to the same app-server.
codex --remote ws://127.0.0.1:8799
If the app-server's CODEX_HOME doesn't have cross-agent-teams-mcp configured, the codex agent inside --remote won't see the MCP tools at all and register_agent will never fire.
Recommended: launcher with tmux pane auto-bind
For pokes to be injected directly into the running Codex thread (rather than landing as a tmux paste), the daemon needs to know which tmux pane the codex process lives in. The launcher pre-claims a pane via the pre-register-codex-pane CLI before exec'ing codex. Add to ~/.zshrc:
free-xats-codex() {
local xats_agent_id codex_home search_dir
xats_agent_id="$(uuidgen)"
search_dir="$PWD"
while [[ "$search_dir" != "/" ]]; do
if [[ -f "$search_dir/.codex/config.toml" ]]; then
codex_home="$search_dir/.codex"
break
fi
search_dir="${search_dir:h}"
done
if [[ -n "$TMUX_PANE" ]]; then
npx -y cross-agent-teams-mcp pre-register-codex-pane \
--pane "$TMUX_PANE" \
--agent-id "$xats_agent_id" \
>/dev/null 2>&1 \
|| echo "[xats] pre-register failed (continuing without pane claim)" >&2
fi
if [[ -n "$codex_home" ]]; then
CODEX_HOME="$codex_home" exec codex \
--remote ws://127.0.0.1:8799 \
-C "$PWD" \
-c xats.agent_id="\"$xats_agent_id\"" "$@"
else
exec codex \
--remote ws://127.0.0.1:8799 \
-C "$PWD" \
-c xats.agent_id="\"$xats_agent_id\"" "$@"
fi
}
What the launcher does:
- Inside tmux (
$TMUX_PANEset): pre-registers the pane → uuid mapping with the daemon (120s TTL). When the codex agent later callsregister_agent({agent_type: "codex", thread_id: $CODEX_THREAD_ID, ...}), the daemon resolvestmux_pane_idautomatically by matching the pre-reg against the codex argv. --remote ws://127.0.0.1:8799connects to the long-lived app-server from step (1) above.-c xats.agent_id="\"$uuid\""exposes the uuid in codex's argv so the daemon can verify the pane.
More detail (auth headers, lower-level register_agent form): [docs/configs/codex-cli.md](docs/configs/codex-cli.md).
opencode
opencode ships a first-class headless HTTP API (POST /session/{id}/prompt_async) that the daemon uses as a dedicated wake-up transport — no tmux pane injection required. The transport is activated by registering with agent_type="opencode" and a base_url pointing at the opencode process's HTTP server.
When another agent pokes this opencode, the daemon POSTs the wake hint to prompt_async, which starts a fresh opencode agent turn — the agent wakes on its own and reads its inbox with no manual prompt, the same first-class push-wake that Claude Code and Codex get (not the passive tmux paste that custom agents fall back to).
Add a free-xats-opencode zsh function to ~/.zshrc (mirrors the free-xats-codex pattern):
free-xats-opencode() {
local port
port="$(node -e 'const s=require("net").createServer();s.listen(0,"127.0.0.1",()=>{console.log(s.address().port);s.close()})')"
OPENCODE_XATS_BASE_URL="http://127.0.0.1:${port}" exec opencode --port "${port}" --hostname 127.0.0.1 "$@"
}
Then replace plain opencode with free-xats-opencode:
free-xats-opencode # default agent
free-xats-opencode --agent build --model glm-5.2 # args pass through
What the launcher does:
- Allocates a free TCP port on
127.0.0.1(supports concurrent opencode instances without port conflicts). - Exports
OPENCODE_XATS_BASE_URL=http://127.0.0.1:so the agent's Bash tool can read it and pass it asbase_urltoregister_agent. exec opencode --port --hostname 127.0.0.1starts the TUI with its HTTP server bound to loopback.
Inside the opencode TUI say:
> 注册到 xats, name: oc-1, team: default
The agent detects $OPENCODE_XATS_BASE_URL, picks agent_type="opencode" automatically, passes the env value as base_url, and omits session_id (the daemon auto-resolves it as the most recently updated session on that base_url). auth_token_ref is only required when the opencode server was started with OPENCODE_SERVER_PASSWORD set; in that case also pass auth_token_ref: "OPENCODE_SERVER_PASSWORD".
If you launch opencode via plain opencode (without the wrapper), the env var is absent, the agent falls back to agent_type="custom" with agent_type_name="opencode", and pokes are delivered via tmux pane injection (see next section).
Other coding agents (cursor, ...)
Anything that is not Claude Code, Codex, or opencode-via-launcher — cursor, an editor extension, your own harness — connects over plain Streamable HTTP and registers as agent_type="custom" (the agent figures this out for you). There is no dedicated wake-up transport for these; cross-agent pokes are delivered by injecting text into the agent's tmux pane, so run the agent inside a tmux window and the daemon will resolve pid → tty → pane automatically when you register.
Per-tool config snippets live in [docs/configs/opencode.md](docs/configs/opencode.md) (and docs/configs/ for the rest).
3. Use it from your agent
Once your agent is connected to the daemon, you don't have to memorize tool names. Just talk to the agent in plain language and it will pick the right tool — the README below shows the kinds of things you say, not the underlying API.
> Note: always run these from inside the agent session. Don't hand-drive the MCP protocol with curl (or any other external HTTP client) to register or send — that opens a different MCP session, and worse, a curl register_agent triggers a cross-session takeover that force-closes your real session. If your MCP client transport is dead and you just need a lifeboat, use the loopback-only REST API below instead — it never touches your session.
Lifeboat: the loopback REST API. If an agent's MCP client transport breaks, it can no longer call any xats tool — not even to say it is stuck. For exactly that case the daemon exposes a tiny, loopback-only REST surface on the same port under /api/. It resolves the agent by (team, name), reuses the same send / inbox / list-agents logic as the MCP tools, and has zero session side-effects (no takeover — safe even while your MCP session is still alive). Remote callers get 403 by design; if the daemon was started with --token, present it just like /mcp (Authorization: Bearer or ?token=).
# send as an already-registered agent
curl -s http://127.0.0.1:/api/send \
-H 'content-type: application/json' \
-d '{"from":{"team":"default","name":"alice"},
"to":{"team":"default","name":"bob"},
"body":"my MCP client is wedged — restarting"}'
# read your inbox — omit since_event_id to advance your read cursor,
# or pass it for a read-only peek that does NOT advance the cursor
curl -s 'http://127.0.0.1:/api/inbox?team=default&name=alice'
# list a team's agents
curl -s 'http://127.0.0.1:/api/agents?team=default'
There is deliberately no register_agent over REST — creating or rebinding an identity is the very takeover footgun this surface avoids, so an agent must have registered once (over MCP) before it can use the lifeboat.
> Security note: "loopback-only" includes a browser running on the same machine, so run the daemon with --token to keep a local web page from reaching /api/. Without a token, the worst a malicious local page can do is advance an agent's inbox cursor via a cross-site GET /api/inbox — it cannot read any response (CORS), send, or i
…
Source & license
This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: jtianling
- Source: jtianling/cross-agent-teams-mcp
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.