Skip to content

Commit 168e8b5

Browse files
gwharris7Copilot
andcommitted
Relocate MCP connection gating to agentframework extension
Move connection-readiness gating out of the core tooling package and into the Agent Framework extension. Core now only parses the per-server schema fields (connectivityStatus, allConnectionsUrl, missingConnectionsUrl) onto each MCPServerConfig and never gates or raises. Core: - Remove the internal McpDiscoveryResult dataclass, the deprecated _enforce_connection_readiness method, and the _CONNECTIVITY_READY constant. - _parse_gateway_response/_load_servers_from_gateway return List[MCPServerConfig] again; response-level aggregate fields are ignored. - Delete exceptions.py and drop McpConnectionsRequiredError from the public API. Extension (agentframework): - Own McpConnectionsRequiredError (all_connections_url, connectivity_status, server_names) and export it from the package root. - Gate at registration time in add_tool_servers_to_agent: when any discovered server is Pending, raise before building the agent so the turn handler can prompt the user with all_connections_url. Replaces the per-server FunctionTool placeholder mechanism. Raising at construction time (not inside a FunctionTool) is required because Agent Framework's tool-call loop swallows exceptions raised inside a tool and returns 'Error: Function failed.' to the model, which would drop the setup URL. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 22b3eab commit 168e8b5

12 files changed

Lines changed: 475 additions & 230 deletions

File tree

