1- from typing import TYPE_CHECKING , Any
1+ from __future__ import annotations
2+
3+ from contextlib import asynccontextmanager
4+ from typing import TYPE_CHECKING , Any , AsyncIterator
25
36from dspy .adapters .types .tool import Tool , convert_input_schema_to_tool_args
47
58if TYPE_CHECKING :
69 import mcp
710
811
9- def _convert_mcp_tool_result (call_tool_result : " mcp.types.CallToolResult" ) -> str | list [Any ]:
12+ def _convert_mcp_tool_result (call_tool_result : mcp .types .CallToolResult ) -> str | list [Any ]:
1013 from mcp .types import TextContent
1114
1215 text_contents : list [TextContent ] = []
@@ -27,7 +30,7 @@ def _convert_mcp_tool_result(call_tool_result: "mcp.types.CallToolResult") -> st
2730 return tool_content or non_text_contents
2831
2932
30- def convert_mcp_tool (session : " mcp.ClientSession" , tool : " mcp.types.Tool" ) -> Tool :
33+ def convert_mcp_tool (session : mcp .ClientSession , tool : mcp .types .Tool ) -> Tool :
3134 """Build a DSPy tool from an MCP tool.
3235
3336 Args:
@@ -45,3 +48,122 @@ async def func(*args, **kwargs):
4548 return _convert_mcp_tool_result (result )
4649
4750 return Tool (func = func , name = tool .name , desc = tool .description , args = args , arg_types = arg_types , arg_desc = arg_desc )
51+
52+
53+ def _validate_tool_filters (include_tools : list [str ] | None , exclude_tools : list [str ] | None ) -> None :
54+ if include_tools and exclude_tools :
55+ raise ValueError ("Cannot specify both include_tools and exclude_tools." )
56+
57+
58+ def _validate_tool_names (requested : list [str ] | None , available : list [str ]) -> None :
59+ if not requested :
60+ return
61+ unknown = set (requested ) - set (available )
62+ if unknown :
63+ raise ValueError (f"Tool names not found on MCP server: { unknown } . Available tools: { set (available )} " )
64+
65+
66+ def _filter_mcp_tools (
67+ tools : list [mcp .types .Tool ],
68+ include_tools : list [str ] | None ,
69+ exclude_tools : list [str ] | None ,
70+ ) -> list [mcp .types .Tool ]:
71+ if include_tools is not None :
72+ tools = [t for t in tools if t .name in set (include_tools )]
73+ elif exclude_tools is not None :
74+ excluded = set (exclude_tools )
75+ tools = [t for t in tools if t .name not in excluded ]
76+ return tools
77+
78+
79+ @asynccontextmanager
80+ async def stdio_mcp_tools (
81+ server_params : mcp .StdioServerParameters ,
82+ * ,
83+ include_tools : list [str ] | None = None ,
84+ exclude_tools : list [str ] | None = None ,
85+ timeout : float | None = None ,
86+ ) -> AsyncIterator [list [Tool ]]:
87+ """Yield DSPy tools backed by a live MCP stdio session.
88+
89+ Usage::
90+
91+ params = StdioServerParameters(command="docker", args=[...])
92+ async with stdio_mcp_tools(params, exclude_tools=["dangerous"], timeout=30) as tools:
93+ react = dspy.ReAct(MySignature, tools=tools)
94+ result = await react.acall(...)
95+
96+ Args:
97+ server_params: MCP stdio server parameters.
98+ include_tools: If set, only include tools whose names are in this list.
99+ exclude_tools: If set, exclude tools whose names are in this list.
100+ timeout: Read timeout in seconds for the MCP session. None means no timeout.
101+
102+ Yields:
103+ A list of DSPy Tool objects.
104+
105+ Raises:
106+ ValueError: If both include_tools and exclude_tools are specified, or if any
107+ requested tool names are not found on the MCP server.
108+ """
109+ _validate_tool_filters (include_tools , exclude_tools )
110+
111+ from datetime import timedelta
112+
113+ from mcp import ClientSession
114+ from mcp .client .stdio import stdio_client
115+
116+ read_timeout = timedelta (seconds = timeout ) if timeout is not None else None
117+ async with stdio_client (server_params ) as (read , write ):
118+ async with ClientSession (read , write , read_timeout_seconds = read_timeout ) as session :
119+ await session .initialize ()
120+ listed = await session .list_tools ()
121+ _validate_tool_names (include_tools or exclude_tools , [t .name for t in listed .tools ])
122+ filtered = _filter_mcp_tools (listed .tools , include_tools , exclude_tools )
123+ yield [convert_mcp_tool (session , t ) for t in filtered ]
124+
125+
126+ @asynccontextmanager
127+ async def http_mcp_tools (
128+ url : str ,
129+ * ,
130+ include_tools : list [str ] | None = None ,
131+ exclude_tools : list [str ] | None = None ,
132+ timeout : float | None = None ,
133+ ) -> AsyncIterator [list [Tool ]]:
134+ """Yield DSPy tools backed by a live MCP streamable-HTTP session.
135+
136+ Usage::
137+
138+ async with http_mcp_tools("http://127.0.0.1:8003/mcp", include_tools=["search"], timeout=30) as tools:
139+ react = dspy.ReAct(MySignature, tools=tools)
140+ result = await react.acall(...)
141+
142+ Args:
143+ url: The URL of the MCP streamable-HTTP server.
144+ include_tools: If set, only include tools whose names are in this list.
145+ exclude_tools: If set, exclude tools whose names are in this list.
146+ timeout: Read timeout in seconds for the MCP session. None means no timeout.
147+
148+ Yields:
149+ A list of DSPy Tool objects.
150+
151+ Raises:
152+ ValueError: If both include_tools and exclude_tools are specified, or if any
153+ requested tool names are not found on the MCP server.
154+ """
155+ _validate_tool_filters (include_tools , exclude_tools )
156+
157+ from datetime import timedelta
158+
159+ from mcp import ClientSession
160+ from mcp .client .streamable_http import streamable_http_client
161+
162+ read_timeout = timedelta (seconds = timeout ) if timeout is not None else None
163+ async with streamable_http_client (url ) as (read , write , _ ):
164+ async with ClientSession (read , write , read_timeout_seconds = read_timeout ) as session :
165+ await session .initialize ()
166+ listed = await session .list_tools ()
167+ _validate_tool_names (include_tools or exclude_tools , [t .name for t in listed .tools ])
168+ filtered = _filter_mcp_tools (listed .tools , include_tools , exclude_tools )
169+ yield [convert_mcp_tool (session , t ) for t in filtered ]
0 commit comments