From 84bf8b78685caa5082efbc093cca80d12e1709af Mon Sep 17 00:00:00 2001 From: syedkazmi14 <144976253+syedkazmi14@users.noreply.github.com> Date: Mon, 27 Jul 2026 23:22:04 +0000 Subject: [PATCH 1/3] Document and normalize MCP tool filter names --- python/README.md | 1 + python/copilot/_mode.py | 38 +++++++++++++++++++++++++++++++++++++- python/copilot/session.py | 17 ++++++++++++++--- python/test_tool_set.py | 18 ++++++++++++++++++ 4 files changed, 70 insertions(+), 4 deletions(-) diff --git a/python/README.md b/python/README.md index 86f568bd7a..76dca29f40 100644 --- a/python/README.md +++ b/python/README.md @@ -220,6 +220,7 @@ These are passed as keyword arguments to `create_session()`: - `on_permission_request` (callable): Optional handler called before each tool execution to approve or deny it. When omitted, permission requests are emitted as events and left pending for manual resolution. Use `PermissionHandler.approve_all` to allow everything, or provide a custom function for fine-grained control. See [Permission Handling](#permission-handling) section. - `on_user_input_request` (callable): Handler for user input requests from the agent (enables ask_user tool). See [User Input Requests](#user-input-requests) section. - `hooks` (SessionHooks): Hook handlers for session lifecycle events. See [Session Hooks](#session-hooks) section. +- `available_tools` / `excluded_tools` / custom-agent `tools`: MCP tools registered from `mcp_servers` are exposed to the runtime as `-`. Use that prefixed form when filtering MCP tools so they remain visible to the model. **Session Lifecycle Methods:** diff --git a/python/copilot/_mode.py b/python/copilot/_mode.py index d8baf26633..8ec219a848 100644 --- a/python/copilot/_mode.py +++ b/python/copilot/_mode.py @@ -10,7 +10,7 @@ from __future__ import annotations import re -from collections.abc import Iterable +from collections.abc import Iterable, Mapping from typing import TYPE_CHECKING, Any, Literal if TYPE_CHECKING: @@ -126,6 +126,42 @@ def _normalize_tool_filter(value: Any) -> list[str] | None: return list(value) +def _expand_mcp_tool_filter_names( + items: list[str] | None, + mcp_servers: Mapping[str, Any] | None, +) -> list[str] | None: + """Expand bare MCP tool names to the server-prefixed form used by the runtime. + + The Copilot CLI registers MCP tools under ``-``. This + helper rewrites bare names to that form when the corresponding server config + explicitly exposes that tool in its ``tools`` allowlist. + """ + if items is None or not mcp_servers: + return items + + expanded: list[str] = [] + for entry in items: + if entry == "*" or ":" in entry: + expanded.append(entry) + continue + + matched_entries: list[str] = [] + for server_name, config in mcp_servers.items(): + if not isinstance(config, Mapping): + continue + tools = config.get("tools") + if not isinstance(tools, list): + continue + if entry in tools: + matched_entries.append(f"{server_name}-{entry}") + + if matched_entries: + expanded.extend(matched_entries) + else: + expanded.append(entry) + return expanded + + def _validate_tool_filter_list(field: str, items: list[str] | None) -> None: """Reject bare ``"*"`` entries (must use ``builtin:*``/``mcp:*``/``custom:*``).""" if items is None: diff --git a/python/copilot/session.py b/python/copilot/session.py index 50de96f191..18bf1a0950 100644 --- a/python/copilot/session.py +++ b/python/copilot/session.py @@ -1031,7 +1031,12 @@ class SessionHooks(TypedDict, total=False): class MCPStdioServerConfig(TypedDict, total=False): - """Configuration for a local/stdio MCP server.""" + """Configuration for a local/stdio MCP server. + + Tools exposed by the runtime are registered under the server key, e.g. + ``-``. Use that prefixed form in + ``available_tools``/``excluded_tools`` and custom-agent ``tools`` lists. + """ tools: list[str] # List of tools to include. [] means none. "*" means all. type: NotRequired[Literal["local", "stdio"]] # Server type @@ -1043,7 +1048,12 @@ class MCPStdioServerConfig(TypedDict, total=False): class MCPHTTPServerConfig(TypedDict, total=False): - """Configuration for a remote MCP server (HTTP or SSE).""" + """Configuration for a remote MCP server (HTTP or SSE). + + Tools exposed by the runtime are registered under the server key, e.g. + ``-``. Use that prefixed form in + ``available_tools``/``excluded_tools`` and custom-agent ``tools`` lists. + """ tools: list[str] # List of tools to include. [] means none. "*" means all. type: Literal["http", "sse"] # Server type @@ -1065,7 +1075,8 @@ class CustomAgentConfig(TypedDict, total=False): name: str # Unique name of the custom agent display_name: NotRequired[str] # Display name for UI purposes description: NotRequired[str] # Description of what the agent does - # List of tool names the agent can use + # List of tool names the agent can use. MCP tools registered from + # ``mcp_servers`` are exposed to the runtime as ``-``. tools: NotRequired[list[str] | None] prompt: str # The prompt content for the agent # MCP servers specific to agent diff --git a/python/test_tool_set.py b/python/test_tool_set.py index 6a65e0df20..31094a5a66 100644 --- a/python/test_tool_set.py +++ b/python/test_tool_set.py @@ -13,6 +13,7 @@ _enable_session_store_default, _enable_session_telemetry_default, _enable_skills_default, + _expand_mcp_tool_filter_names, _post_create_options_patch, _require_available_tools_for_empty_mode, _require_storage_for_empty_mode, @@ -135,6 +136,23 @@ def test_accepts_source_qualified_wildcard(self): def test_accepts_none(self): _validate_tool_filter_list("available_tools", None) + def test_expands_bare_mcp_tool_names_to_server_prefixed_names(self): + expanded = _expand_mcp_tool_filter_names( + ["list_items", "run_query"], + { + "my-server": {"type": "http", "tools": ["list_items"]}, + "other-server": {"type": "http", "tools": ["run_query"]}, + }, + ) + assert expanded == ["my-server-list_items", "other-server-run_query"] + + def test_leaves_unmatched_bare_names_unchanged(self): + expanded = _expand_mcp_tool_filter_names( + ["view"], + {"my-server": {"type": "http", "tools": ["list_items"]}}, + ) + assert expanded == ["view"] + class TestSystemMessageForMode: def test_copilot_cli_pass_through(self): From ecde6a019a2106a487f141b7b03623d48e3015e0 Mon Sep 17 00:00:00 2001 From: syedkazmi14 <144976253+syedkazmi14@users.noreply.github.com> Date: Mon, 27 Jul 2026 19:16:22 -0500 Subject: [PATCH 2/3] Normalize Python MCP tool filter aliases --- python/README.md | 2 +- python/copilot/_mode.py | 27 +++---- python/copilot/client.py | 108 +++++++++++++++++++++++----- python/copilot/session.py | 22 ++++-- python/test_client.py | 145 ++++++++++++++++++++++++++++++++++++++ python/test_tool_set.py | 14 +++- 6 files changed, 281 insertions(+), 37 deletions(-) diff --git a/python/README.md b/python/README.md index 76dca29f40..ab6c682159 100644 --- a/python/README.md +++ b/python/README.md @@ -220,7 +220,7 @@ These are passed as keyword arguments to `create_session()`: - `on_permission_request` (callable): Optional handler called before each tool execution to approve or deny it. When omitted, permission requests are emitted as events and left pending for manual resolution. Use `PermissionHandler.approve_all` to allow everything, or provide a custom function for fine-grained control. See [Permission Handling](#permission-handling) section. - `on_user_input_request` (callable): Handler for user input requests from the agent (enables ask_user tool). See [User Input Requests](#user-input-requests) section. - `hooks` (SessionHooks): Hook handlers for session lifecycle events. See [Session Hooks](#session-hooks) section. -- `available_tools` / `excluded_tools` / custom-agent `tools`: MCP tools registered from `mcp_servers` are exposed to the runtime as `-`. Use that prefixed form when filtering MCP tools so they remain visible to the model. +- `available_tools` / `excluded_tools` / `default_agent.excluded_tools` / custom-agent `tools`: MCP tools registered from `mcp_servers` are exposed to the runtime as `-`. The SDK automatically adds matching prefixed aliases for bare tool names when they correspond to tools exposed by `mcp_servers`, including `tools=["*"]`. Use the prefixed form when you need exact source-specific control. **Session Lifecycle Methods:** diff --git a/python/copilot/_mode.py b/python/copilot/_mode.py index 8ec219a848..7bdcbc58e2 100644 --- a/python/copilot/_mode.py +++ b/python/copilot/_mode.py @@ -130,35 +130,38 @@ def _expand_mcp_tool_filter_names( items: list[str] | None, mcp_servers: Mapping[str, Any] | None, ) -> list[str] | None: - """Expand bare MCP tool names to the server-prefixed form used by the runtime. + """Add server-prefixed MCP tool names alongside matching bare tool filters. The Copilot CLI registers MCP tools under ``-``. This - helper rewrites bare names to that form when the corresponding server config - explicitly exposes that tool in its ``tools`` allowlist. + helper preserves the caller's original bare entry and appends the matching + ``-`` aliases for MCP servers that expose the tool, + either explicitly or via ``"*"``, so existing builtin/custom filters keep + working while MCP tools remain reachable. """ if items is None or not mcp_servers: return items expanded: list[str] = [] + seen: set[str] = set() for entry in items: - if entry == "*" or ":" in entry: + if entry not in seen: expanded.append(entry) + seen.add(entry) + if entry == "*" or ":" in entry: continue - matched_entries: list[str] = [] for server_name, config in mcp_servers.items(): if not isinstance(config, Mapping): continue tools = config.get("tools") if not isinstance(tools, list): continue - if entry in tools: - matched_entries.append(f"{server_name}-{entry}") - - if matched_entries: - expanded.extend(matched_entries) - else: - expanded.append(entry) + if "*" not in tools and entry not in tools: + continue + prefixed_entry = f"{server_name}-{entry}" + if prefixed_entry not in seen: + expanded.append(prefixed_entry) + seen.add(prefixed_entry) return expanded diff --git a/python/copilot/client.py b/python/copilot/client.py index dbe808bed2..b03bdb2e8e 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -43,6 +43,7 @@ _enable_session_store_default, _enable_session_telemetry_default, _enable_skills_default, + _expand_mcp_tool_filter_names, _mcp_oauth_token_storage_default, _memory_default, _normalize_tool_filter, @@ -229,6 +230,49 @@ def _mcp_servers_to_wire( return wire +def _normalize_custom_agent_tool_filters( + custom_agents: list[CustomAgentConfig] | None, + session_mcp_servers: Mapping[str, Any] | None, +) -> list[CustomAgentConfig] | None: + """Normalize bare MCP tool filters for custom agents.""" + if custom_agents is None: + return None + + normalized_agents: list[CustomAgentConfig] = [] + for agent in custom_agents: + normalized_agent = dict(agent) + agent_mcp_servers = normalized_agent.get("mcp_servers") + merged_mcp_servers: dict[str, Any] = {} + if session_mcp_servers: + merged_mcp_servers.update(session_mcp_servers) + if isinstance(agent_mcp_servers, Mapping): + merged_mcp_servers.update(agent_mcp_servers) + if "tools" in normalized_agent: + normalized_agent["tools"] = _expand_mcp_tool_filter_names( + normalized_agent.get("tools"), + merged_mcp_servers or None, + ) + normalized_agents.append(cast(CustomAgentConfig, normalized_agent)) + return normalized_agents + + +def _normalize_default_agent_tool_filters( + default_agent: DefaultAgentConfig | dict[str, Any] | None, + mcp_servers: Mapping[str, Any] | None, +) -> DefaultAgentConfig | dict[str, Any] | None: + """Normalize bare MCP tool filters for the default agent config.""" + if default_agent is None: + return None + + normalized_agent = dict(default_agent) + if "excluded_tools" in normalized_agent: + normalized_agent["excluded_tools"] = _expand_mcp_tool_filter_names( + normalized_agent.get("excluded_tools"), + mcp_servers, + ) + return normalized_agent + + def _large_output_to_wire(config: Mapping[str, Any]) -> dict[str, Any]: """Convert a ``LargeToolOutputConfig`` mapping to wire format.""" wire: dict[str, Any] = {} @@ -1784,11 +1828,15 @@ async def create_session( these tools will be available. Applies to the full merged tool catalog including built-in tools, MCP tools, and custom tools registered via ``tools=``. Custom tool names must be explicitly - included or they will be hidden from the model. Takes precedence - over ``excluded_tools``. + included or they will be hidden from the model. MCP tools are + registered at runtime as ``-``, and the + SDK automatically adds matching prefixed aliases for bare names + that correspond to tools exposed by ``mcp_servers``. Takes + precedence over ``excluded_tools``. excluded_tools: List of tools to disable. Applies to all tools - including custom tools registered via ``tools=``. Ignored if - ``available_tools`` is set. + including custom tools registered via ``tools=``. Bare MCP tool + names are normalized the same way as ``available_tools``. + Ignored if ``available_tools`` is set. on_user_input_request: Handler for user input requests. hooks: Lifecycle hooks for the session. working_directory: Working directory for the session. @@ -1838,9 +1886,13 @@ async def create_session( `"persistent"` uses disk-based storage (shared across sessions). `"in-memory"` stores embeddings in memory (discarded on session end). Defaults to `"in-memory"` in empty mode. - custom_agents: Custom agent configurations. + custom_agents: Custom agent configurations. Bare MCP tool names in + each agent's ``tools`` list are normalized against both session + and agent-level ``mcp_servers``. default_agent: Configuration for the default agent, - including tool visibility controls. + including tool visibility controls. Bare MCP tool names in + ``default_agent.excluded_tools`` are normalized against + ``mcp_servers``. agent: Agent to use for the session. config_directory: Override for the configuration directory. enable_config_discovery: When True, automatically discovers MCP server @@ -1939,10 +1991,18 @@ async def create_session( # Empty-mode validation and normalization mode = self._options.mode _require_available_tools_for_empty_mode(mode, _normalize_tool_filter(available_tools)) - available_tools = _normalize_tool_filter(available_tools) - excluded_tools = _normalize_tool_filter(excluded_tools) + available_tools = _expand_mcp_tool_filter_names( + _normalize_tool_filter(available_tools), + mcp_servers, + ) + excluded_tools = _expand_mcp_tool_filter_names( + _normalize_tool_filter(excluded_tools), + mcp_servers, + ) _validate_tool_filter_list("available_tools", available_tools) _validate_tool_filter_list("excluded_tools", excluded_tools) + custom_agents = _normalize_custom_agent_tool_filters(custom_agents, mcp_servers) + default_agent = _normalize_default_agent_tool_filters(default_agent, mcp_servers) # Mode "empty" strips environment_context from the system message. system_message = _system_message_for_mode(mode, system_message) # Mode "empty" defaults selected session config flags to restrictive values; @@ -2448,11 +2508,15 @@ async def resume_session( these tools will be available. Applies to the full merged tool catalog including built-in tools, MCP tools, and custom tools registered via ``tools=``. Custom tool names must be explicitly - included or they will be hidden from the model. Takes precedence - over ``excluded_tools``. + included or they will be hidden from the model. MCP tools are + registered at runtime as ``-``, and the + SDK automatically adds matching prefixed aliases for bare names + that correspond to tools exposed by ``mcp_servers``. Takes + precedence over ``excluded_tools``. excluded_tools: List of tools to disable. Applies to all tools - including custom tools registered via ``tools=``. Ignored if - ``available_tools`` is set. + including custom tools registered via ``tools=``. Bare MCP tool + names are normalized the same way as ``available_tools``. + Ignored if ``available_tools`` is set. on_user_input_request: Handler for user input requests. hooks: Lifecycle hooks for the session. working_directory: Working directory for the session. @@ -2502,9 +2566,13 @@ async def resume_session( `"persistent"` uses disk-based storage (shared across sessions). `"in-memory"` stores embeddings in memory (discarded on session end). Defaults to `"in-memory"` in empty mode. - custom_agents: Custom agent configurations. + custom_agents: Custom agent configurations. Bare MCP tool names in + each agent's ``tools`` list are normalized against both session + and agent-level ``mcp_servers``. default_agent: Configuration for the default agent, - including tool visibility controls. + including tool visibility controls. Bare MCP tool names in + ``default_agent.excluded_tools`` are normalized against + ``mcp_servers``. agent: Agent to use for the session. config_directory: Override for the configuration directory. enable_config_discovery: When True, automatically discovers MCP server @@ -2606,10 +2674,18 @@ async def resume_session( # Empty-mode validation and normalization mode = self._options.mode _require_available_tools_for_empty_mode(mode, _normalize_tool_filter(available_tools)) - available_tools = _normalize_tool_filter(available_tools) - excluded_tools = _normalize_tool_filter(excluded_tools) + available_tools = _expand_mcp_tool_filter_names( + _normalize_tool_filter(available_tools), + mcp_servers, + ) + excluded_tools = _expand_mcp_tool_filter_names( + _normalize_tool_filter(excluded_tools), + mcp_servers, + ) _validate_tool_filter_list("available_tools", available_tools) _validate_tool_filter_list("excluded_tools", excluded_tools) + custom_agents = _normalize_custom_agent_tool_filters(custom_agents, mcp_servers) + default_agent = _normalize_default_agent_tool_filters(default_agent, mcp_servers) system_message = _system_message_for_mode(mode, system_message) enable_session_telemetry = _enable_session_telemetry_default(mode, enable_session_telemetry) skip_embedding_retrieval = _skip_embedding_retrieval_default(mode, skip_embedding_retrieval) diff --git a/python/copilot/session.py b/python/copilot/session.py index 18bf1a0950..cdd7597c90 100644 --- a/python/copilot/session.py +++ b/python/copilot/session.py @@ -1034,8 +1034,10 @@ class MCPStdioServerConfig(TypedDict, total=False): """Configuration for a local/stdio MCP server. Tools exposed by the runtime are registered under the server key, e.g. - ``-``. Use that prefixed form in - ``available_tools``/``excluded_tools`` and custom-agent ``tools`` lists. + ``-``. The SDK automatically adds matching prefixed + aliases for bare names used in ``available_tools``/``excluded_tools``, + ``default_agent.excluded_tools``, and custom-agent ``tools`` lists when the + server exposes those tools. """ tools: list[str] # List of tools to include. [] means none. "*" means all. @@ -1051,8 +1053,10 @@ class MCPHTTPServerConfig(TypedDict, total=False): """Configuration for a remote MCP server (HTTP or SSE). Tools exposed by the runtime are registered under the server key, e.g. - ``-``. Use that prefixed form in - ``available_tools``/``excluded_tools`` and custom-agent ``tools`` lists. + ``-``. The SDK automatically adds matching prefixed + aliases for bare names used in ``available_tools``/``excluded_tools``, + ``default_agent.excluded_tools``, and custom-agent ``tools`` lists when the + server exposes those tools. """ tools: list[str] # List of tools to include. [] means none. "*" means all. @@ -1076,7 +1080,8 @@ class CustomAgentConfig(TypedDict, total=False): display_name: NotRequired[str] # Display name for UI purposes description: NotRequired[str] # Description of what the agent does # List of tool names the agent can use. MCP tools registered from - # ``mcp_servers`` are exposed to the runtime as ``-``. + # ``mcp_servers`` are exposed to the runtime as ``-``, + # and the SDK adds matching prefixed aliases for bare names automatically. tools: NotRequired[list[str] | None] prompt: str # The prompt content for the agent # MCP servers specific to agent @@ -1095,8 +1100,11 @@ class DefaultAgentConfig(TypedDict, total=False): when no custom agent is selected. """ - # List of tool names to exclude from the default agent. - # These tools remain available to custom sub-agents that reference them. + # List of tool names to exclude from the default agent. MCP tools registered + # from ``mcp_servers`` are exposed to the runtime as + # ``-``, and the SDK adds matching prefixed aliases + # for bare names automatically. These tools remain available to custom + # sub-agents that reference them. excluded_tools: list[str] diff --git a/python/test_client.py b/python/test_client.py index 5e1b8be634..e7eb73254f 100644 --- a/python/test_client.py +++ b/python/test_client.py @@ -263,6 +263,151 @@ async def mock_request(method, params, **kwargs): finally: await client.force_stop() + @pytest.mark.asyncio + async def test_create_session_normalizes_mcp_tool_filter_names_before_sending(self): + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) + captured = {} + + async def mock_request(method, params, **kwargs): + captured[method] = params + if method == "session.create": + result = {"sessionId": params["sessionId"], "workspacePath": None} + callback = kwargs.get("on_response_inline") + if callback is not None: + callback(result) + return result + return {} + + client._client = Mock() + client._client.request = AsyncMock(side_effect=mock_request) + await client.create_session( + on_permission_request=PermissionHandler.approve_all, + available_tools=["list_items", "run_query"], + excluded_tools=["run_query"], + mcp_servers={ + "session-db": { + "type": "http", + "url": "https://example.com/session-db", + "tools": ["list_items"], + }, + "session-wild": { + "type": "http", + "url": "https://example.com/session-wild", + "tools": ["*"], + }, + }, + custom_agents=[ + { + "name": "db-agent", + "prompt": "Use the database tools.", + "tools": ["list_items", "run_query"], + "mcp_servers": { + "agent-db": { + "type": "http", + "url": "https://example.com/agent-db", + "tools": ["run_query"], + } + }, + } + ], + default_agent={"excluded_tools": ["run_query"]}, + ) + + assert captured["session.create"]["availableTools"] == [ + "list_items", + "session-db-list_items", + "session-wild-list_items", + "run_query", + "session-wild-run_query", + ] + assert captured["session.create"]["excludedTools"] == [ + "run_query", + "session-wild-run_query", + ] + assert captured["session.create"]["customAgents"][0]["tools"] == [ + "list_items", + "session-db-list_items", + "session-wild-list_items", + "run_query", + "session-wild-run_query", + "agent-db-run_query", + ] + assert captured["session.create"]["defaultAgent"]["excludedTools"] == [ + "run_query", + "session-wild-run_query", + ] + + @pytest.mark.asyncio + async def test_resume_session_normalizes_mcp_tool_filter_names_before_sending(self): + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) + captured = {} + + async def mock_request(method, params, **kwargs): + captured[method] = params + if method == "session.resume": + return {"sessionId": params["sessionId"], "workspacePath": None} + return {} + + client._client = Mock() + client._client.request = AsyncMock(side_effect=mock_request) + await client.resume_session( + "session-with-mcp-filters", + on_permission_request=PermissionHandler.approve_all, + available_tools=["list_items", "run_query"], + excluded_tools=["run_query"], + mcp_servers={ + "session-db": { + "type": "http", + "url": "https://example.com/session-db", + "tools": ["list_items"], + }, + "session-wild": { + "type": "http", + "url": "https://example.com/session-wild", + "tools": ["*"], + }, + }, + custom_agents=[ + { + "name": "db-agent", + "prompt": "Use the database tools.", + "tools": ["list_items", "run_query"], + "mcp_servers": { + "agent-db": { + "type": "http", + "url": "https://example.com/agent-db", + "tools": ["run_query"], + } + }, + } + ], + default_agent={"excluded_tools": ["run_query"]}, + ) + + assert captured["session.resume"]["availableTools"] == [ + "list_items", + "session-db-list_items", + "session-wild-list_items", + "run_query", + "session-wild-run_query", + ] + assert captured["session.resume"]["excludedTools"] == [ + "run_query", + "session-wild-run_query", + ] + assert captured["session.resume"]["customAgents"][0]["tools"] == [ + "list_items", + "session-db-list_items", + "session-wild-list_items", + "run_query", + "session-wild-run_query", + "agent-db-run_query", + ] + assert captured["session.resume"]["defaultAgent"]["excludedTools"] == [ + "run_query", + "session-wild-run_query", + ] + @pytest.mark.asyncio async def test_mcp_auth_handler_registers_interest_after_cloud_create_only_with_handler(self): client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) diff --git a/python/test_tool_set.py b/python/test_tool_set.py index 31094a5a66..d45ebd7a6c 100644 --- a/python/test_tool_set.py +++ b/python/test_tool_set.py @@ -144,7 +144,19 @@ def test_expands_bare_mcp_tool_names_to_server_prefixed_names(self): "other-server": {"type": "http", "tools": ["run_query"]}, }, ) - assert expanded == ["my-server-list_items", "other-server-run_query"] + assert expanded == [ + "list_items", + "my-server-list_items", + "run_query", + "other-server-run_query", + ] + + def test_expands_bare_mcp_tool_names_for_wildcard_servers(self): + expanded = _expand_mcp_tool_filter_names( + ["run_query"], + {"my-server": {"type": "http", "tools": ["*"]}}, + ) + assert expanded == ["run_query", "my-server-run_query"] def test_leaves_unmatched_bare_names_unchanged(self): expanded = _expand_mcp_tool_filter_names( From d8d4eff273eae79c5bf0feef59a58cecda1f80c1 Mon Sep 17 00:00:00 2001 From: syedkazmi14 <144976253+syedkazmi14@users.noreply.github.com> Date: Tue, 28 Jul 2026 17:23:43 -0500 Subject: [PATCH 3/3] Document MCP tool filter naming across SDKs --- dotnet/README.md | 6 + dotnet/src/Types.cs | 29 +++- go/README.md | 6 + go/types.go | 50 ++++-- java/README.md | 7 + .../github/copilot/rpc/CustomAgentConfig.java | 4 +- .../copilot/rpc/DefaultAgentConfig.java | 5 +- .../copilot/rpc/ResumeSessionConfig.java | 15 +- .../com/github/copilot/rpc/SessionConfig.java | 13 +- nodejs/README.md | 6 + nodejs/src/types.ts | 16 +- python/README.md | 2 +- python/copilot/_mode.py | 41 +---- python/copilot/client.py | 128 ++++------------ python/copilot/session.py | 29 ++-- python/test_client.py | 145 ------------------ python/test_tool_set.py | 30 ---- rust/README.md | 6 + rust/src/types.rs | 28 +++- 19 files changed, 204 insertions(+), 362 deletions(-) diff --git a/dotnet/README.md b/dotnet/README.md index 796ef22549..83398aff2c 100644 --- a/dotnet/README.md +++ b/dotnet/README.md @@ -64,6 +64,12 @@ await session.SendAsync(new MessageOptions { Prompt = "What is 2+2?" }); await done.Task; ``` +When targeting MCP tools configured through `McpServers`, remember the runtime +tool name is `-`. For `AvailableTools` and +`ExcludedTools`, prefer the source-qualified form +`mcp:-`. For `CustomAgents[].Tools` and +`DefaultAgent.ExcludedTools`, use `-` directly. + ## API Reference ### CopilotClient diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index 63e39a2c56..b4e4af5463 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -2604,6 +2604,8 @@ public sealed class CustomAgentConfig /// /// List of tool names the agent can use. Null for all tools. + /// For MCP tools from , use the + /// runtime tool name <server-key>-<tool-name>. /// [JsonPropertyName("tools")] public IList? Tools { get; set; } @@ -2655,7 +2657,9 @@ public sealed class DefaultAgentConfig /// /// List of tool names to exclude from the default agent. /// These tools remain available to custom sub-agents that reference them - /// in their list. + /// in their list. For MCP tools from + /// , use + /// <server-key>-<tool-name>. /// public IList? ExcludedTools { get; set; } } @@ -3008,10 +3012,19 @@ protected SessionConfigBase(SessionConfigBase? other) /// System message configuration for the session. public SystemMessageConfig? SystemMessage { get; set; } - /// List of tool names to allow; only these tools will be available when specified. + /// + /// List of tool names to allow; only these tools will be available when specified. + /// For MCP tools from , the runtime tool name is + /// <server-key>-<tool-name>; prefer the source-qualified + /// filter form mcp:<server-key>-<tool-name>. + /// public IList? AvailableTools { get; set; } - /// List of tool names to exclude from the session. + /// + /// List of tool names to exclude from the session. + /// Use the same MCP naming convention as when + /// targeting tools from . + /// public IList? ExcludedTools { get; set; } /// @@ -3174,13 +3187,19 @@ protected SessionConfigBase(SessionConfigBase? other) /// public McpOAuthTokenStorageMode? McpOAuthTokenStorage { get; set; } - /// Custom agent configurations for the session. + /// + /// Custom agent configurations for the session. + /// When an agent list targets an MCP + /// tool from , use + /// <server-key>-<tool-name>. + /// public IList? CustomAgents { get; set; } /// /// Configuration for the default agent (the built-in agent that handles turns when no custom agent is selected). /// Use to hide specific tools from the default agent - /// while keeping them available to custom sub-agents. + /// while keeping them available to custom sub-agents. For MCP tools from + /// , use <server-key>-<tool-name>. /// public DefaultAgentConfig? DefaultAgent { get; set; } diff --git a/go/README.md b/go/README.md index 5787e5a53e..f3f98572cc 100644 --- a/go/README.md +++ b/go/README.md @@ -89,6 +89,12 @@ func main() { } ``` +When targeting MCP tools configured through `MCPServers`, remember the runtime +tool name is `-`. For `AvailableTools` and +`ExcludedTools`, prefer the source-qualified form +`mcp:-`. For `CustomAgents[].Tools` and +`DefaultAgent.ExcludedTools`, use `-` directly. + ## Distributing your application with an embedded GitHub Copilot CLI The SDK supports bundling, using Go's `embed` package, the Copilot CLI binary within your application's distribution. diff --git a/go/types.go b/go/types.go index 6111b7be41..07bf544143 100644 --- a/go/types.go +++ b/go/types.go @@ -888,7 +888,9 @@ type CustomAgentConfig struct { // Description of what the agent does Description string `json:"description,omitempty"` // Tools is the list of tool names the agent can use. Nil omits the field - // (all tools); an empty non-nil slice sends "tools": [] (no tools). + // (all tools); an empty non-nil slice sends "tools": [] (no tools). For + // MCP tools from MCPServers, use the runtime tool name + // -. Tools []string `json:"tools,omitzero"` // Prompt is the prompt content for the agent Prompt string `json:"prompt"` @@ -909,7 +911,9 @@ type CustomAgentConfig struct { // them available to custom sub-agents. type DefaultAgentConfig struct { // ExcludedTools is a list of tool names to exclude from the default agent. - // These tools remain available to custom sub-agents that reference them in their Tools list. + // These tools remain available to custom sub-agents that reference them in + // their Tools list. For MCP tools from MCPServers, use + // -. ExcludedTools []string `json:"excludedTools,omitempty"` } @@ -1034,11 +1038,15 @@ type SessionConfig struct { Tools []Tool // SystemMessage configures system message customization SystemMessage *SystemMessageConfig - // AvailableTools is a list of tool names to allow. When specified, only these tools will be available. - // Takes precedence over ExcludedTools. + // AvailableTools is a list of tool names to allow. When specified, only + // these tools will be available. Takes precedence over ExcludedTools. For + // MCP tools from MCPServers, the runtime tool name is + // -; prefer the source-qualified filter form + // mcp:-. AvailableTools []string - // ExcludedTools is a list of tool names to disable. All other tools remain available. - // Ignored if AvailableTools is specified. + // ExcludedTools is a list of tool names to disable. All other tools remain + // available. Ignored if AvailableTools is specified. Use the same MCP naming + // convention as AvailableTools when targeting tools from MCPServers. ExcludedTools []string // ExcludedBuiltInAgents is a list of built-in agent names to exclude from // the session. Excluded built-in agents are hidden from discovery and cannot @@ -1126,10 +1134,14 @@ type SessionConfig struct { // MCPOAuthTokenStorage controls how MCP OAuth tokens are stored for this session. // When empty, the runtime default ("in-memory") is used. MCPOAuthTokenStorage string - // CustomAgents configures custom agents for the session + // CustomAgents configures custom agents for the session. When an agent Tools + // list targets an MCP tool from MCPServers, use + // -. CustomAgents []CustomAgentConfig - // DefaultAgent configures the default agent (the built-in agent that handles turns when no custom agent is selected). - // Use ExcludedTools to hide tools from the default agent while keeping them available to sub-agents. + // DefaultAgent configures the default agent (the built-in agent that handles + // turns when no custom agent is selected). Use ExcludedTools to hide tools + // from the default agent while keeping them available to sub-agents. For MCP + // tools from MCPServers, use - in ExcludedTools. DefaultAgent *DefaultAgentConfig // Agent is the name of the custom agent to activate when the session starts. // Must match the Name of one of the agents in CustomAgents. @@ -1441,11 +1453,15 @@ type ResumeSessionConfig struct { Tools []Tool // SystemMessage configures system message customization SystemMessage *SystemMessageConfig - // AvailableTools is a list of tool names to allow. When specified, only these tools will be available. - // Takes precedence over ExcludedTools. + // AvailableTools is a list of tool names to allow. When specified, only + // these tools will be available. Takes precedence over ExcludedTools. For + // MCP tools from MCPServers, the runtime tool name is + // -; prefer the source-qualified filter form + // mcp:-. AvailableTools []string - // ExcludedTools is a list of tool names to disable. All other tools remain available. - // Ignored if AvailableTools is specified. + // ExcludedTools is a list of tool names to disable. All other tools remain + // available. Ignored if AvailableTools is specified. Use the same MCP naming + // convention as AvailableTools when targeting tools from MCPServers. ExcludedTools []string // ExcludedBuiltInAgents is a list of built-in agent names to exclude from // the session. Excluded built-in agents are hidden from discovery and cannot @@ -1577,9 +1593,13 @@ type ResumeSessionConfig struct { // MCPOAuthTokenStorage controls how MCP OAuth tokens are stored for this session. // When empty, the runtime default ("in-memory") is used. MCPOAuthTokenStorage string - // CustomAgents configures custom agents for the session + // CustomAgents configures custom agents for the session. When an agent Tools + // list targets an MCP tool from MCPServers, use + // -. CustomAgents []CustomAgentConfig - // DefaultAgent configures the default agent (the built-in agent that handles turns when no custom agent is selected). + // DefaultAgent configures the default agent (the built-in agent that handles + // turns when no custom agent is selected). For MCP tools from MCPServers, + // use - in ExcludedTools. DefaultAgent *DefaultAgentConfig // Agent is the name of the custom agent to activate when the session starts. // Must match the Name of one of the agents in CustomAgents. diff --git a/java/README.md b/java/README.md index 42e438e527..13e56123b8 100644 --- a/java/README.md +++ b/java/README.md @@ -120,6 +120,13 @@ public class CopilotSDK { } ``` +When targeting MCP tools configured through `setMcpServers(...)`, remember the +runtime tool name is `-`. For `setAvailableTools(...)` +and `setExcludedTools(...)`, prefer the source-qualified filter form +`mcp:-`. For `CustomAgentConfig.setTools(...)` and +`DefaultAgentConfig.setExcludedTools(...)`, use `-` +directly. + ## Try it with JBang You can run the SDK without setting up a full Java project, by using [JBang](https://www.jbang.dev/). diff --git a/java/src/main/java/com/github/copilot/rpc/CustomAgentConfig.java b/java/src/main/java/com/github/copilot/rpc/CustomAgentConfig.java index 5136f77786..991ef31625 100644 --- a/java/src/main/java/com/github/copilot/rpc/CustomAgentConfig.java +++ b/java/src/main/java/com/github/copilot/rpc/CustomAgentConfig.java @@ -144,7 +144,9 @@ public List getTools() { * Sets the tools available to this agent. *

* These can reference both built-in tools and custom tools registered in the - * session. + * session. For MCP tools from {@link SessionConfig#setMcpServers(Map)} or + * {@link ResumeSessionConfig#setMcpServers(Map)}, use the runtime tool name + * {@code -}. * * @param tools * the list of tool names diff --git a/java/src/main/java/com/github/copilot/rpc/DefaultAgentConfig.java b/java/src/main/java/com/github/copilot/rpc/DefaultAgentConfig.java index 4be5dbbb9b..1e863b6d31 100644 --- a/java/src/main/java/com/github/copilot/rpc/DefaultAgentConfig.java +++ b/java/src/main/java/com/github/copilot/rpc/DefaultAgentConfig.java @@ -46,7 +46,10 @@ public List getExcludedTools() { * Sets the list of tool names to exclude from the default agent. *

* These tools remain available to custom sub-agents that reference them in - * their {@link CustomAgentConfig#setTools(List)} list. + * their {@link CustomAgentConfig#setTools(List)} list. For MCP tools from + * {@link SessionConfig#setMcpServers(Map)} or + * {@link ResumeSessionConfig#setMcpServers(Map)}, use the runtime tool name + * {@code -}. * * @param excludedTools * the list of tool names to exclude from the default agent diff --git a/java/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java b/java/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java index 0efe1c5c6a..ebdd5243a9 100644 --- a/java/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java +++ b/java/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java @@ -209,7 +209,10 @@ public List getAvailableTools() { * Sets the list of tool names that are allowed in this session. *

* When specified, only tools in this list will be available to the assistant. - * Takes precedence over excluded tools. + * Takes precedence over excluded tools. MCP tools from + * {@link #setMcpServers(Map)} use the runtime name + * {@code -}; prefer the source-qualified filter form + * {@code mcp:-}. * * @param availableTools * the list of allowed tool names @@ -233,7 +236,9 @@ public List getExcludedTools() { * Sets the list of tool names to exclude from this session. *

* Tools in this list will not be available to the assistant. Ignored if - * available tools is specified. + * available tools is specified. Use the same MCP naming convention as + * {@link #setAvailableTools(List)} when targeting tools from + * {@link #setMcpServers(Map)}. * * @param excludedTools * the list of tool names to exclude @@ -1304,6 +1309,10 @@ public List getCustomAgents() { /** * Sets custom agent configurations. + *

+ * When an agent {@link CustomAgentConfig#setTools(List)} list targets an + * MCP tool from {@link #setMcpServers(Map)}, use the runtime tool name + * {@code -}. * * @param customAgents * the list of custom agent configurations @@ -1329,6 +1338,8 @@ public DefaultAgentConfig getDefaultAgent() { *

* Use {@link DefaultAgentConfig#setExcludedTools(List)} to hide specific tools * from the default agent while keeping them available to custom sub-agents. + * MCP tools from {@link #setMcpServers(Map)} use the runtime tool name + * {@code -}. * * @param defaultAgent * the default agent configuration diff --git a/java/src/main/java/com/github/copilot/rpc/SessionConfig.java b/java/src/main/java/com/github/copilot/rpc/SessionConfig.java index 3533ceefac..d91615a443 100644 --- a/java/src/main/java/com/github/copilot/rpc/SessionConfig.java +++ b/java/src/main/java/com/github/copilot/rpc/SessionConfig.java @@ -309,6 +309,9 @@ public List getAvailableTools() { * Sets the list of tool names that are allowed in this session. *

* When specified, only tools in this list will be available to the assistant. + * MCP tools from {@link #setMcpServers(Map)} use the runtime name + * {@code -}; prefer the source-qualified filter form + * {@code mcp:-}. * * @param availableTools * the list of allowed tool names @@ -331,7 +334,9 @@ public List getExcludedTools() { /** * Sets the list of tool names to exclude from this session. *

- * Tools in this list will not be available to the assistant. + * Tools in this list will not be available to the assistant. Use the same + * MCP naming convention as {@link #setAvailableTools(List)} when targeting + * tools from {@link #setMcpServers(Map)}. * * @param excludedTools * the list of tool names to exclude @@ -955,7 +960,9 @@ public List getCustomAgents() { * Sets custom agent configurations. *

* Custom agents allow extending the assistant with specialized behaviors and - * capabilities. + * capabilities. When an agent {@link CustomAgentConfig#setTools(List)} list + * targets an MCP tool from {@link #setMcpServers(Map)}, use the runtime + * tool name {@code -}. * * @param customAgents * the list of custom agent configurations @@ -981,6 +988,8 @@ public DefaultAgentConfig getDefaultAgent() { *

* Use {@link DefaultAgentConfig#setExcludedTools(List)} to hide specific tools * from the default agent while keeping them available to custom sub-agents. + * MCP tools from {@link #setMcpServers(Map)} use the runtime tool name + * {@code -}. * * @param defaultAgent * the default agent configuration diff --git a/nodejs/README.md b/nodejs/README.md index e9d83c6df9..7c050fdf8d 100644 --- a/nodejs/README.md +++ b/nodejs/README.md @@ -71,6 +71,12 @@ await using session = await client.createSession({ // session is automatically disconnected when leaving scope ``` +When targeting MCP tools configured through `mcpServers`, remember the runtime +tool name is `-`. For `availableTools` and +`excludedTools`, prefer `new ToolSet().addMcp("-")` or +the raw `mcp:-` form. For `customAgents[].tools` and +`defaultAgent.excludedTools`, use `-` directly. + ## API Reference ### CopilotClient diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index 65fc00cfe1..ad2714bc93 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -1512,7 +1512,8 @@ export interface CustomAgentConfig { description?: string; /** * List of tool names the agent can use. - * Use null or undefined for all tools. + * Use null or undefined for all tools. For MCP tools from `mcpServers`, + * use the runtime tool name `-`. */ tools?: string[] | null; /** @@ -1877,7 +1878,10 @@ export interface SessionConfigBase { * Supports source-qualified filter patterns (`builtin:*`, `builtin:`, * `mcp:*`, `mcp:`, `custom:*`, `custom:`) as well as the bare * name form (exact match across any source). Build this list with - * {@link ToolSet} for type safety and readable intent. + * {@link ToolSet} for type safety and readable intent. For MCP tools from + * `mcpServers`, the runtime tool name is `-`, so + * prefer `new ToolSet().addMcp("-")` or the raw + * `mcp:-` form. * * Composes with {@link excludedTools}: a tool is enabled when it matches * `availableTools` (or `availableTools` is unset) AND it does not match @@ -1887,7 +1891,8 @@ export interface SessionConfigBase { /** * List of tool names to disable. Supports the same pattern syntax as - * {@link availableTools}. + * {@link availableTools}. Use the same MCP naming convention when targeting + * tools from `mcpServers`. * * Always takes precedence over {@link availableTools}: a tool listed here * is disabled even if it also matches `availableTools`. @@ -2117,6 +2122,8 @@ export interface SessionConfigBase { /** * Custom agent configurations for the session. + * When an agent `tools` list targets an MCP tool from `mcpServers`, use + * `-`. */ customAgents?: CustomAgentConfig[]; @@ -2124,7 +2131,8 @@ export interface SessionConfigBase { * Configuration for the default agent (the built-in agent that handles * turns when no custom agent is selected). * Use `excludedTools` to hide specific tools from the default agent while keeping - * them available to custom sub-agents. + * them available to custom sub-agents. For MCP tools from `mcpServers`, + * use `-`. */ defaultAgent?: DefaultAgentConfig; diff --git a/python/README.md b/python/README.md index ab6c682159..669b8798d5 100644 --- a/python/README.md +++ b/python/README.md @@ -220,7 +220,7 @@ These are passed as keyword arguments to `create_session()`: - `on_permission_request` (callable): Optional handler called before each tool execution to approve or deny it. When omitted, permission requests are emitted as events and left pending for manual resolution. Use `PermissionHandler.approve_all` to allow everything, or provide a custom function for fine-grained control. See [Permission Handling](#permission-handling) section. - `on_user_input_request` (callable): Handler for user input requests from the agent (enables ask_user tool). See [User Input Requests](#user-input-requests) section. - `hooks` (SessionHooks): Hook handlers for session lifecycle events. See [Session Hooks](#session-hooks) section. -- `available_tools` / `excluded_tools` / `default_agent.excluded_tools` / custom-agent `tools`: MCP tools registered from `mcp_servers` are exposed to the runtime as `-`. The SDK automatically adds matching prefixed aliases for bare tool names when they correspond to tools exposed by `mcp_servers`, including `tools=["*"]`. Use the prefixed form when you need exact source-specific control. +- `available_tools` / `excluded_tools` / `default_agent.excluded_tools` / custom-agent `tools`: MCP tools registered from `mcp_servers` are exposed to the runtime as `-`. For `available_tools` and `excluded_tools`, prefer `ToolSet().add_mcp("-")` or the raw `mcp:-` form. For custom-agent `tools` and `default_agent.excluded_tools`, use `-` directly. **Session Lifecycle Methods:** diff --git a/python/copilot/_mode.py b/python/copilot/_mode.py index 7bdcbc58e2..d8baf26633 100644 --- a/python/copilot/_mode.py +++ b/python/copilot/_mode.py @@ -10,7 +10,7 @@ from __future__ import annotations import re -from collections.abc import Iterable, Mapping +from collections.abc import Iterable from typing import TYPE_CHECKING, Any, Literal if TYPE_CHECKING: @@ -126,45 +126,6 @@ def _normalize_tool_filter(value: Any) -> list[str] | None: return list(value) -def _expand_mcp_tool_filter_names( - items: list[str] | None, - mcp_servers: Mapping[str, Any] | None, -) -> list[str] | None: - """Add server-prefixed MCP tool names alongside matching bare tool filters. - - The Copilot CLI registers MCP tools under ``-``. This - helper preserves the caller's original bare entry and appends the matching - ``-`` aliases for MCP servers that expose the tool, - either explicitly or via ``"*"``, so existing builtin/custom filters keep - working while MCP tools remain reachable. - """ - if items is None or not mcp_servers: - return items - - expanded: list[str] = [] - seen: set[str] = set() - for entry in items: - if entry not in seen: - expanded.append(entry) - seen.add(entry) - if entry == "*" or ":" in entry: - continue - - for server_name, config in mcp_servers.items(): - if not isinstance(config, Mapping): - continue - tools = config.get("tools") - if not isinstance(tools, list): - continue - if "*" not in tools and entry not in tools: - continue - prefixed_entry = f"{server_name}-{entry}" - if prefixed_entry not in seen: - expanded.append(prefixed_entry) - seen.add(prefixed_entry) - return expanded - - def _validate_tool_filter_list(field: str, items: list[str] | None) -> None: """Reject bare ``"*"`` entries (must use ``builtin:*``/``mcp:*``/``custom:*``).""" if items is None: diff --git a/python/copilot/client.py b/python/copilot/client.py index b03bdb2e8e..86bf80f5cc 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -43,7 +43,6 @@ _enable_session_store_default, _enable_session_telemetry_default, _enable_skills_default, - _expand_mcp_tool_filter_names, _mcp_oauth_token_storage_default, _memory_default, _normalize_tool_filter, @@ -228,51 +227,6 @@ def _mcp_servers_to_wire( del config["working_directory"] wire[name] = config return wire - - -def _normalize_custom_agent_tool_filters( - custom_agents: list[CustomAgentConfig] | None, - session_mcp_servers: Mapping[str, Any] | None, -) -> list[CustomAgentConfig] | None: - """Normalize bare MCP tool filters for custom agents.""" - if custom_agents is None: - return None - - normalized_agents: list[CustomAgentConfig] = [] - for agent in custom_agents: - normalized_agent = dict(agent) - agent_mcp_servers = normalized_agent.get("mcp_servers") - merged_mcp_servers: dict[str, Any] = {} - if session_mcp_servers: - merged_mcp_servers.update(session_mcp_servers) - if isinstance(agent_mcp_servers, Mapping): - merged_mcp_servers.update(agent_mcp_servers) - if "tools" in normalized_agent: - normalized_agent["tools"] = _expand_mcp_tool_filter_names( - normalized_agent.get("tools"), - merged_mcp_servers or None, - ) - normalized_agents.append(cast(CustomAgentConfig, normalized_agent)) - return normalized_agents - - -def _normalize_default_agent_tool_filters( - default_agent: DefaultAgentConfig | dict[str, Any] | None, - mcp_servers: Mapping[str, Any] | None, -) -> DefaultAgentConfig | dict[str, Any] | None: - """Normalize bare MCP tool filters for the default agent config.""" - if default_agent is None: - return None - - normalized_agent = dict(default_agent) - if "excluded_tools" in normalized_agent: - normalized_agent["excluded_tools"] = _expand_mcp_tool_filter_names( - normalized_agent.get("excluded_tools"), - mcp_servers, - ) - return normalized_agent - - def _large_output_to_wire(config: Mapping[str, Any]) -> dict[str, Any]: """Convert a ``LargeToolOutputConfig`` mapping to wire format.""" wire: dict[str, Any] = {} @@ -1828,15 +1782,16 @@ async def create_session( these tools will be available. Applies to the full merged tool catalog including built-in tools, MCP tools, and custom tools registered via ``tools=``. Custom tool names must be explicitly - included or they will be hidden from the model. MCP tools are - registered at runtime as ``-``, and the - SDK automatically adds matching prefixed aliases for bare names - that correspond to tools exposed by ``mcp_servers``. Takes - precedence over ``excluded_tools``. + included or they will be hidden from the model. MCP tools from + ``mcp_servers`` are named ``-``; when + using source-qualified filters, prefer + ``ToolSet().add_mcp("-")`` or the raw + ``mcp:-`` form. Takes precedence over + ``excluded_tools``. excluded_tools: List of tools to disable. Applies to all tools - including custom tools registered via ``tools=``. Bare MCP tool - names are normalized the same way as ``available_tools``. - Ignored if ``available_tools`` is set. + including custom tools registered via ``tools=``. Use the same + MCP naming convention as ``available_tools``. Ignored if + ``available_tools`` is set. on_user_input_request: Handler for user input requests. hooks: Lifecycle hooks for the session. working_directory: Working directory for the session. @@ -1886,13 +1841,13 @@ async def create_session( `"persistent"` uses disk-based storage (shared across sessions). `"in-memory"` stores embeddings in memory (discarded on session end). Defaults to `"in-memory"` in empty mode. - custom_agents: Custom agent configurations. Bare MCP tool names in - each agent's ``tools`` list are normalized against both session - and agent-level ``mcp_servers``. + custom_agents: Custom agent configurations. When an agent's + ``tools`` list targets an MCP tool from ``mcp_servers``, use + the runtime tool name ``-``. default_agent: Configuration for the default agent, - including tool visibility controls. Bare MCP tool names in - ``default_agent.excluded_tools`` are normalized against - ``mcp_servers``. + including tool visibility controls. When + ``default_agent.excluded_tools`` targets an MCP tool from + ``mcp_servers``, use ``-``. agent: Agent to use for the session. config_directory: Override for the configuration directory. enable_config_discovery: When True, automatically discovers MCP server @@ -1991,18 +1946,10 @@ async def create_session( # Empty-mode validation and normalization mode = self._options.mode _require_available_tools_for_empty_mode(mode, _normalize_tool_filter(available_tools)) - available_tools = _expand_mcp_tool_filter_names( - _normalize_tool_filter(available_tools), - mcp_servers, - ) - excluded_tools = _expand_mcp_tool_filter_names( - _normalize_tool_filter(excluded_tools), - mcp_servers, - ) + available_tools = _normalize_tool_filter(available_tools) + excluded_tools = _normalize_tool_filter(excluded_tools) _validate_tool_filter_list("available_tools", available_tools) _validate_tool_filter_list("excluded_tools", excluded_tools) - custom_agents = _normalize_custom_agent_tool_filters(custom_agents, mcp_servers) - default_agent = _normalize_default_agent_tool_filters(default_agent, mcp_servers) # Mode "empty" strips environment_context from the system message. system_message = _system_message_for_mode(mode, system_message) # Mode "empty" defaults selected session config flags to restrictive values; @@ -2508,15 +2455,16 @@ async def resume_session( these tools will be available. Applies to the full merged tool catalog including built-in tools, MCP tools, and custom tools registered via ``tools=``. Custom tool names must be explicitly - included or they will be hidden from the model. MCP tools are - registered at runtime as ``-``, and the - SDK automatically adds matching prefixed aliases for bare names - that correspond to tools exposed by ``mcp_servers``. Takes - precedence over ``excluded_tools``. + included or they will be hidden from the model. MCP tools from + ``mcp_servers`` are named ``-``; when + using source-qualified filters, prefer + ``ToolSet().add_mcp("-")`` or the raw + ``mcp:-`` form. Takes precedence over + ``excluded_tools``. excluded_tools: List of tools to disable. Applies to all tools - including custom tools registered via ``tools=``. Bare MCP tool - names are normalized the same way as ``available_tools``. - Ignored if ``available_tools`` is set. + including custom tools registered via ``tools=``. Use the same + MCP naming convention as ``available_tools``. Ignored if + ``available_tools`` is set. on_user_input_request: Handler for user input requests. hooks: Lifecycle hooks for the session. working_directory: Working directory for the session. @@ -2566,13 +2514,13 @@ async def resume_session( `"persistent"` uses disk-based storage (shared across sessions). `"in-memory"` stores embeddings in memory (discarded on session end). Defaults to `"in-memory"` in empty mode. - custom_agents: Custom agent configurations. Bare MCP tool names in - each agent's ``tools`` list are normalized against both session - and agent-level ``mcp_servers``. + custom_agents: Custom agent configurations. When an agent's + ``tools`` list targets an MCP tool from ``mcp_servers``, use + the runtime tool name ``-``. default_agent: Configuration for the default agent, - including tool visibility controls. Bare MCP tool names in - ``default_agent.excluded_tools`` are normalized against - ``mcp_servers``. + including tool visibility controls. When + ``default_agent.excluded_tools`` targets an MCP tool from + ``mcp_servers``, use ``-``. agent: Agent to use for the session. config_directory: Override for the configuration directory. enable_config_discovery: When True, automatically discovers MCP server @@ -2674,18 +2622,10 @@ async def resume_session( # Empty-mode validation and normalization mode = self._options.mode _require_available_tools_for_empty_mode(mode, _normalize_tool_filter(available_tools)) - available_tools = _expand_mcp_tool_filter_names( - _normalize_tool_filter(available_tools), - mcp_servers, - ) - excluded_tools = _expand_mcp_tool_filter_names( - _normalize_tool_filter(excluded_tools), - mcp_servers, - ) + available_tools = _normalize_tool_filter(available_tools) + excluded_tools = _normalize_tool_filter(excluded_tools) _validate_tool_filter_list("available_tools", available_tools) _validate_tool_filter_list("excluded_tools", excluded_tools) - custom_agents = _normalize_custom_agent_tool_filters(custom_agents, mcp_servers) - default_agent = _normalize_default_agent_tool_filters(default_agent, mcp_servers) system_message = _system_message_for_mode(mode, system_message) enable_session_telemetry = _enable_session_telemetry_default(mode, enable_session_telemetry) skip_embedding_retrieval = _skip_embedding_retrieval_default(mode, skip_embedding_retrieval) diff --git a/python/copilot/session.py b/python/copilot/session.py index cdd7597c90..3bf30d7d85 100644 --- a/python/copilot/session.py +++ b/python/copilot/session.py @@ -1033,11 +1033,9 @@ class SessionHooks(TypedDict, total=False): class MCPStdioServerConfig(TypedDict, total=False): """Configuration for a local/stdio MCP server. - Tools exposed by the runtime are registered under the server key, e.g. - ``-``. The SDK automatically adds matching prefixed - aliases for bare names used in ``available_tools``/``excluded_tools``, - ``default_agent.excluded_tools``, and custom-agent ``tools`` lists when the - server exposes those tools. + Tools exposed by the runtime are named ``-`` in + runtime-facing lists such as custom-agent ``tools`` and + ``default_agent.excluded_tools``. """ tools: list[str] # List of tools to include. [] means none. "*" means all. @@ -1052,11 +1050,9 @@ class MCPStdioServerConfig(TypedDict, total=False): class MCPHTTPServerConfig(TypedDict, total=False): """Configuration for a remote MCP server (HTTP or SSE). - Tools exposed by the runtime are registered under the server key, e.g. - ``-``. The SDK automatically adds matching prefixed - aliases for bare names used in ``available_tools``/``excluded_tools``, - ``default_agent.excluded_tools``, and custom-agent ``tools`` lists when the - server exposes those tools. + Tools exposed by the runtime are named ``-`` in + runtime-facing lists such as custom-agent ``tools`` and + ``default_agent.excluded_tools``. """ tools: list[str] # List of tools to include. [] means none. "*" means all. @@ -1079,9 +1075,8 @@ class CustomAgentConfig(TypedDict, total=False): name: str # Unique name of the custom agent display_name: NotRequired[str] # Display name for UI purposes description: NotRequired[str] # Description of what the agent does - # List of tool names the agent can use. MCP tools registered from - # ``mcp_servers`` are exposed to the runtime as ``-``, - # and the SDK adds matching prefixed aliases for bare names automatically. + # List of tool names the agent can use. For MCP tools from ``mcp_servers``, + # use the runtime tool name ``-``. tools: NotRequired[list[str] | None] prompt: str # The prompt content for the agent # MCP servers specific to agent @@ -1100,11 +1095,9 @@ class DefaultAgentConfig(TypedDict, total=False): when no custom agent is selected. """ - # List of tool names to exclude from the default agent. MCP tools registered - # from ``mcp_servers`` are exposed to the runtime as - # ``-``, and the SDK adds matching prefixed aliases - # for bare names automatically. These tools remain available to custom - # sub-agents that reference them. + # List of tool names to exclude from the default agent. For MCP tools from + # ``mcp_servers``, use ``-``. These tools remain + # available to custom sub-agents that reference them. excluded_tools: list[str] diff --git a/python/test_client.py b/python/test_client.py index e7eb73254f..5e1b8be634 100644 --- a/python/test_client.py +++ b/python/test_client.py @@ -263,151 +263,6 @@ async def mock_request(method, params, **kwargs): finally: await client.force_stop() - @pytest.mark.asyncio - async def test_create_session_normalizes_mcp_tool_filter_names_before_sending(self): - client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) - captured = {} - - async def mock_request(method, params, **kwargs): - captured[method] = params - if method == "session.create": - result = {"sessionId": params["sessionId"], "workspacePath": None} - callback = kwargs.get("on_response_inline") - if callback is not None: - callback(result) - return result - return {} - - client._client = Mock() - client._client.request = AsyncMock(side_effect=mock_request) - await client.create_session( - on_permission_request=PermissionHandler.approve_all, - available_tools=["list_items", "run_query"], - excluded_tools=["run_query"], - mcp_servers={ - "session-db": { - "type": "http", - "url": "https://example.com/session-db", - "tools": ["list_items"], - }, - "session-wild": { - "type": "http", - "url": "https://example.com/session-wild", - "tools": ["*"], - }, - }, - custom_agents=[ - { - "name": "db-agent", - "prompt": "Use the database tools.", - "tools": ["list_items", "run_query"], - "mcp_servers": { - "agent-db": { - "type": "http", - "url": "https://example.com/agent-db", - "tools": ["run_query"], - } - }, - } - ], - default_agent={"excluded_tools": ["run_query"]}, - ) - - assert captured["session.create"]["availableTools"] == [ - "list_items", - "session-db-list_items", - "session-wild-list_items", - "run_query", - "session-wild-run_query", - ] - assert captured["session.create"]["excludedTools"] == [ - "run_query", - "session-wild-run_query", - ] - assert captured["session.create"]["customAgents"][0]["tools"] == [ - "list_items", - "session-db-list_items", - "session-wild-list_items", - "run_query", - "session-wild-run_query", - "agent-db-run_query", - ] - assert captured["session.create"]["defaultAgent"]["excludedTools"] == [ - "run_query", - "session-wild-run_query", - ] - - @pytest.mark.asyncio - async def test_resume_session_normalizes_mcp_tool_filter_names_before_sending(self): - client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) - captured = {} - - async def mock_request(method, params, **kwargs): - captured[method] = params - if method == "session.resume": - return {"sessionId": params["sessionId"], "workspacePath": None} - return {} - - client._client = Mock() - client._client.request = AsyncMock(side_effect=mock_request) - await client.resume_session( - "session-with-mcp-filters", - on_permission_request=PermissionHandler.approve_all, - available_tools=["list_items", "run_query"], - excluded_tools=["run_query"], - mcp_servers={ - "session-db": { - "type": "http", - "url": "https://example.com/session-db", - "tools": ["list_items"], - }, - "session-wild": { - "type": "http", - "url": "https://example.com/session-wild", - "tools": ["*"], - }, - }, - custom_agents=[ - { - "name": "db-agent", - "prompt": "Use the database tools.", - "tools": ["list_items", "run_query"], - "mcp_servers": { - "agent-db": { - "type": "http", - "url": "https://example.com/agent-db", - "tools": ["run_query"], - } - }, - } - ], - default_agent={"excluded_tools": ["run_query"]}, - ) - - assert captured["session.resume"]["availableTools"] == [ - "list_items", - "session-db-list_items", - "session-wild-list_items", - "run_query", - "session-wild-run_query", - ] - assert captured["session.resume"]["excludedTools"] == [ - "run_query", - "session-wild-run_query", - ] - assert captured["session.resume"]["customAgents"][0]["tools"] == [ - "list_items", - "session-db-list_items", - "session-wild-list_items", - "run_query", - "session-wild-run_query", - "agent-db-run_query", - ] - assert captured["session.resume"]["defaultAgent"]["excludedTools"] == [ - "run_query", - "session-wild-run_query", - ] - @pytest.mark.asyncio async def test_mcp_auth_handler_registers_interest_after_cloud_create_only_with_handler(self): client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) diff --git a/python/test_tool_set.py b/python/test_tool_set.py index d45ebd7a6c..6a65e0df20 100644 --- a/python/test_tool_set.py +++ b/python/test_tool_set.py @@ -13,7 +13,6 @@ _enable_session_store_default, _enable_session_telemetry_default, _enable_skills_default, - _expand_mcp_tool_filter_names, _post_create_options_patch, _require_available_tools_for_empty_mode, _require_storage_for_empty_mode, @@ -136,35 +135,6 @@ def test_accepts_source_qualified_wildcard(self): def test_accepts_none(self): _validate_tool_filter_list("available_tools", None) - def test_expands_bare_mcp_tool_names_to_server_prefixed_names(self): - expanded = _expand_mcp_tool_filter_names( - ["list_items", "run_query"], - { - "my-server": {"type": "http", "tools": ["list_items"]}, - "other-server": {"type": "http", "tools": ["run_query"]}, - }, - ) - assert expanded == [ - "list_items", - "my-server-list_items", - "run_query", - "other-server-run_query", - ] - - def test_expands_bare_mcp_tool_names_for_wildcard_servers(self): - expanded = _expand_mcp_tool_filter_names( - ["run_query"], - {"my-server": {"type": "http", "tools": ["*"]}}, - ) - assert expanded == ["run_query", "my-server-run_query"] - - def test_leaves_unmatched_bare_names_unchanged(self): - expanded = _expand_mcp_tool_filter_names( - ["view"], - {"my-server": {"type": "http", "tools": ["list_items"]}}, - ) - assert expanded == ["view"] - class TestSystemMessageForMode: def test_copilot_cli_pass_through(self): diff --git a/rust/README.md b/rust/README.md index 6d92224088..985d35b156 100644 --- a/rust/README.md +++ b/rust/README.md @@ -31,6 +31,12 @@ client.stop().await.ok(); # } ``` +When targeting MCP tools configured through `mcp_servers`, remember the runtime +tool name is `-`. For `available_tools` and +`excluded_tools`, prefer `ToolSet::new().add_mcp("-")` +or the raw `mcp:-` form. For `custom_agents[].tools` +and `default_agent.excluded_tools`, use `-` directly. + ## Architecture ```text diff --git a/rust/src/types.rs b/rust/src/types.rs index 6ccb43c854..d5b4a92aa7 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -608,6 +608,8 @@ pub struct CustomAgentConfig { #[serde(default, skip_serializing_if = "Option::is_none")] pub description: Option, /// List of tool names the agent can use. `None` means all tools. + /// For MCP tools from `mcp_servers`, use the runtime tool name + /// `-`. #[serde(default, skip_serializing_if = "Option::is_none")] pub tools: Option>, /// Prompt content for the agent. @@ -706,6 +708,7 @@ impl CustomAgentConfig { #[serde(rename_all = "camelCase")] pub struct DefaultAgentConfig { /// Tool names to exclude from the default agent. + /// For MCP tools from `mcp_servers`, use `-`. #[serde(default, skip_serializing_if = "Option::is_none")] pub excluded_tools: Option>, } @@ -1598,9 +1601,14 @@ pub struct SessionConfig { pub extension_sdk_path: Option, /// Stable extension identity for canvas/tool providers on this connection. pub extension_info: Option, - /// Allowlist of built-in tool names the agent may use. + /// Allowlist of tool names the agent may use. + /// For MCP tools from `mcp_servers`, the runtime tool name is + /// `-`; prefer the source-qualified filter form + /// `mcp:-`. pub available_tools: Option>, - /// Blocklist of built-in tool names the agent must not use. + /// Blocklist of tool names the agent must not use. + /// Use the same MCP naming convention as `available_tools` when targeting + /// tools from `mcp_servers`. pub excluded_tools: Option>, /// Names of built-in agents to exclude from the session. /// @@ -1682,10 +1690,13 @@ pub struct SessionConfig { /// submission, session start/end, errors). pub hooks: Option, /// Custom agents (sub-agents) configured for this session. + /// When an agent `tools` list targets an MCP tool from `mcp_servers`, use + /// `-`. pub custom_agents: Option>, /// Configures the built-in default agent. Use `excluded_tools` to /// hide tools from the default agent while keeping them available - /// to custom sub-agents that reference them in their `tools` list. + /// to custom sub-agents that reference them in their `tools` list. For + /// MCP tools from `mcp_servers`, use `-`. pub default_agent: Option, /// Name of the custom agent to activate when the session starts. /// Must match the `name` of one of the agents in [`Self::custom_agents`]. @@ -2803,8 +2814,13 @@ pub struct ResumeSessionConfig { /// Stable extension identity for canvas/tool providers on this connection. pub extension_info: Option, /// Allowlist of tool names the agent may use. + /// For MCP tools from `mcp_servers`, the runtime tool name is + /// `-`; prefer the source-qualified filter form + /// `mcp:-`. pub available_tools: Option>, - /// Blocklist of built-in tool names. + /// Blocklist of tool names. + /// Use the same MCP naming convention as `available_tools` when targeting + /// tools from `mcp_servers`. pub excluded_tools: Option>, /// Names of built-in agents to exclude from the resumed session. /// @@ -2855,8 +2871,12 @@ pub struct ResumeSessionConfig { /// Enable session hooks on resume. pub hooks: Option, /// Custom agents to re-supply on resume. + /// When an agent `tools` list targets an MCP tool from `mcp_servers`, use + /// `-`. pub custom_agents: Option>, /// Configures the built-in default agent on resume. + /// For MCP tools from `mcp_servers`, use `-` in + /// `excluded_tools`. pub default_agent: Option, /// Name of the custom agent to activate. pub agent: Option,