Skip to content

Commit a1daaf1

Browse files
authored
Replace deprecated pydantic-ai MCP classes with MCPToolset in common.ai (#69006)
1 parent 5aa3ff7 commit a1daaf1

6 files changed

Lines changed: 130 additions & 108 deletions

File tree

providers/common/ai/docs/toolsets.rst

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,10 @@ passed to any pydantic-ai ``Agent``, including via
4242
.. note::
4343

4444
``AgentOperator`` accepts **any** ``AbstractToolset`` implementation — not
45-
just the Airflow-native toolsets above. PydanticAI's own MCP server
46-
classes (``MCPServerStreamableHTTP``, ``MCPServerSSE``, ``MCPServerStdio``)
47-
and third-party toolsets work too. The Airflow-native toolsets add
48-
connection management, secret backend integration, and the connection UI,
49-
but you are not locked in.
45+
just the Airflow-native toolsets above. PydanticAI's own ``MCPToolset``
46+
(built over a FastMCP transport) and third-party toolsets work too. The
47+
Airflow-native toolsets add connection management, secret backend
48+
integration, and the connection UI, but you are not locked in.
5049

5150

5251
Using Toolsets Directly with PydanticAI
@@ -336,29 +335,30 @@ Using Multiple MCP Servers
336335
],
337336
)
338337
339-
Direct PydanticAI MCP Servers
340-
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
338+
Direct PydanticAI MCP Toolsets
339+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
341340

342-
For prototyping or when you want full PydanticAI control, you can pass MCP
343-
server instances directly — no Airflow connection needed:
341+
For prototyping or when you want full PydanticAI control, you can pass
342+
``MCPToolset`` instances directly — no Airflow connection needed:
344343

345344
.. code-block:: python
346345
347-
from pydantic_ai.mcp import MCPServerStreamableHTTP, MCPServerStdio
346+
from fastmcp.client.transports import StdioTransport
347+
from pydantic_ai.mcp import MCPToolset
348348
349349
AgentOperator(
350350
task_id="direct_mcp",
351351
prompt="What tools are available?",
352352
llm_conn_id="pydanticai_default",
353353
toolsets=[
354-
MCPServerStreamableHTTP("http://localhost:3001/mcp"),
355-
MCPServerStdio("uvx", args=["mcp-run-python"]),
354+
MCPToolset("http://localhost:3001/mcp"),
355+
MCPToolset(StdioTransport(command="uvx", args=["mcp-run-python"])),
356356
],
357357
)
358358
359-
This works because PydanticAI's MCP server classes implement
360-
``AbstractToolset``. The tradeoff: URLs and credentials are hardcoded in DAG
361-
code instead of being managed through Airflow connections and secret backends.
359+
This works because PydanticAI's ``MCPToolset`` implements ``AbstractToolset``.
360+
The tradeoff: URLs and credentials are hardcoded in DAG code instead of being
361+
managed through Airflow connections and secret backends.
362362

363363

364364
.. _agent-skills:

providers/common/ai/src/airflow/providers/common/ai/example_dags/example_mcp.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -74,19 +74,20 @@ def example_mcp_multiple_servers():
7474

7575

7676
# ---------------------------------------------------------------------------
77-
# 3. Direct PydanticAI MCP servers (no Airflow connection needed)
77+
# 3. Direct PydanticAI MCP toolsets (no Airflow connection needed)
7878
# ---------------------------------------------------------------------------
79-
# AgentOperator accepts any PydanticAI AbstractToolset, including MCP servers
79+
# AgentOperator accepts any PydanticAI AbstractToolset, including MCPToolset
8080
# directly. Use this for prototyping or when you want full PydanticAI control.
8181
#
82-
# from pydantic_ai.mcp import MCPServerStreamableHTTP, MCPServerStdio
82+
# from fastmcp.client.transports import StdioTransport
83+
# from pydantic_ai.mcp import MCPToolset
8384
#
8485
# AgentOperator(
8586
# task_id="direct_mcp",
8687
# prompt="What tools are available?",
8788
# llm_conn_id="pydanticai_default",
8889
# toolsets=[
89-
# MCPServerStreamableHTTP("http://localhost:3001/mcp"),
90-
# MCPServerStdio("uvx", args=["mcp-run-python"]),
90+
# MCPToolset("http://localhost:3001/mcp"),
91+
# MCPToolset(StdioTransport(command="uvx", args=["mcp-run-python"])),
9192
# ],
9293
# )

