Skip to content

Commit 867d24b

Browse files
gwharris7Copilot
andcommitted
Make MCP connection gating non-blocking per server
Switch the agentframework extension from blocking gating (raise McpConnectionsRequiredError before building the agent when any server is Pending) to non-blocking per-server gating: - Ready servers (and legacy sources with no connectivityStatus) are wired as live MCPStreamableHTTPTool instances, unchanged. - A Pending server is registered as a single placeholder FunctionTool named after the server (max_invocations=1). When the model invokes it, the placeholder RETURNS a static setup message including missing_connections_url (fallback all_connections_url) for the model to relay. The agent is always built and Ready tools stay usable; the user is only prompted about connections if their request actually needs the Pending server. The placeholder returns the message rather than raising it: Agent Framework swallows exceptions raised inside a tool and converts UserInputRequiredException into tool-result content, so neither reaches the turn handler. Returning the text is the only way to surface the URL through the model. McpConnectionsRequiredError stays exported for developers who want custom blocking gating; it now carries missing_connections_url, connectivity_status, server_names, and shares the new format_mcp_connections_required_message helper with the placeholder so the message wording has a single source of truth. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 168e8b5 commit 867d24b

5 files changed

Lines changed: 328 additions & 178 deletions

File tree

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

Lines changed: 50 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,7 @@ McpToolRegistrationService.add_tool_servers_to_agent()
4141
4242
├── Resolve agent identity
4343
├── Exchange token for MCP scope
44-
├── Gate: raise McpConnectionsRequiredError if any server is Pending
45-
├── Create MCPStreamableHTTPTool for each (Ready) server
44+
├── Gate (per server, non-blocking): Ready → MCPStreamableHTTPTool; Pending → placeholder tool
4645
└── Create ChatAgent with all tools
4746
4847
@@ -77,50 +76,65 @@ mcp_tool = MCPStreamableHTTPTool(
7776
)
7877
```
7978

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.
79+
### Connection-readiness gating (non-blocking, per server)
80+
81+
MCP server discovery runs every turn. The gateway reports each server's `connectivityStatus`
82+
(`"Ready"` or `"Pending"`) along with a `missingConnectionsUrl` the user can visit to set up the
83+
connection(s) that server needs. Gating is **per server and non-blocking**:
84+
85+
- A **Ready** server (or a legacy source with no `connectivityStatus`) is wired as a live
86+
`MCPStreamableHTTPTool`, exactly as before.
87+
- A **Pending** server is wired as a single **placeholder tool** named after the server. The agent
88+
is still built and the Ready servers remain fully usable for the turn. Only if the model invokes
89+
the placeholder — because the user's request actually needs that server — does it return a static
90+
message (including `missingConnectionsUrl`) for the model to relay to the user. If the turn never
91+
needs the Pending server, the user is never bothered. A later turn re-runs discovery and wires the
92+
server for real once its connections are in place.
93+
94+
The placeholder **returns** the message rather than raising it. Agent Framework's tool-call loop
95+
catches exceptions raised inside a `FunctionTool` and reflects them to the model as an opaque error
96+
string (`"Error: Function failed."` by default), and it converts `UserInputRequiredException` into
97+
tool-result content rather than propagating it — so returning the text is the only way to surface
98+
the setup URL through the model. Because surfacing flows through the model, exact verbatim delivery
99+
is best-effort; the placeholder's description instructs the model to relay the message and URL
100+
verbatim, and `max_invocations=1` stops the model from looping on it within a turn.
101+
102+
> **Note:** A Pending server is represented by one placeholder named after the server (its real
103+
> sub-tools are invisible until it connects). The model routes the user's intent to it by server
104+
> name plus description — best-effort, not guaranteed.
95105
96106
```python
97107
from microsoft_agents_a365.tooling.extensions.agentframework import (
98108
McpToolRegistrationService,
99-
McpConnectionsRequiredError,
100109
)
101110

