acpbot injects MCP servers into each ACP session on ensure / session/new:
- Per-repo servers from
<repo>/.acpbot/mcp.json(optional profile filter) - Built-in server named
acpbot(host tools: speak, Telegram send, schedules)
The name acpbot is reserved.
Built-in host tools (server acpbot)
| Tool | Purpose |
|---|---|
speak | TTS → voice note in the topic |
update | Primary mid-turn progress channel — edits the live working bubble (⏳ …), not a new spam message |
telegram_send | Permanent mid-turn text (not the working bubble) |
telegram_send_photo | Image under the session repo |
telegram_send_file | Document under the session repo |
schedule_create | Create a delayed / recurring job |
schedule_list | List jobs for this session (or whole repo) |
schedule_cancel | Disable a job |
schedule_run_now | Mark due so the host fires on the next tick |
Working bubble vs permanent messages
- On turn start the worker posts
⏳ Working…in the topic. updateis the default progress tool. Call it after major steps, long waits, or plan changes so the operator sees progress without waiting for the final reply. Prefer short 1–3 sentence pings; do not dump the final answer intoupdate, and do not call on every tiny tool step.- When the turn needs the operator (permission / question), the bubble becomes
❓ Waiting for…. - When the turn ends, the worker deletes the bubble, then delivers the final assistant reply.
- Use
telegram_sendfor content that should stay in the chat history (links, intermediate results).
Agent-facing habit text lives in the bundled telegram skill (skills/telegram/SKILL.md).
Outbound Telegram tools never see the bot token. They POST to the worker Unix API — Worker API.
Agent guidance for these tools is in bundled skills telegram and schedules (Skills).
Disable host MCP entirely:
[features]
mcp = false
(Env override: ACPBOT_MCP=0.)
Remote OAuth MCP → per-slot stdio proxy
Agents (especially Grok) mishandle remote OAuth MCP. acpbot always proxies remotes via the official MCP TypeScript SDK (@modelcontextprotocol/client + @modelcontextprotocol/server):
Agent (one process per topic/slot)
└── stdio ──► acpbot mcp-proxy (McpServer + StdioServerTransport)
└── Client + StreamableHTTPClientTransport
└── HTTP + Bearer ──► remote gateway
- Per slot: each topic session (
repo/name) gets its own agent + its ownmcp-proxychildren. Slots do not share proxy processes. - OAuth tokens stay on the host (
/mcp auth); the proxy’sAuthProviderre-reads the store every request and force-refreshes on 401. - The agent only sees a normal stdio server (named like the gateway id, e.g.
full). - Always attached: remotes start as
mcp-proxyat session spawn (and on/mcp add). Until OAuth, the proxy advertises an empty tool list. - After
/mcp auth: the live proxy connects, registers tools, and sendstools/list_changed— no agent restart. - Reauth / token refresh / session drop: proxy reconnects upstream — no agent restart.
Why empty tools first?
stdio MCP children are fixed when the agent process is spawned. Starting the proxy before auth means:
/mcp add(or session start with remotes in.acpbot/mcp.json) always attaches the proxy./mcp authonly stores a token; the running proxy connects and lists tools.- Reauth never needs another agent respawn.
If you add a remote while a topic is already live, acpbot force-respawns once so the new stdio child appears; later auth does not.
Per-repo MCP (.acpbot/mcp.json)
Each session’s cwd (repo root) may declare:
{
"mcpServers": [
{
"name": "local-tools",
"command": "bun",
"args": ["run", ".acpbot/tools/server.ts"],
"env": { "FOO": "bar" }
}
]
}
Path resolution
- Relative path-like tokens (
./…,.acpbot/…) resolve from the repo root ..escapes outside the repo are rejected- Absolute paths are allowed (system / shared tools)
- npm specs (
@scope/pkg), flags, and bare binaries are left unchanged - Containment is lexical (no symlink follow for the escape check)
- Injected env into MCP children:
ACPBOT_SESSION_KEY,ACPBOT_REPO_ROOT,ACPBOT_REPO_STATE_DIR(per-repo config tree; not hostACPBOT_STATE_DIR)
Missing or invalid JSON → built-in only (warn on invalid).
Remote servers (HTTP / SSE)
Register via topic commands (persisted in the repo registry, not tokens):
/mcp add linear https://mcp.example/…
/mcp status
/mcp remove linear
/mcp auth linear
OAuth: OAuth.
MCP profiles
When a repo has more servers than a workflow needs, filter by profile.
Today mcpProfile is repo-global — every session in that repo shares the filter. Per-topic selection is not wired yet (build path accepts an override for tests / future hooks).
<repo>/.acpbot/config.json:
{
"defaultAgent": "grok-build",
"mcpProfile": "automation"
}
<repo>/.acpbot/mcp.profiles.json:
{
"automation": ["schedule", "homeassistant"],
"coding": []
}
Rules:
| Case | Result |
|---|---|
| Profile set and key exists | Filter repo MCP to that name list, then merge built-in acpbot |
Empty list [] | No repo MCP; acpbot still added |
Allowlist name missing from mcp.json | Ignored |
| Missing / unknown / unreadable config | Fail-open: no filter (all servers). Warn when a profile was requested but not applied |
defaultAgent in per-repo config is read for future defaults; session create still uses global default_agent from config.toml today.
Merge order
- Repo servers (after optional profile filter)
- Built-in
acpbothost server
Related
- Schedules — schedule tools + host fire
- Worker API — Telegram outbound
- OAuth — remote auth