providers/common/ai/src/airflow/providers/common/ai/hooks/mcp.py

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ class MCPHook(BaseHook):
4747
- **Extra.transport**: Transport type — ``http`` (default), ``sse``, or ``stdio``
4848
- **Extra.command**: Command to run for stdio transport (e.g. ``uvx``)
4949
- **Extra.args**: Command arguments for stdio transport (e.g. ``["mcp-run-python"]``)
50-
- **Extra.timeout**: Connection timeout in seconds for stdio (default: 10)
50+
- **Extra.timeout**: Connection init timeout in seconds for stdio (default: 10)
5151
5252
For HTTP/SSE transports the ``Authorization`` header is, by default, a static
5353
``Bearer`` token taken from the connection ``password``. Endpoints that require
@@ -121,22 +121,27 @@ def _auth_headers(self, conn: Any) -> dict[str, str] | None:
121121

122122
def get_conn(self) -> Any:
123123
"""
124-
Return a configured PydanticAI MCP server instance.
124+
Return a configured PydanticAI MCP toolset instance.
125125
126-
Creates the appropriate MCP server based on the transport type
127-
in the connection's extra field:
126+
Builds a :class:`~pydantic_ai.mcp.MCPToolset` over the FastMCP transport
127+
matching the transport type in the connection's extra field:
128128
129-
- ``http`` (default): :class:`~pydantic_ai.mcp.MCPServerStreamableHTTP`
130-
- ``sse``: :class:`~pydantic_ai.mcp.MCPServerSSE`
131-
- ``stdio``: :class:`~pydantic_ai.mcp.MCPServerStdio`
129+
- ``http`` (default): ``fastmcp.client.transports.StreamableHttpTransport``
130+
- ``sse``: ``fastmcp.client.transports.SSETransport``
131+
- ``stdio``: ``fastmcp.client.transports.StdioTransport``
132+
133+
When ``tool_prefix`` is set the toolset is wrapped via
134+
:meth:`~pydantic_ai.toolsets.abstract.AbstractToolset.prefixed`, so a
135+
prefix of ``"weather"`` yields tool names like ``weather_get_forecast``.
132136
133137
The result is cached for the lifetime of this hook instance.
134138
"""
135139
if self._server is not None:
136140
return self._server
137141