102111
service = McpToolRegistrationService()
103112

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.
113+
# Non-blocking: Pending servers become placeholders, so no special handling is needed in the
114+
# turn handler. The agent is always built and Ready tools always run.
115+
agent = await service.add_tool_servers_to_agent(
116+
chat_client=chat_client,
117+
agent_instructions="You are a helpful assistant.",
118+
initial_tools=[],
119+
auth=auth_context,
120+
auth_handler_name="graph",
121+
turn_context=turn_context,
122+
)
123+
```
124+
125+
For developers who instead want **blocking** behavior (abort the whole turn until every server is
126+
connected), the extension still exports `McpConnectionsRequiredError`. Inspect the discovered
127+
servers yourself and raise it before building the agent:
128+
129+
```python
130+
from microsoft_agents_a365.tooling.extensions.agentframework import McpConnectionsRequiredError
119131
```
120132

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).
133+
`McpConnectionsRequiredError` exposes `missing_connections_url`, `connectivity_status`, and
134+
`server_names`, and its message is built from the same `format_mcp_connections_required_message`
135+
helper the placeholder uses. It is owned and exported by this extension
136+
(`microsoft_agents_a365.tooling` core only parses the per-server connection metadata; it never
137+
gates or raises).
124138

125139
### Chat History API
126140

Original file line numberDiff line numberDiff line change
@@ -1,44 +1,77 @@
11
# Copyright (c) Microsoft Corporation.
22
# Licensed under the MIT License.
33

4-
"""Exceptions raised by the Agent Framework MCP tooling extension."""
4+
"""Exceptions and message helpers for the Agent Framework MCP tooling extension."""
55

66
from typing import List, Optional
77

88

9+
def format_mcp_connections_required_message(
10+
*,
11+
server_names: List[str],
12+
connectivity_status: Optional[str],
13+
missing_connections_url: Optional[str],
14+
) -> str:
15+
"""Build the static, user-facing message shown when an MCP server needs connection setup.
16+
17+
This single helper is the source of truth for the wording so that the message a Pending
18+
server's placeholder tool returns to the model is identical to the message carried by
19+
``McpConnectionsRequiredError``.
20+
21+
Args:
22+
server_names: Names of the MCP server(s) whose downstream connections are not set up.
23+
connectivity_status: The gateway-reported ``connectivityStatus`` (typically ``"Pending"``).
24+
missing_connections_url: URL the user opens to set up the missing connection(s). May be
25+
``None`` when the gateway did not supply one.
26+
27+
Returns:
28+
A human-readable message suitable for relaying verbatim to the end user.
29+
"""
30+
servers_text = ", ".join(server_names) if server_names else "(unknown)"
31+
message = (
32+
f"The tool(s) from MCP server(s) [{servers_text}] can't be used yet because the "
33+
f"required data connection(s) aren't set up (connectivityStatus={connectivity_status})."
34+
)
35+
if missing_connections_url:
36+
message += f" Set up the missing connection(s) here: {missing_connections_url}"
37+
else:
38+
message += " Ask your administrator to set up the required connection(s)."
39+
return message
40+
41+
942
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.
43+
"""Raised when an MCP server the user invoked is not yet connection-ready.
44+
45+
The tooling gateway reports a per-server ``connectivityStatus`` of ``"Pending"`` when an MCP
46+
server has downstream connections the user has not yet established. Discovery runs every turn.
47+
48+
The agentframework extension gates **per server, non-blocking**: Ready servers are wired as
49+
real tools and a Pending server is registered as a placeholder tool. Only when the model
50+
actually invokes that placeholder (because the user's request needs the server) is the static
51+
setup message — including ``missing_connections_url`` — surfaced; the rest of the turn runs
52+
normally with the Ready tools.
53+
54+
This exception is **not** raised by the non-blocking gate (Agent Framework swallows exceptions
55+
raised inside a tool, and converts ``UserInputRequiredException`` into tool-result content, so
56+
neither reaches the developer's turn handler). It is exported for developers who want to
57+
implement their own *blocking* gating — e.g. inspect the discovered servers and raise this
58+
before building the agent to abort the whole turn — and as the structured carrier of the same
59+
message the placeholder returns.
2860
"""
2961

3062
def __init__(
3163
self,
32-
all_connections_url: Optional[str],
64+
missing_connections_url: Optional[str],
3365
connectivity_status: Optional[str],
3466
server_names: List[str],
3567
) -> None:
36-
self.all_connections_url = all_connections_url
68+
self.missing_connections_url = missing_connections_url
3769
self.connectivity_status = connectivity_status
3870
self.server_names = server_names
39-
servers_text = ", ".join(server_names) if server_names else "(unknown)"
4071
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}"
72+
format_mcp_connections_required_message(
73+
server_names=server_names,
74+
connectivity_status=connectivity_status,
75+
missing_connections_url=missing_connections_url,
76+
)
4477
)

0 commit comments

Comments
 (0)