mirascope 1.21.6__py3-none-any.whl → 1.22.1__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 +2 -0
- mirascope/mcp/__init__.py +4 -3
- mirascope/mcp/_utils.py +277 -0
- mirascope/mcp/client.py +105 -259
- mirascope/mcp/server.py +15 -2
- mirascope/mcp/tools.py +16 -0
- {mirascope-1.21.6.dist-info → mirascope-1.22.1.dist-info}/METADATA +3 -2
- {mirascope-1.21.6.dist-info → mirascope-1.22.1.dist-info}/RECORD +10 -9
- {mirascope-1.21.6.dist-info → mirascope-1.22.1.dist-info}/WHEEL +0 -0
- {mirascope-1.21.6.dist-info → mirascope-1.22.1.dist-info}/licenses/LICENSE +0 -0
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 .
|
|
4
|
-
from .
|
|
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"]
|
mirascope/mcp/_utils.py
ADDED
|
@@ -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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
from
|
|
5
|
-
from
|
|
6
|
-
|
|
7
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
64
|
-
|
|
23
|
+
class MCPClient(ClientSession):
|
|
24
|
+
"""The SSE client session that connects to the MCP server.
|
|
65
25
|
|
|
66
|
-
|
|
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
|
-
|
|
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
|
-
|
|
82
|
-
|
|
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
|
-
|
|
214
|
-
|
|
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
|
-
|
|
218
|
-
|
|
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[
|
|
224
|
-
|
|
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,
|
|
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[
|
|
236
|
-
|
|
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.
|
|
94
|
+
result = await self._session.get_prompt(name, kwargs) # type: ignore
|
|
244
95
|
|
|
245
96
|
return [
|
|
246
|
-
|
|
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[
|
|
253
|
-
|
|
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 =
|
|
258
|
-
|
|
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
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
) ->
|
|
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
|
-
|
|
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
|
-
"""
|
|
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.
|
|
3
|
+
Version: 1.22.1
|
|
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
|
|
@@ -68,6 +68,7 @@ Requires-Dist: pillow<11,>=10.4.0; extra == 'gemini'
|
|
|
68
68
|
Provides-Extra: google
|
|
69
69
|
Requires-Dist: google-genai<2,>=1.2.0; extra == 'google'
|
|
70
70
|
Requires-Dist: pillow<11,>=10.4.0; extra == 'google'
|
|
71
|
+
Requires-Dist: proto-plus>=1.24.0; extra == 'google'
|
|
71
72
|
Provides-Extra: groq
|
|
72
73
|
Requires-Dist: groq<1,>=0.9.0; extra == 'groq'
|
|
73
74
|
Provides-Extra: hyperdx
|
|
@@ -79,7 +80,7 @@ Requires-Dist: litellm<2,>=1.42.12; extra == 'litellm'
|
|
|
79
80
|
Provides-Extra: logfire
|
|
80
81
|
Requires-Dist: logfire<4,>=1.0.0; extra == 'logfire'
|
|
81
82
|
Provides-Extra: mcp
|
|
82
|
-
Requires-Dist: mcp>=1.
|
|
83
|
+
Requires-Dist: mcp>=1.3.0; extra == 'mcp'
|
|
83
84
|
Provides-Extra: mistral
|
|
84
85
|
Requires-Dist: mistralai<2,>=1.0.0; extra == 'mistral'
|
|
85
86
|
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=
|
|
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=
|
|
347
|
-
mirascope/mcp/
|
|
348
|
-
mirascope/mcp/
|
|
349
|
-
mirascope/mcp/
|
|
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.
|
|
374
|
-
mirascope-1.
|
|
375
|
-
mirascope-1.
|
|
376
|
-
mirascope-1.
|
|
374
|
+
mirascope-1.22.1.dist-info/METADATA,sha256=YoitXID5GPP9Aj5JURQaBvSk07JHTnbA6s0PuDv37C8,8783
|
|
375
|
+
mirascope-1.22.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
376
|
+
mirascope-1.22.1.dist-info/licenses/LICENSE,sha256=LAs5Q8mdawTsVdONpDGukwsoc4KEUBmmonDEL39b23Y,1072
|
|
377
|
+
mirascope-1.22.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|