Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 36 additions & 2 deletions docs/acp.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,9 @@ assert slow.retention.transition_timeout_s == 120
than twice on the same one. Only a bootstrap resume candidate is closed when its configuration
is refused.
- **Closure is confirmed, not assumed.** The process handle stays on the room until
`__aexit__` succeeds and the agent's stdout reaches EOF, so a descendant that
inherited it keeps the room closing until it exits, with a warning naming the
`__aexit__` succeeds, its child is reaped and the agent's stdout reaches EOF.
A child left running by a successful context exit is killed and waited for;
a descendant that inherited stdout keeps the room closing until it exits, with a warning naming the
agent's pid. A failed or cancelled exit leaves the room owned, and a retry kills
and waits for the same process. `cleanup_all()` and its alias `stop()` share one
`transition_timeout_s` budget across the rooms and the shared Band MCP server,
Expand All @@ -176,6 +177,39 @@ assert slow.retention.transition_timeout_s == 120
permission or extension requests carry no prompt id, so a call arriving after its
turn is served with the room's current tools.

## Per-room MCP servers

`resolve_mcp_servers(workspace)`, a keyword-only constructor argument of
`ACPClientAdapter`, `CopilotACPAdapter` and `CursorACPAdapter`, returns native
`acp.schema` descriptors for that room's workspace.
It runs once per session setup after the agent initializes. A reused live session
keeps its servers; a restored session and its fresh-session fallback share one
deep-copied snapshot. Recovery and a deliberate wake resolve anew on the new
connection, as does a new session replacing a stale Band MCP endpoint.

The result replaces `config.mcp_servers`, including when empty, except Band routing:
a configured `band` entry and the injected Band server remain after the resolved
external servers. Callback failure never falls back to configured external servers.

- `McpServerStdio` is always accepted. `HttpMcpServer` and `SseMcpServer` require the
agent to explicitly advertise that transport in `initialize`. `AcpMcpServer` is
refused. Names must be unique and may not be `band`.
- A refused descriptor fails the turn with `ACPMcpServerResolutionError`, naming
only its server and transport, never its URL, headers, command or environment.
Resolution failures recover the room's process through its retained owner; the
next turn resolves again on a fresh connection.
- Queued turns share the room's initializer. One cancelled waiter leaves it for
another; the last waiter cancels it once without waiting for callback settlement.
The initializer's retained lease holds the process and workspace until it settles.
A callback that swallows cancellation or requests teardown cannot publish a session
or pass its late result to the agent.
- A callback requesting `on_cleanup`, `cleanup_all` or `stop` gets
`CleanupIncompleteError` rather than waiting on itself. The retained owner continues
teardown after the callback unwinds, preserving the process, workspace and Band
MCP server until closure is confirmed.
- There is no resolver timeout: a callback that never returns holds that room's
setup, while other rooms continue independently.

## Session config

`resolve_session_config` runs after each new or restored session and before its first
Expand Down
5 changes: 5 additions & 0 deletions src/band/adapters/copilot_acp.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
from band.integrations.acp.client_adapter import (
ACPClientAdapter,
ACPClientAdapterConfig,
McpServerResolver,
PermissionResolver,
)
from band.integrations.acp.session_config import SessionConfigResolver
Expand Down Expand Up @@ -80,6 +81,7 @@ def __init__(
workspace_for_room: WorkspaceResolver | None = None,
resolve_session_config: SessionConfigResolver | None = None,
resolve_permission: PermissionResolver | None = None,
resolve_mcp_servers: McpServerResolver | None = None,
**features: Unpack[FeatureKwargs],
) -> None:
"""Bridge Band rooms to ``copilot --acp``.
Expand All @@ -92,6 +94,8 @@ def __init__(
resolve_session_config: Picks session config options; exclusive
with ``config.model`` and ``config.reasoning_effort``.
resolve_permission: Chooses a permission option per tool call.
resolve_mcp_servers: Returns native MCP server descriptors once per
room session setup; see :class:`ACPClientAdapter`.
"""
config = config or CopilotACPAdapterConfig()
super().__init__(
Expand All @@ -100,6 +104,7 @@ def __init__(
workspace_for_room=workspace_for_room,
resolve_session_config=resolve_session_config,
resolve_permission=resolve_permission,
resolve_mcp_servers=resolve_mcp_servers,
**features,
)

Expand Down
5 changes: 5 additions & 0 deletions src/band/adapters/cursor_acp.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
ACPClientAdapter,
ACPClientAdapterConfig,
ACPPermissionRequest,
McpServerResolver,
)
from band.integrations.acp.client_profiles import (
CURSOR_ASK_QUESTION_METHOD,
Expand Down Expand Up @@ -207,6 +208,7 @@ def __init__(
additional_tools: list[CustomToolDef] | None = None,
workspace_for_room: WorkspaceResolver | None = None,
resolve_session_config: SessionConfigResolver | None = None,
resolve_mcp_servers: McpServerResolver | None = None,
**features: Unpack[FeatureKwargs],
) -> None:
"""Bridge Band rooms to Cursor's ``agent acp``.
Expand All @@ -217,6 +219,8 @@ def __init__(
workspace_for_room: Maps a room id to its absolute workspace;
exclusive with ``config.cwd``.
resolve_session_config: Picks session config options.
resolve_mcp_servers: Returns native MCP server descriptors once per
room session setup; see :class:`ACPClientAdapter`.
"""
config = config or CursorACPAdapterConfig()
# Each room's one accepted turn, from acceptance until it actually
Expand All @@ -231,6 +235,7 @@ def __init__(
additional_tools=additional_tools,
workspace_for_room=workspace_for_room,
resolve_session_config=resolve_session_config,
resolve_mcp_servers=resolve_mcp_servers,
resolve_permission=self._resolve_cursor_permission,
**features,
)
Expand Down
Loading
Loading