mirascope 1.21.6__py3-none-any.whl → 1.22.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
mirascope/llm/__init__.py CHANGED
@@ -4,6 +4,7 @@ from ._context import context
4
4
  from ._override import override
5
5
  from .call_response import CallResponse
6
6
  from .stream import Stream
7
+ from .tool import Tool
7
8
 
8
9
  __all__ = [
9
10
  "CallResponse",
@@ -11,6 +12,7 @@ __all__ = [
11
12
  "LocalProvider",
12
13
  "Provider",
13
14
  "Stream",
15
+ "Tool",
14
16
  "calculate_cost",
15
17
  "call",
16
18
  "context",
mirascope/mcp/__init__.py CHANGED
@@ -1,6 +1,7 @@
1
1
  """Mirascope Model Context Protocol (MCP) implementation."""
2
2
 
3
- from .server import MCPServer
4
- from .tools import MCPTool
3
+ from .client import MCPClient, sse_client, stdio_client
4
+ from .server import MCPServer # DEPRECATED
5
+ from .tools import MCPTool # DEPRECATED
5
6
 
6
- __all__ = ["MCPServer", "MCPTool"]
7
+ __all__ = ["MCPClient", "MCPServer", "MCPTool", "sse_client", "stdio_client"]
@@ -0,0 +1,277 @@
1
+ """MCP client utility functions."""
2
+
3
+ import base64
4
+ import string
5
+ from collections.abc import AsyncGenerator, Awaitable, Callable
6
+ from contextlib import asynccontextmanager
7
+ from typing import Any
8
+
9
+ from anyio import create_memory_object_stream, create_task_group
10
+ from anyio.streams.memory import MemoryObjectReceiveStream
11
+ from mcp.types import (
12
+ EmbeddedResource,
13
+ ImageContent,
14
+ JSONRPCMessage,
15
+ PromptMessage,
16
+ TextContent,
17
+ TextResourceContents,
18
+ )
19
+ from mcp.types import Tool as MCPTool
20
+ from pydantic import BaseModel, ConfigDict, Field, create_model
21
+
22
+ from ..core import BaseMessageParam, BaseTool
23
+ from ..core.base import AudioPart, DocumentPart, ImagePart
24
+
25
+
26
+ def create_tool_call(
27
+ name: str,
28
+ call_tool: Callable[[str, dict | None], Awaitable[Any]],
29
+ ) -> Callable[..., Awaitable[list[str | ImageContent | EmbeddedResource]]]:
30
+ """Create a tool call function for a Mirascope Tool.
31
+
32
+ Args:
33
+ name: The name of the tool
34
+ call_tool: The function to call the tool
35
+
36
+ Returns:
37
+ A function that can be used as the call method for a Tool
38
+ """
39
+
40
+ async def call(self: BaseTool) -> list[str | ImageContent | EmbeddedResource]:
41
+ result = await call_tool(name, self.args) # pyright: ignore [reportOptionalCall]
42
+ if result.isError:
43
+ raise RuntimeError(f"MCP Server returned error: {self._name()}")
44
+ return [
45
+ content.text if isinstance(content, TextContent) else content
46
+ for content in result.content
47
+ ]
48
+
49
+ return call
50
+
51
+
52
+ def convert_prompt_message_to_base_message_params(
53
+ prompt_message: PromptMessage,
54
+ ) -> BaseMessageParam:
55
+ """
56
+ Convert a single PromptMessage back into a BaseMessageParam.
57
+
58
+ Args:
59
+ prompt_message: A PromptMessage instance.
60
+
61
+ Returns:
62
+ A BaseMessageParam instance representing the given prompt_message.
63
+
64
+ Raises:
65
+ ValueError: If the role is invalid or if the content type is unsupported.
66
+ """
67
+
68
+ # Validate role
69
+ if prompt_message.role not in ["user", "assistant"]:
70
+ raise ValueError(f"invalid role: {prompt_message.role}")
71
+
72
+ content = prompt_message.content
73
+
74
+ # Handle TextContent
75
+ if isinstance(content, TextContent):
76
+ # If it's text, we can just return a single string
77
+ return BaseMessageParam(role=prompt_message.role, content=content.text)
78
+
79
+ # Handle ImageContent
80
+ elif isinstance(content, ImageContent):
81
+ decoded_image = base64.b64decode(content.data)
82
+
83
+ image_part = ImagePart(
84
+ type="image",
85
+ image=decoded_image,
86
+ media_type=content.mimeType,
87
+ detail=None, # detail not provided by PromptMessage
88
+ )
89
+ return BaseMessageParam(role=prompt_message.role, content=[image_part])
90
+
91
+ elif isinstance(content, EmbeddedResource):
92
+ resource = content.resource
93
+ if isinstance(resource, TextResourceContents):
94
+ # For text content embedded resources, just return the text
95
+ return BaseMessageParam(role=prompt_message.role, content=resource.text)
96
+ else:
97
+ mime_type = resource.mimeType
98
+ if not mime_type:
99
+ raise ValueError(
100
+ "BlobResourceContents has no mimeType, cannot determine content type."
101
+ )
102
+
103
+ decoded_data = base64.b64decode(resource.blob)
104
+
105
+ if mime_type.startswith("image/"):
106
+ # Treat as ImagePart
107
+ image_part = ImagePart(
108
+ type="image",
109
+ image=decoded_data,
110
+ media_type=mime_type,
111
+ detail=None,
112
+ )
113
+ return BaseMessageParam(role=prompt_message.role, content=[image_part])
114
+ elif mime_type == "application/pdf":
115
+ doc_part = DocumentPart(
116
+ type="document", media_type=mime_type, document=decoded_data
117
+ )
118
+ return BaseMessageParam(role=prompt_message.role, content=[doc_part])
119
+ elif mime_type.startswith("audio/"):
120
+ # Treat as AudioPart
121
+ audio_part = AudioPart(
122
+ type="audio", media_type=mime_type, audio=decoded_data
123
+ )
124
+ return BaseMessageParam(role=prompt_message.role, content=[audio_part])
125
+ else:
126
+ raise ValueError(f"Unsupported mime type: {mime_type}")
127
+
128
+ else:
129
+ raise ValueError(f"Unsupported content type: {type(content)}")
130
+
131
+
132
+ def snake_to_pascal(snake: str) -> str:
133
+ """Convert a snake_case string to PascalCase.
134
+
135
+ Args:
136
+ snake: A snake_case string
137
+
138
+ Returns:
139
+ The string converted to PascalCase
140
+ """
141
+ return string.capwords(snake.replace("_", " ")).replace(" ", "")
142
+
143
+
144
+ def build_object_type(
145
+ properties: dict[str, Any], required: list[str], name: str
146
+ ) -> type[BaseModel]:
147
+ """Build a Pydantic model from JSON Schema properties.
148
+
149
+ Args:
150
+ properties: JSON Schema properties dictionary
151
+ required: List of required property names
152
+ name: Name for the generated model
153
+
154
+ Returns:
155
+ A dynamically created Pydantic model class
156
+ """
157
+ fields = {}
158
+ for prop_name, prop_schema in properties.items():
159
+ class_name = snake_to_pascal(prop_name)
160
+ type_ = json_schema_to_python_type(prop_schema, class_name)
161
+ if prop_name in required:
162
+ fields[prop_name] = (
163
+ type_,
164
+ Field(..., description=prop_schema.get("description")),
165
+ )
166
+ else:
167
+ fields[prop_name] = (
168
+ type_ | None,
169
+ Field(None, description=prop_schema.get("description")),
170
+ )
171
+
172
+ return create_model(name, __config__=ConfigDict(extra="allow"), **fields)
173
+
174
+
175
+ def json_schema_to_python_type(schema: dict[str, Any], name: str = "Model") -> Any: # noqa: ANN401
176
+ """
177
+ Recursively convert a JSON Schema snippet into a Python type annotation or a dynamically generated pydantic model.
178
+
179
+ Args:
180
+ schema: JSON Schema to convert
181
+ name: Name for any generated models
182
+
183
+ Returns:
184
+ Corresponding Python type or Pydantic model
185
+ """
186
+ json_type = schema.get("type", "any")
187
+
188
+ if json_type == "string":
189
+ return str
190
+ elif json_type == "number":
191
+ return float
192
+ elif json_type == "integer":
193
+ return int
194
+ elif json_type == "boolean":
195
+ return bool
196
+ elif json_type == "array":
197
+ # Recursively determine the items type for arrays
198
+ items_schema = schema.get("items", {})
199
+ items_type = json_schema_to_python_type(items_schema)
200
+ return list[items_type]
201
+ elif json_type == "object":
202
+ # Recursively build a dynamic model for objects
203
+ properties = schema.get("properties", {})
204
+ required = schema.get("required", [])
205
+ return build_object_type(properties, required, name)
206
+ else:
207
+ # Default fallback
208
+ return Any
209
+
210
+
211
+ def create_tool_from_mcp_tool(tool: MCPTool) -> type[BaseTool]:
212
+ """
213
+ Create a `BaseTool` type definition from the given MCP Tool instance.
214
+
215
+ Args:
216
+ tool: MCP tool instance.
217
+
218
+ Returns:
219
+ A dynamically created `Tool` schema.
220
+ """
221
+ schema = tool.inputSchema
222
+ properties = schema.get("properties", {})
223
+ required_fields = schema.get("required", [])
224
+
225
+ fields = {}
226
+ for field_name, field_schema in properties.items():
227
+ field_type = json_schema_to_python_type(field_schema)
228
+
229
+ if field_name in required_fields:
230
+ default = Field(..., description=field_schema.get("description"))
231
+ annotation = field_type
232
+ else:
233
+ default = Field(None, description=field_schema.get("description"))
234
+ annotation = field_type | None
235
+
236
+ fields[field_name] = (annotation, default)
237
+
238
+ return create_model(snake_to_pascal(tool.name), __base__=BaseTool, **fields)
239
+
240
+
241
+ @asynccontextmanager
242
+ async def read_stream_exception_filer(
243
+ original_read: MemoryObjectReceiveStream[JSONRPCMessage | Exception],
244
+ exception_handler: Callable[[Exception], None] | None = None,
245
+ ) -> AsyncGenerator[MemoryObjectReceiveStream[JSONRPCMessage], None]:
246
+ """
247
+ Handle exceptions in the original stream.
248
+ Default handler is to ignore read stream exception for invalid JSON lines and log them.
249
+
250
+ Args:
251
+ original_read: The original stream to read from.
252
+ exception_handler: An optional handler for exception.
253
+
254
+ Yields:
255
+ A filtered stream with exceptions removed
256
+ """
257
+ read_stream_writer, filtered_read = create_memory_object_stream(0)
258
+
259
+ async def task() -> None:
260
+ try:
261
+ async with original_read:
262
+ async for msg in original_read:
263
+ if isinstance(msg, Exception):
264
+ if exception_handler:
265
+ exception_handler(msg)
266
+ continue
267
+ await read_stream_writer.send(msg)
268
+ finally:
269
+ await read_stream_writer.aclose()
270
+
271
+ async with create_task_group() as tg:
272
+ tg.start_soon(task)
273
+ try:
274
+ yield filtered_read
275
+ finally:
276
+ await filtered_read.aclose()
277
+ tg.cancel_scope.cancel()
mirascope/mcp/client.py CHANGED
@@ -1,305 +1,151 @@
1
- import base64
2
- import string
3
- from collections.abc import AsyncGenerator, Awaitable, Callable
4
- from contextlib import asynccontextmanager
5
- from typing import Any, TypeVar
6
-
7
- from anyio import create_memory_object_stream, create_task_group
8
- from anyio.streams.memory import MemoryObjectReceiveStream
1
+ """The `MCPServer` Class and context managers."""
2
+
3
+ import contextlib
4
+ from collections.abc import AsyncIterator, Awaitable, Callable
5
+ from datetime import timedelta
6
+ from typing import Any
7
+
9
8
  from mcp import ClientSession
10
- from mcp.client.stdio import StdioServerParameters, stdio_client
9
+ from mcp.client.session import ListRootsFnT, SamplingFnT
10
+ from mcp.client.sse import sse_client as mcp_sse_client
11
+ from mcp.client.stdio import StdioServerParameters
12
+ from mcp.client.stdio import stdio_client as mcp_stdio_client
11
13
  from mcp.types import (
12
14
  BlobResourceContents,
13
- CallToolResult,
14
- EmbeddedResource,
15
- ImageContent,
16
- JSONRPCMessage,
17
- Prompt,
18
- PromptMessage,
19
15
  Resource,
20
- TextContent,
21
- TextResourceContents,
22
- Tool,
23
16
  )
24
- from pydantic import AnyUrl, BaseModel, ConfigDict, Field, create_model
25
-
26
- from mirascope.core import BaseMessageParam, BaseTool
27
- from mirascope.core.base import AudioPart, DocumentPart, ImagePart
28
-
29
- _BaseToolT = TypeVar("_BaseToolT", bound=BaseTool)
30
-
31
-
32
- def _create_tool_call(
33
- name: str,
34
- call_tool: Callable[[str, dict | None], Awaitable[CallToolResult]],
35
- ) -> Callable[..., Awaitable[list[str | ImageContent | EmbeddedResource]]]:
36
- async def call(self: BaseTool) -> list[str | ImageContent | EmbeddedResource]:
37
- result = await call_tool(name, self.args) # pyright: ignore [reportOptionalCall]
38
- if result.isError:
39
- raise RuntimeError(f"MCP Server returned error: {self._name()}")
40
- parsed_results = []
41
- for content in result.content:
42
- if isinstance(content, TextContent):
43
- parsed_results.append(content.text)
44
- else:
45
- parsed_results.append(content)
46
- return [
47
- content.text if isinstance(content, TextContent) else content
48
- for content in result.content
49
- ]
17
+ from pydantic import AnyUrl
50
18
 
51
- return call
52
-
53
-
54
- def _convert_prompt_message_to_base_message_params(
55
- prompt_message: PromptMessage,
56
- ) -> BaseMessageParam:
57
- """
58
- Convert a single PromptMessage back into a BaseMessageParam.
19
+ from mirascope.core import BaseMessageParam, BaseTool, TextPart
20
+ from mirascope.mcp import _utils
59
21
 
60
- Args:
61
- prompt_message: A PromptMessage instance.
62
22
 
63
- Returns:
64
- A BaseMessageParam instance representing the given prompt_message.
23
+ class MCPClient(ClientSession):
24
+ """The SSE client session that connects to the MCP server.
65
25
 
66
- Raises:
67
- ValueError: If the role is invalid or if the content type is unsupported.
26
+ All of the results from the server are converted into Mirascope-friendly types.
68
27
  """
69
28
 
70
- # Validate role
71
- if prompt_message.role not in ["user", "assistant"]:
72
- raise ValueError(f"invalid role: {prompt_message.role}")
73
-
74
- content = prompt_message.content
75
-
76
- # Handle TextContent
77
- if isinstance(content, TextContent):
78
- # If it's text, we can just return a single string
79
- return BaseMessageParam(role=prompt_message.role, content=content.text)
29
+ _session: ClientSession
80
30
 
81
- # Handle ImageContent
82
- elif isinstance(content, ImageContent):
83
- decoded_image = base64.b64decode(content.data)
84
-
85
- image_part = ImagePart(
86
- type="image",
87
- image=decoded_image,
88
- media_type=content.mimeType,
89
- detail=None, # detail not provided by PromptMessage
90
- )
91
- return BaseMessageParam(role=prompt_message.role, content=[image_part])
92
-
93
- elif isinstance(content, EmbeddedResource):
94
- resource = content.resource
95
- if isinstance(resource, TextResourceContents):
96
- return BaseMessageParam(role=prompt_message.role, content=resource.text)
97
- else:
98
- mime_type = resource.mimeType
99
- if not mime_type:
100
- raise ValueError(
101
- "BlobResourceContents has no mimeType, cannot determine content type."
102
- )
103
-
104
- decoded_data = base64.b64decode(resource.blob)
105
-
106
- if mime_type.startswith("image/"):
107
- # Treat as ImagePart
108
- image_part = ImagePart(
109
- type="image",
110
- image=decoded_data,
111
- media_type=mime_type,
112
- detail=None,
113
- )
114
- return BaseMessageParam(role=prompt_message.role, content=[image_part])
115
- elif mime_type == "application/pdf":
116
- doc_part = DocumentPart(
117
- type="document", media_type=mime_type, document=decoded_data
118
- )
119
- return BaseMessageParam(role=prompt_message.role, content=[doc_part])
120
- elif mime_type.startswith("audio/"):
121
- # Treat as AudioPart
122
- audio_part = AudioPart(
123
- type="audio", media_type=mime_type, audio=decoded_data
124
- )
125
- return BaseMessageParam(role=prompt_message.role, content=[audio_part])
126
- else:
127
- raise ValueError(f"Unsupported mime type: {mime_type}")
128
-
129
- else:
130
- raise ValueError(f"Unsupported content type: {type(content)}")
131
-
132
-
133
- def snake_to_pascal(snake: str) -> str:
134
- return string.capwords(snake.replace("_", " ")).replace(" ", "")
135
-
136
-
137
- def build_object_type(
138
- properties: dict[str, Any], required: list[str], name: str
139
- ) -> type[BaseModel]:
140
- fields = {}
141
- for prop_name, prop_schema in properties.items():
142
- class_name = snake_to_pascal(prop_name)
143
- type_ = json_schema_to_python_type(prop_schema, class_name)
144
- if prop_name in required:
145
- fields[prop_name] = (
146
- type_,
147
- Field(..., description=prop_schema.get("description")),
148
- )
149
- else:
150
- fields[prop_name] = (
151
- type_ | None,
152
- Field(None, description=prop_schema.get("description")),
153
- )
154
-
155
- return create_model(name, __config__=ConfigDict(extra="allow"), **fields)
156
-
157
-
158
- def json_schema_to_python_type(schema: dict[str, Any], name: str = "Model") -> Any: # noqa: ANN401
159
- """
160
- Recursively convert a JSON Schema snippet into a Python type annotation or a dynamically generated pydantic model.
161
- """
162
- json_type = schema.get("type", "any")
163
-
164
- if json_type == "string":
165
- return str
166
- elif json_type == "number":
167
- return float
168
- elif json_type == "integer":
169
- return int
170
- elif json_type == "boolean":
171
- return bool
172
- elif json_type == "array":
173
- # Recursively determine the items type for arrays
174
- items_schema = schema.get("items", {})
175
- items_type = json_schema_to_python_type(items_schema)
176
- return list[items_type]
177
- elif json_type == "object":
178
- # Recursively build a dynamic model for objects
179
- properties = schema.get("properties", {})
180
- required = schema.get("required", [])
181
- return build_object_type(properties, required, name)
182
- else:
183
- # Default fallback
184
- return Any
185
-
186
-
187
- def create_model_from_tool(tool: Tool) -> type[BaseModel]:
188
- """
189
- Create a dynamic pydantic model from the given Tool instance based on its inputSchema.
190
- """
191
- schema = tool.inputSchema
192
- properties = schema.get("properties", {})
193
- required_fields = schema.get("required", [])
194
-
195
- fields = {}
196
- for field_name, field_schema in properties.items():
197
- field_type = json_schema_to_python_type(field_schema)
198
-
199
- if field_name in required_fields:
200
- default = Field(..., description=field_schema.get("description"))
201
- annotation = field_type
202
- else:
203
- default = Field(None, description=field_schema.get("description"))
204
- annotation = field_type | None
205
-
206
- fields[field_name] = (annotation, default)
207
-
208
- return create_model(
209
- snake_to_pascal(tool.name), __config__=ConfigDict(extra="allow"), **fields
210
- )
31
+ def __init__(self, session: ClientSession) -> None:
32
+ """Initializes an instance of `MCPClient`.
211
33
 
34
+ Args:
35
+ session: The original MCP `ClientSession`.
36
+ """
37
+ self._session = session
212
38
 
213
- class MCPClient:
214
- def __init__(self, session: ClientSession) -> None:
215
- self.session: ClientSession = session
39
+ async def list_resources(self) -> list[Resource]: # pyright: ignore [reportIncompatibleMethodOverride]
40
+ """List all resources available on the MCP server.
216
41
 
217
- async def list_resources(self) -> list[Resource]:
218
- result = await self.session.list_resources()
42
+ Returns:
43
+ A list of Resource objects
44
+ """
45
+ result = await self._session.list_resources()
219
46
  return result.resources
220
47
 
221
- async def read_resource(
48
+ async def read_resource( # pyright: ignore [reportIncompatibleMethodOverride]
222
49
  self, uri: str | AnyUrl
223
- ) -> list[str | BlobResourceContents]:
224
- result = await self.session.read_resource(
50
+ ) -> list[TextPart | BlobResourceContents]:
51
+ """Read a resource from the MCP server.
52
+
53
+ Args:
54
+ uri: URI of the resource to read
55
+
56
+ Returns:
57
+ Contents of the resource, either as string or BlobResourceContents
58
+ """
59
+ result = await self._session.read_resource(
225
60
  uri if isinstance(uri, AnyUrl) else AnyUrl(uri)
226
61
  )
227
- parsed_results = []
62
+ parsed_results: list[TextPart | BlobResourceContents] = []
228
63
  for content in result.contents:
229
- if isinstance(content, TextResourceContents):
230
- parsed_results.append(content.text)
231
- else:
64
+ if isinstance(content, BlobResourceContents):
232
65
  parsed_results.append(content)
66
+ else:
67
+ parsed_results.append(TextPart(type="text", text=content.text))
233
68
  return parsed_results
234
69
 
235
- async def list_prompts(self) -> list[Prompt]:
236
- result = await self.session.list_prompts()
70
+ async def list_prompts(self) -> list[Any]: # pyright: ignore [reportIncompatibleMethodOverride]
71
+ """List all prompts available on the MCP server.
72
+
73
+ Returns:
74
+ A list of Prompt objects
75
+ """
76
+ result = await self._session.list_prompts()
237
77
  return result.prompts
238
78
 
239
79
  async def get_prompt_template(
240
80
  self, name: str
241
81
  ) -> Callable[..., Awaitable[list[BaseMessageParam]]]:
82
+ """Get a prompt template from the MCP server.
83
+
84
+ Args:
85
+ name: Name of the prompt template
86
+
87
+ Returns:
88
+ A callable that accepts keyword arguments and returns a list of BaseMessageParam
89
+ """
90
+
91
+ # TODO: wrap this with `llm.prompt` once it's implemented so that they prompts
92
+ # can be run super easily inside of an `llm.context` block.
242
93
  async def async_prompt(**kwargs: str) -> list[BaseMessageParam]:
243
- result = await self.session.get_prompt(name, kwargs)
94
+ result = await self._session.get_prompt(name, kwargs) # type: ignore
244
95
 
245
96
  return [
246
- _convert_prompt_message_to_base_message_params(prompt_message)
97
+ _utils.convert_prompt_message_to_base_message_params(prompt_message)
247
98
  for prompt_message in result.messages
248
99
  ]
249
100
 
250
101
  return async_prompt
251
102
 
252
- async def list_tools(self) -> list[type[BaseModel]]:
253
- list_tool_result = await self.session.list_tools()
103
+ async def list_tools(self) -> list[type[BaseTool]]: # pyright: ignore [reportIncompatibleMethodOverride]
104
+ """List all tools available on the MCP server.
105
+
106
+ Returns:
107
+ A list of dynamically created `BaseTool` types.
108
+ """
109
+ list_tool_result = await self._session.list_tools()
254
110
 
255
111
  converted_tools = []
256
112
  for tool in list_tool_result.tools:
257
- model = create_model_from_tool(tool)
258
- model.call = _create_tool_call(tool.name, self.session.call_tool) # pyright: ignore [reportAttributeAccessIssue]
113
+ model = _utils.create_tool_from_mcp_tool(tool)
114
+ tool_call_method = _utils.create_tool_call(
115
+ tool.name, self._session.call_tool
116
+ )
117
+ model.call = tool_call_method # pyright: ignore [reportAttributeAccessIssue]
259
118
  converted_tools.append(model)
260
119
  return converted_tools
261
120
 
262
121
 
263
- @asynccontextmanager
264
- async def read_stream_exception_filer(
265
- original_read: MemoryObjectReceiveStream[JSONRPCMessage | Exception],
266
- exception_handler: Callable[[Exception], None] | None = None,
267
- ) -> AsyncGenerator[MemoryObjectReceiveStream[JSONRPCMessage], None]:
268
- """
269
- Handle exceptions in the original stream.
270
- default handler is to ignore read stream exception for invalid JSON lines and log them.
271
- Args:
272
- original_read: The original stream to read from.
273
- exception_handler: An optional handler for exception.
274
- """
275
- read_stream_writer, filtered_read = create_memory_object_stream(0)
276
-
277
- async def task() -> None:
278
- try:
279
- async with original_read:
280
- async for msg in original_read:
281
- if isinstance(msg, Exception):
282
- if exception_handler:
283
- exception_handler(msg)
284
- continue
285
- await read_stream_writer.send(msg)
286
- finally:
287
- await read_stream_writer.aclose()
288
-
289
- async with create_task_group() as tg:
290
- tg.start_soon(task)
291
- try:
292
- yield filtered_read
293
- finally:
294
- await filtered_read.aclose()
295
- tg.cancel_scope.cancel()
296
-
297
-
298
- @asynccontextmanager
299
- async def create_mcp_client(
122
+ @contextlib.asynccontextmanager
123
+ async def sse_client(
124
+ url: str,
125
+ list_roots_callback: ListRootsFnT | None = None,
126
+ read_timeout_seconds: timedelta | None = None,
127
+ sampling_callback: SamplingFnT | None = None,
128
+ session: ClientSession | None = None,
129
+ ) -> AsyncIterator[MCPClient]:
130
+ async with (
131
+ mcp_sse_client(url) as (read, write),
132
+ ClientSession(
133
+ read,
134
+ write,
135
+ read_timeout_seconds=read_timeout_seconds,
136
+ sampling_callback=sampling_callback,
137
+ list_roots_callback=list_roots_callback,
138
+ ) as session,
139
+ ):
140
+ await session.initialize()
141
+ yield MCPClient(session)
142
+
143
+
144
+ @contextlib.asynccontextmanager
145
+ async def stdio_client(
300
146
  server_parameters: StdioServerParameters,
301
147
  read_stream_exception_handler: Callable[[Exception], None] | None = None,
302
- ) -> AsyncGenerator[MCPClient, None]:
148
+ ) -> AsyncIterator[MCPClient]:
303
149
  """
304
150
  Create a MCPClient instance with the given server parameters and exception handler.
305
151
 
@@ -311,11 +157,11 @@ async def create_mcp_client(
311
157
 
312
158
  """
313
159
  async with (
314
- stdio_client(server_parameters) as (read, write),
315
- read_stream_exception_filer(
160
+ mcp_stdio_client(server_parameters) as (read, write),
161
+ _utils.read_stream_exception_filer(
316
162
  read, read_stream_exception_handler
317
163
  ) as filtered_read,
318
- ClientSession(filtered_read, write) as session,
164
+ ClientSession(filtered_read, write) as session, # pyright: ignore [reportArgumentType]
319
165
  ):
320
166
  await session.initialize()
321
167
  yield MCPClient(session)
mirascope/mcp/server.py CHANGED
@@ -1,6 +1,7 @@
1
- """MCP server implementation."""
1
+ """The `MCPServer` Class and context managers."""
2
2
 
3
3
  import inspect
4
+ import warnings
4
5
  from collections.abc import Awaitable, Callable, Iterable
5
6
  from typing import Literal, ParamSpec, cast, overload
6
7
 
@@ -117,7 +118,12 @@ def _generate_prompt_from_function(fn: Callable) -> Prompt:
117
118
 
118
119
 
119
120
  class MCPServer:
120
- """MCP server implementation."""
121
+ """MCP server implementation.
122
+
123
+ DEPRECATED: The MCPServer implementation is deprecated and will be removed in a future version.
124
+ Mirascope will only implement the client-side of MCP in the future, allowing it to connect to any
125
+ MCP server implementation, even if not built with Mirascope.
126
+ """
121
127
 
122
128
  def __init__(
123
129
  self,
@@ -134,6 +140,13 @@ class MCPServer:
134
140
  ]
135
141
  | None = None,
136
142
  ) -> None:
143
+ warnings.warn(
144
+ "MCPServer is deprecated and will be removed in a future version. "
145
+ "Mirascope will only implement the client-side of MCP in the future. "
146
+ "We recommend using the official MCP SDK (e.g. `FastMCP`) for server-side implementations.",
147
+ DeprecationWarning,
148
+ stacklevel=2,
149
+ )
137
150
  self.name: str = name
138
151
  self.version: str = version
139
152
  self.server: Server = Server(name)
mirascope/mcp/tools.py CHANGED
@@ -1,10 +1,15 @@
1
1
  """The `MCPTool` class for easy tool usage with MCPTool LLM calls.
2
2
 
3
3
  usage docs: learn/tools.md
4
+
5
+ DEPRECATED: The MCPTool class is deprecated and will be removed in a future version.
6
+ Mirascope will only implement the client-side of MCP in the future, allowing it to connect to any
7
+ MCP server implementation, even if not built with Mirascope.
4
8
  """
5
9
 
6
10
  from __future__ import annotations
7
11
 
12
+ import warnings
8
13
  from typing import Any
9
14
 
10
15
  from pydantic import BaseModel
@@ -34,6 +39,9 @@ class MCPToolToolConfig(ToolConfig, total=False):
34
39
  class MCPTool(BaseTool):
35
40
  """A class for defining tools for MCP LLM calls.
36
41
 
42
+ DEPRECATED: The MCPTool class is deprecated and will be removed in a future version.
43
+ Mirascope will only implement the client-side of MCP in the future.
44
+
37
45
  Example:
38
46
 
39
47
  ```python
@@ -74,6 +82,14 @@ class MCPTool(BaseTool):
74
82
  print(tool_type.tool_schema()) # prints the MCP-specific tool schema
75
83
  ```
76
84
  """
85
+ # Show deprecation warning
86
+ warnings.warn(
87
+ "The MCPTool class is deprecated and will be removed in a future version. "
88
+ "Mirascope will only implement the client-side of MCP in the future. "
89
+ "We recommend using the official MCP SDK (e.g. `FastMCP`) for server-side implementations.",
90
+ DeprecationWarning,
91
+ stacklevel=2,
92
+ )
77
93
  kwargs = {
78
94
  "input_schema": cls.model_json_schema(),
79
95
  "name": cls._name(),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mirascope
3
- Version: 1.21.6
3
+ Version: 1.22.0
4
4
  Summary: LLM abstractions that aren't obstructions
5
5
  Project-URL: Homepage, https://mirascope.com
6
6
  Project-URL: Documentation, https://mirascope.com/WELCOME
@@ -79,7 +79,7 @@ Requires-Dist: litellm<2,>=1.42.12; extra == 'litellm'
79
79
  Provides-Extra: logfire
80
80
  Requires-Dist: logfire<4,>=1.0.0; extra == 'logfire'
81
81
  Provides-Extra: mcp
82
- Requires-Dist: mcp>=1.0.0; extra == 'mcp'
82
+ Requires-Dist: mcp>=1.3.0; extra == 'mcp'
83
83
  Provides-Extra: mistral
84
84
  Requires-Dist: mistralai<2,>=1.0.0; extra == 'mistral'
85
85
  Provides-Extra: openai
@@ -333,7 +333,7 @@ mirascope/integrations/otel/__init__.py,sha256=OzboYfm3fUNwKTuu08KX83hQHYI4oZYN2
333
333
  mirascope/integrations/otel/_utils.py,sha256=SCVb3MpcpqLpCpumJEbEdINceNdusnyT6iuKPz66sBc,8778
334
334
  mirascope/integrations/otel/_with_hyperdx.py,sha256=f17uxXQk5zZPtyj6zwPwJz5i7atsnUPOoq1LqT8JO0E,1637
335
335
  mirascope/integrations/otel/_with_otel.py,sha256=tbjd6BEbcSfnsm5CWHBoHwbRNrHt6-t4or-SYGQSD-w,1659
336
- mirascope/llm/__init__.py,sha256=zo7QD39zQ6HIU_N2yHrzd6bbcT9YmlikZ84JN_r5k4E,397
336
+ mirascope/llm/__init__.py,sha256=rCukXJUqSGiDGNMMM9g3NS8clnrCxgCpaFIXIG1M660,432
337
337
  mirascope/llm/_call.py,sha256=zPULSy5GLlJszphRvc3WUSFEsmP83Abp0FzbZefTa8Y,13848
338
338
  mirascope/llm/_context.py,sha256=vtHJkLlFfUwyR_hYEHXAw3xunpHhLn67k4kuFw50GR8,12481
339
339
  mirascope/llm/_override.py,sha256=m4MdOhM-aJRIGP7NBJhscq3ISNct6FBPn3jjmryFo_Q,112292
@@ -343,10 +343,11 @@ mirascope/llm/call_response.py,sha256=4w_2Dgt325lEjMaFlg7Iq2AGp8cI0uSaqCa8MR_S5G
343
343
  mirascope/llm/call_response_chunk.py,sha256=bZwO43ipc6PO1VLgGSaAPRqCIUyZD_Ty5oxdJX62yno,1966
344
344
  mirascope/llm/stream.py,sha256=GtUKyLBlKqqZTOKjdL9FLInCXJ0ZOEAe6nymbjKwyTQ,5293
345
345
  mirascope/llm/tool.py,sha256=MQRJBPhP1d-OyOz3PE_VsKmSXca0chySyYO1U9OW8ck,1824
346
- mirascope/mcp/__init__.py,sha256=mGboroTrBbzuZ_8uBssOhkqiJOJ4mNCvaJvS7mhumhg,155
347
- mirascope/mcp/client.py,sha256=ZsjaT6E5i4Cdm3iVp2-JTuDniUlzynWeeJvnBAtuffQ,11123
348
- mirascope/mcp/server.py,sha256=7BjZO3DkaiokEOgJOY9fbEX3M6YwAExN6Kw9s-Dp7Sc,11737
349
- mirascope/mcp/tools.py,sha256=IKQZCiQHh_YxJM-V90U9Z6xPQTYIfWFDBj8khhHDiw4,2254
346
+ mirascope/mcp/__init__.py,sha256=1tvG-fRD-_mr2vF3M49OgTLR7Vw8zdT6LucxnY4prF8,282
347
+ mirascope/mcp/_utils.py,sha256=jyuE28W9Gy6sF17dRNZnqXRBhD_dmJBxIXq99UWmpHk,9221
348
+ mirascope/mcp/client.py,sha256=ZK88o4UelR9htFWAWDbw084ZTrRL8tONdxlIX62adnM,5608
349
+ mirascope/mcp/server.py,sha256=weXBk9ZmUcfZ5SZcR-dGRxvVXRMpn5UsE6ancnK1HZk,12402
350
+ mirascope/mcp/tools.py,sha256=IT7CEqbBBiFc7mkaRpWIepUpuy_oSdcEa4cwGopEYWc,3079
350
351
  mirascope/retries/__init__.py,sha256=xw3jJm-vL4kR10VaKN6A8KGoP2CsAg5_Gy1eWVMgYhQ,249
351
352
  mirascope/retries/fallback.py,sha256=LPSyIfPAHajt3-M_Z1M4ZSbgHjbXghMBbb9fjNQ27UU,4876
352
353
  mirascope/retries/tenacity.py,sha256=stBJPjEpUzP53IBVBFtqY2fUSgmOV1-sIIXZZJ9pvLY,1387
@@ -370,7 +371,7 @@ mirascope/v0/base/ops_utils.py,sha256=1Qq-VIwgHBaYutiZsS2MUQ4OgPC3APyywI5bTiTAmA
370
371
  mirascope/v0/base/prompts.py,sha256=FM2Yz98cSnDceYogiwPrp4BALf3_F3d4fIOCGAkd-SE,1298
371
372
  mirascope/v0/base/types.py,sha256=ZfatJoX0Yl0e3jhv0D_MhiSVHLYUeJsdN3um3iE10zY,352
372
373
  mirascope/v0/base/utils.py,sha256=XREPENRQTu8gpMhHU8RC8qH_am3FfGUvY-dJ6x8i-mw,681
373
- mirascope-1.21.6.dist-info/METADATA,sha256=G019BNHoF6aiD4C12Dqv77qn3vJhI2Zi-KWJTfwc0L4,8730
374
- mirascope-1.21.6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
375
- mirascope-1.21.6.dist-info/licenses/LICENSE,sha256=LAs5Q8mdawTsVdONpDGukwsoc4KEUBmmonDEL39b23Y,1072
376
- mirascope-1.21.6.dist-info/RECORD,,
374
+ mirascope-1.22.0.dist-info/METADATA,sha256=2Tod_tF9thr1KIVCfKCVTGfgtBvd9NLgOIdIo5YMvlQ,8730
375
+ mirascope-1.22.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
376
+ mirascope-1.22.0.dist-info/licenses/LICENSE,sha256=LAs5Q8mdawTsVdONpDGukwsoc4KEUBmmonDEL39b23Y,1072
377
+ mirascope-1.22.0.dist-info/RECORD,,