Skip to content
Merged
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
2 changes: 1 addition & 1 deletion cecli/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from packaging import version

__version__ = "1.3.0.dev"
__version__ = "1.3.1.dev"
safe_version = __version__

try:
Expand Down
17 changes: 17 additions & 0 deletions cecli/coders/base_coder.py
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,23 @@ async def create(
if from_coder.tui:
res.tui = from_coder.tui

# Sub-agents get a dedicated, independent MCP manager so they
# can rebuild a custom tool list (their own LocalServer tools /
# filters) and be disconnected independently from the parent.
# The parent's server configs are copied into a fresh manager
# whose connections are created on this loop; the Local server
# is left to initialize_mcp_tools() so it is recreated from the
# sub-agent's filters.
if (
from_coder.mcp_manager
and res.uuid
and res.parent_uuid
and res.parent_uuid != res.uuid
):
res.mcp_manager = await from_coder.mcp_manager.spawn_child(
io=IOProxy.unwrap(res.io)
)

if res.mcp_manager:
# When switching to a non-agent coder, disconnect the "Local" MCP server
# (which provides agent-only tools like tool calling and file editing)
Expand Down
6 changes: 2 additions & 4 deletions cecli/helpers/hashpos/hashpos.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,8 @@ class HashPos:
rf"^({UNIQUE_HASH_DELIMITER}|{HASH_DELIMITER}[{_B1024_REGEX_SET}]{{4}}{HASH_DELIMITER})$"
)

# Loose prefix for robust stripping: Matches a tilde-wrapped 4-char string containing non-ASCII
_LOOSE_PREFIX_RE = re.compile(
rf"^[{HASH_DELIMITER}]?(?=.{{0,3}}[^\x00-\x7f]).{{4}}{HASH_DELIMITER}"
)
# Loose prefix for robust stripping: Matches a emdash-wrapped 4-char string containing non-ASCII
_LOOSE_PREFIX_RE = re.compile(rf"^[{HASH_DELIMITER}]?\S{{0,4}}{HASH_DELIMITER}")

def __init__(self, source_text: str = ""):
self.lines = source_text.splitlines()
Expand Down
48 changes: 48 additions & 0 deletions cecli/mcp/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,46 @@ async def _connect(server: McpServer) -> tuple[McpServer, bool, bool]:
tool_desc = tool.get("function", {}).get("description", "").split("\n")[0]
self.io.tool_output(f" - {tool_name}: {tool_desc}")

async def spawn_child(self, io=None) -> "McpServerManager":
"""Create a new, independent manager for a sub-agent.

Rebuilds fresh McpServer instances from this manager's server configs
so the sub-agent sees the same full set of servers it can include or
exclude (via registered_servers), but with its own connections that
can be torn down together with the sub-agent instead of being shared
with the parent. The "Local" server is deliberately left out so
AgentCoder.initialize_mcp_tools() can create and connect its own
instance and rebuild the tool list from the sub-agent's own filters
(the parent may exclude tools a child opts into).
"""
server_io = io if io is not None else self.io
servers = [
_recreate_server(server, io=server_io, verbose=self.verbose)
for server in self._servers
if not isinstance(server, LocalServer)
]

child = McpServerManager(servers=servers, io=server_io, verbose=self.verbose)
child._connection_loop = asyncio.get_running_loop()

# Mirror the parent's connected set, but with independent connections so
# the child can be disconnected without affecting the parent.
connected_names = {
server.name
for server in self._servers
if server in self._connected_servers or server.is_connected
}
for server in servers:
if server.name in connected_names:
try:
await child.connect_server(server.name)
except Exception as exc:
child._log_warning(
f"Failed to connect MCP server {server.name} for sub-agent: {exc}"
)

return child


def get_local_tool_schemas():
"""Returns the JSON schemas for all local tools using the tool registry."""
Expand Down Expand Up @@ -449,3 +489,11 @@ def _mcp_tools_to_openai_tools(tools):
}
for tool in tools
]


def _recreate_server(server, io=None, verbose=False):
"""Rebuild a fresh McpServer of the same type/config as an existing one."""
import copy

config = copy.deepcopy(server.config)
return type(server)(config, io=io, verbose=verbose)
Loading