138142
try:
139-
from pydantic_ai.mcp import MCPServerSSE, MCPServerStdio, MCPServerStreamableHTTP
143+
from fastmcp.client.transports import SSETransport, StdioTransport, StreamableHttpTransport
144+
from pydantic_ai.mcp import MCPToolset
140145
except ImportError:
141146
raise ImportError(
142147
'MCP support requires the `mcp` package. Install it with: pip install "pydantic-ai-slim[mcp]"'
@@ -149,15 +154,11 @@ def get_conn(self) -> Any:
149154
if transport == "http":
150155
if not conn.host:
151156
raise ValueError(f"Connection {self.mcp_conn_id!r} requires a host URL for HTTP transport.")
152-
self._server = MCPServerStreamableHTTP(
153-
conn.host, headers=self._auth_headers(conn), tool_prefix=self.tool_prefix
154-
)
157+
toolset = MCPToolset(StreamableHttpTransport(conn.host, headers=self._auth_headers(conn)))
155158
elif transport == "sse":
156159
if not conn.host:
157160
raise ValueError(f"Connection {self.mcp_conn_id!r} requires a host URL for SSE transport.")
158-
self._server = MCPServerSSE(
159-
conn.host, headers=self._auth_headers(conn), tool_prefix=self.tool_prefix
160-
)
161+
toolset = MCPToolset(SSETransport(conn.host, headers=self._auth_headers(conn)))
161162
elif transport == "stdio":
162163
command = extra.get("command")
163164
if not command:
@@ -168,13 +169,14 @@ def get_conn(self) -> Any:
168169
if isinstance(args, str):
169170
args = [args]
170171
timeout = extra.get("timeout", 10)
171-
self._server = MCPServerStdio(command, args=args, timeout=timeout, tool_prefix=self.tool_prefix)
172+
toolset = MCPToolset(StdioTransport(command=command, args=args), init_timeout=timeout)
172173
else:
173174
raise ValueError(
174175
f"Unknown transport {transport!r} in connection {self.mcp_conn_id!r}. "
175176
"Supported: 'http', 'sse', 'stdio'."
176177
)
177178

179+
self._server = toolset.prefixed(self.tool_prefix) if self.tool_prefix else toolset
178180
return self._server
179181

180182
def test_connection(self) -> tuple[bool, str]:

providers/common/ai/src/airflow/providers/common/ai/toolsets/langchain_bridge.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,9 +97,9 @@ def airflow_toolset_to_langchain_tools(
9797
and every ``call_tool`` each run under their own event loop, and pydantic-ai
9898
opens and tears the connection down around each one. For ``MCPToolset`` this
9999
means the server is reconnected on every tool call. That is fine for
100-
stateless tools (and for HTTP/SSE servers, modulo per-call latency), but an
101-
``MCPServerStdio`` server, or any server that keeps state between calls,
102-
will lose that state because each call starts a fresh process/session.
100+
stateless tools (and for HTTP/SSE servers, modulo per-call latency), but a
101+
stdio server, or any server that keeps state between calls, will lose that
102+
state because each call starts a fresh process/session.
103103
104104
.. note::
105105
A pydantic-ai toolset is normally driven inside an agent run, where a

providers/common/ai/src/airflow/providers/common/ai/toolsets/mcp.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,18 +35,17 @@ class MCPToolset(AbstractToolset[Any]):
3535
3636
Reads MCP server transport type, URL, command, and credentials from the
3737
connection via :class:`~airflow.providers.common.ai.hooks.mcp.MCPHook` and
38-
creates the appropriate PydanticAI MCP server instance.
39-
All ``AbstractToolset`` methods delegate to the underlying MCP server.
38+
builds the matching PydanticAI :class:`~pydantic_ai.mcp.MCPToolset`.
39+
All ``AbstractToolset`` methods delegate to the underlying MCP toolset.
4040
4141
This is the recommended way to use MCP servers in Airflow — it stores
4242
server configuration in Airflow connections (and secret backends) rather
4343
than hard-coding URLs and credentials in DAG code.
4444
45-
If you prefer full PydanticAI control, you can pass MCP server instances
46-
directly to ``AgentOperator(toolsets=[...])``, since
47-
:class:`~pydantic_ai.mcp.MCPServerStreamableHTTP`,
48-
:class:`~pydantic_ai.mcp.MCPServerSSE`, and
49-
:class:`~pydantic_ai.mcp.MCPServerStdio` all implement ``AbstractToolset``.
45+
If you prefer full PydanticAI control, you can pass a
46+
:class:`~pydantic_ai.mcp.MCPToolset` (built over a FastMCP transport)
47+
directly to ``AgentOperator(toolsets=[...])``, since it implements
48+
``AbstractToolset``.
5049
5150
For MCP endpoints that need a freshly minted or short-lived token (e.g. a
5251
Snowflake managed MCP server authenticated with a key-pair JWT, or OAuth /

0 commit comments

Comments
 (0)