libraries/microsoft-agents-a365-tooling-extensions-agentframework/docs/design.md

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ McpToolRegistrationService.add_tool_servers_to_agent()
4141
4242
├── Resolve agent identity
4343
├── Exchange token for MCP scope
44-
├── Create MCPStreamableHTTPTool for each server
44+
├── Gate: raise McpConnectionsRequiredError if any server is Pending
45+
├── Create MCPStreamableHTTPTool for each (Ready) server
4546
└── Create ChatAgent with all tools
4647
4748
@@ -76,6 +77,51 @@ mcp_tool = MCPStreamableHTTPTool(
7677
)
7778
```
7879

80+
### Connection-readiness gating
81+
82+
MCP server discovery runs every turn. The gateway reports each server's
83+
`connectivityStatus` (`"Ready"` or `"Pending"`) along with an `allConnectionsUrl` the user
84+
can visit to view and manage the connectors required by that server. When **any** discovered
85+
server is `Pending`, `add_tool_servers_to_agent` raises `McpConnectionsRequiredError`
86+
**before** building the agent, so the developer's turn handler can prompt the user to set up
87+
connections and return. A later turn re-runs discovery and proceeds automatically once the
88+
connections are in place.
89+
90+
The gate raises at agent-construction time — not from inside a tool call — on purpose. Agent
91+
Framework's tool-call loop catches exceptions raised inside a `FunctionTool` and reflects them
92+
to the model as an opaque error string (`"Error: Function failed."` by default), which would
93+
drop the setup URL before it reached the user. Raising during construction lets the typed
94+
exception propagate intact to the turn handler.
95+
96+
```python
97+
from microsoft_agents_a365.tooling.extensions.agentframework import (
98+
McpToolRegistrationService,
99+
McpConnectionsRequiredError,
100+
)
101+
102+
service = McpToolRegistrationService()
103+
104+
try:
105+
agent = await service.add_tool_servers_to_agent(
106+
chat_client=chat_client,
107+
agent_instructions="You are a helpful assistant.",
108+
initial_tools=[],
109+
auth=auth_context,
110+
auth_handler_name="graph",
111+
turn_context=turn_context,
112+
)
113+
except McpConnectionsRequiredError as err:
114+
await turn_context.send_activity(
115+
f"Before I can help, please set up the required connections for "
116+
f"{', '.join(err.server_names)}: {err.all_connections_url}"
117+
)
118+
return # Skip running the model/tools this turn.
119+
```
120+
121+
`McpConnectionsRequiredError` exposes `all_connections_url`, `connectivity_status`, and
122+
`server_names`. It is owned and exported by this extension (`microsoft_agents_a365.tooling`
123+
core only parses the per-server connection metadata; it never gates or raises).
124+
79125
### Chat History API
80126

81127
The service provides methods to send chat history to the MCP platform for real-time threat protection analysis. This enables security scanning of conversation content.

libraries/microsoft-agents-a365-tooling-extensions-agentframework/microsoft_agents_a365/tooling/extensions/agentframework/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@
2121
__version__ = "1.0.0"
2222

2323
# Import services from the services module
24+
from .exceptions import McpConnectionsRequiredError
2425
from .services import McpToolRegistrationService
2526

2627
__all__ = [
2728
"McpToolRegistrationService",
29+
"McpConnectionsRequiredError",
2830
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
"""Exceptions raised by the Agent Framework MCP tooling extension."""
5+
6+
from typing import List, Optional
7+
8+
9+
class McpConnectionsRequiredError(Exception):
10+
"""Raised when one or more configured MCP servers are not yet connection-ready.
11+
12+
The tooling gateway reports a per-server ``connectivityStatus`` of ``"Pending"``
13+
when an MCP server has downstream connections the user has not yet established.
14+
``McpToolRegistrationService.add_tool_servers_to_agent`` discovers the servers
15+
every turn and raises this error *before* building the agent when any server is
16+
Pending, so the agent's turn handler can catch it, reply to the user with
17+
``all_connections_url``, and return without running the model/tools. A later turn
18+
re-runs discovery and proceeds once the connections are in place.
19+
20+
The error is raised at agent-construction time (not from inside a tool call) so it
21+
propagates to the developer's turn handler intact — Agent Framework's tool-call loop
22+
swallows exceptions raised inside a ``FunctionTool`` and reflects them to the model
23+
as an opaque error string, which would drop the setup URL.
24+
25+
``all_connections_url`` lets the user view and manage the full set of connectors
26+
required by the affected servers — including ones already set up — rather than being
27+
scoped strictly to the ones that happen to be missing right now.
28+
"""
29+
30+
def __init__(
31+
self,
32+
all_connections_url: Optional[str],
33+
connectivity_status: Optional[str],
34+
server_names: List[str],
35+
) -> None:
36+
self.all_connections_url = all_connections_url
37+
self.connectivity_status = connectivity_status
38+
self.server_names = server_names
39+
servers_text = ", ".join(server_names) if server_names else "(unknown)"
40+
super().__init__(
41+
f"MCP servers [{servers_text}] require connection setup "
42+
f"(connectivityStatus={connectivity_status}). "
43+
f"Set up connections at: {all_connections_url}"
44+
)

libraries/microsoft-agents-a365-tooling-extensions-agentframework/microsoft_agents_a365/tooling/extensions/agentframework/services/mcp_tool_registration_service.py

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,18 @@
2828
is_development_environment,
2929
)
3030

31+
from ..exceptions import McpConnectionsRequiredError
32+
3133

3234
# Default timeout for MCP server HTTP requests (in seconds)
3335
MCP_HTTP_CLIENT_TIMEOUT_SECONDS = 90.0
3436

37+
# Sentinel per-server status that means a server's downstream connections are
38+
# already in place. Anything else (typically "Pending") triggers the
39+
# connection-readiness gate, which raises McpConnectionsRequiredError before the
40+
# agent is built so the turn handler can prompt the user to set up connections.
41+
_CONNECTIVITY_READY = "Ready"
42+
3543

3644
class McpToolRegistrationService:
3745
"""
@@ -114,11 +122,59 @@ async def add_tool_servers_to_agent(
114122
)
115123

116124
self._logger.info(f"Loaded {len(server_configs)} MCP server configurations")
125+
for c in server_configs:
126+
_name = c.mcp_server_name or c.mcp_server_unique_name
127+
self._logger.info(
128+
" per-server gateway state: name=%s connectivity_status=%r missing_url=%s",
129+
_name,
130+
c.connectivity_status,
131+
c.missing_connections_url,
132+
)
133+
134+
# Connection-readiness gate. Discovery runs every turn; when the
135+
# gateway flags any MCP server's downstream connection(s) as not yet
136+
# set up (``connectivityStatus == "Pending"``), raise before building
137+
# the agent so the developer's turn handler can catch the error,
138+
# prompt the user with ``all_connections_url``, and return without
139+
# running the model/tools. A later turn re-runs discovery and
140+
# proceeds automatically once the connections are in place.
141+
#
142+
# The gate raises here — at agent-construction time — rather than
143+
# from inside a tool call on purpose: Agent Framework's tool-call
144+
# loop swallows exceptions raised inside a ``FunctionTool`` and
145+
# reflects them to the model as an opaque error string, which would
146+
# drop the setup URL before it ever reaches the user.
147+
pending = [
148+
c
149+
for c in server_configs
150+
if c.connectivity_status is not None
151+
and c.connectivity_status != _CONNECTIVITY_READY
152+
]
153+
if pending:
154+
pending_names = [c.mcp_server_name or c.mcp_server_unique_name for c in pending]
155+
# Surface all_connections_url so the user can view and manage the
156+
# full set of connectors required by the affected servers. Use the
157+
# first pending server that provides one (they are environment-scoped
158+
# and typically identical across servers).
159+
all_connections_url = next(
160+
(c.all_connections_url for c in pending if c.all_connections_url),
161+
None,
162+
)
163+
self._logger.info(
164+
"MCP connection gate blocking turn: pending servers=%s, setup URL=%s",
165+
pending_names,
166+
all_connections_url,
167+
)
168+
raise McpConnectionsRequiredError(
169+
all_connections_url=all_connections_url,
170+
connectivity_status=pending[0].connectivity_status,
171+
server_names=pending_names,
172+
)
117173

118174
# Create the agent with all tools (initial + MCP tools)
119175
all_tools = list(initial_tools)
120176

121-
# Add servers as MCPStreamableHTTPTool instances
177+
# Add each Ready server as an MCPStreamableHTTPTool instance.
122178
for config in server_configs:
123179
# Use mcp_server_name if available (not None or empty), otherwise fall back to mcp_server_unique_name
124180
server_name = config.mcp_server_name or config.mcp_server_unique_name
@@ -183,6 +239,9 @@ async def add_tool_servers_to_agent(
183239
self._logger.info(f"Agent created with {len(all_tools)} total tools")
184240
return agent
185241

242+
except McpConnectionsRequiredError:
243+
# Connection-readiness gate — propagate intact to the turn handler.
244+
raise
186245
except Exception as ex:
187246
self._logger.error(f"Failed to add tool servers to agent: {ex}")
188247
raise

libraries/microsoft-agents-a365-tooling/CHANGELOG.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3535
- Added `ChatHistoryMessage` Pydantic model for representing individual messages in chat history
3636
- Added `ChatMessageRequest` Pydantic model for the chat history API request payload
3737
- Added `py.typed` marker for PEP 561 compliance, enabling type checker support
38-
- Added connection-readiness gating to `McpToolServerConfigurationService.list_tool_servers()`. When the tooling gateway reports an aggregate `connectivityStatus` of `"Pending"`, a `McpConnectionsRequiredError` is raised carrying the response-level `missingConnectionsUrl` and the names of the not-`Ready` servers, so the agent's turn handler can prompt the user to set up connections instead of running tools that would fail. Dev-mode manifests and legacy raw-array gateway responses omit the field and are never gated
39-
- Added `McpConnectionsRequiredError` exception (exported from `microsoft_agents_a365.tooling`) with `missing_connections_url`, `connectivity_status`, and `server_names` attributes
40-
- Added `id`, `all_connections_url`, `missing_connections_url`, and `connectivity_status` fields to `MCPServerConfig`, parsed from the per-server gateway payload
41-
- Added internal `McpDiscoveryResult` dataclass; `_parse_gateway_response()` and `_load_servers_from_gateway()` now return it to carry response-level connection metadata alongside the server list (the public return type of `list_tool_servers()` remains `List[MCPServerConfig]`)
38+
- Added `id`, `all_connections_url`, `missing_connections_url`, and `connectivity_status` fields to `MCPServerConfig`, parsed from the per-server gateway payload (`allConnectionsUrl`, `missingConnectionsUrl`, `connectivityStatus`). The core service parses this connection metadata onto each server but does not gate on it — connection-readiness gating is owned by the framework-specific tooling extensions
39+
- `_parse_gateway_response()` and `_load_servers_from_gateway()` return `List[MCPServerConfig]`; the wrapped `{"mcpServers": [...]}` and legacy raw-array gateway response shapes are both supported, and response-level (aggregate) connection fields are ignored

libraries/microsoft-agents-a365-tooling/docs/design.md

Lines changed: 9 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -108,35 +108,15 @@ User-Agent: Agent365SDK/0.1.0 (...)
108108

109109
The gateway returns the same JSON structure, but `mcpServerUniqueName` contains the full endpoint URL.
110110

111-
### Connection gating
112-
113-
When the tooling gateway reports that an agent's MCP servers have unsatisfied downstream
114-
connections, `list_tool_servers()` raises `McpConnectionsRequiredError`. Discovery runs
115-
every turn, so the agent's turn handler should catch the error, reply with the
116-
connection-setup link, and return — a later turn proceeds automatically once the user has
117-
connected.
118-
119-
```python
120-
from microsoft_agents_a365.tooling import McpConnectionsRequiredError
121-
122-
try:
123-
servers = await config_service.list_tool_servers(
124-
agentic_app_id=agentic_app_id,
125-
auth_token=auth_token,
126-
authorization=auth,
127-
auth_handler_name=auth_handler_name,
128-
turn_context=context,
129-
)
130-
except McpConnectionsRequiredError as err:
131-
await context.send_activity(
132-
f"Before I can help, please set up the required connections for "
133-
f"{', '.join(err.server_names)}: {err.missing_connections_url}"
134-
)
135-
return # Skip running the model/tools this turn.
136-
```
137-
138-
The gate fires only for gateway responses with aggregate `connectivityStatus == "Pending"`.
139-
Dev-mode manifests and legacy raw-array responses omit the field and are never gated.
111+
### Connection metadata (per-server)
112+
113+
The core service parses each server's connection metadata — `connectivityStatus`,
114+
`allConnectionsUrl`, and `missingConnectionsUrl` — onto the returned `MCPServerConfig`
115+
objects, but it does **not** gate on them. Connection-readiness gating is the
116+
responsibility of the framework-specific tooling extensions (e.g.
117+
`microsoft-agents-a365-tooling-extensions-agentframework`), which decide how to surface a
118+
`Pending` server and raise/handle the appropriate error. See the relevant extension's
119+
design document for details.
140120

141121
### MCPServerConfig ([models/mcp_server_config.py](../microsoft_agents_a365/tooling/models/mcp_server_config.py))
142122

libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/__init__.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
Provides base utilities and common helper functions.
99
"""
1010

11-
from .exceptions import McpConnectionsRequiredError
1211
from .models import MCPServerConfig
1312
from .services import McpToolServerConfigurationService
1413
from .utils import Constants
@@ -23,7 +22,6 @@
2322
__all__ = [
2423
"MCPServerConfig",
2524
"McpToolServerConfigurationService",
26-
"McpConnectionsRequiredError",
2725
"Constants",
2826
"get_tooling_gateway_for_digital_worker",
2927
"get_mcp_base_url",

libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/exceptions.py

Lines changed: 0 additions & 33 deletions
This file was deleted.

0 commit comments

Comments
 (0)