fastmcp 2.12.5__py3-none-any.whl → 2.13.2__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.
- fastmcp/__init__.py +2 -2
- fastmcp/cli/cli.py +11 -11
- fastmcp/cli/install/claude_code.py +6 -6
- fastmcp/cli/install/claude_desktop.py +3 -3
- fastmcp/cli/install/cursor.py +18 -12
- fastmcp/cli/install/gemini_cli.py +3 -3
- fastmcp/cli/install/mcp_json.py +3 -3
- fastmcp/cli/run.py +13 -8
- fastmcp/client/__init__.py +9 -9
- fastmcp/client/auth/oauth.py +115 -217
- fastmcp/client/client.py +105 -39
- fastmcp/client/logging.py +18 -14
- fastmcp/client/oauth_callback.py +85 -171
- fastmcp/client/sampling.py +1 -1
- fastmcp/client/transports.py +80 -25
- fastmcp/contrib/component_manager/__init__.py +1 -1
- fastmcp/contrib/component_manager/component_manager.py +2 -2
- fastmcp/contrib/component_manager/component_service.py +6 -6
- fastmcp/contrib/mcp_mixin/README.md +32 -1
- fastmcp/contrib/mcp_mixin/__init__.py +2 -2
- fastmcp/contrib/mcp_mixin/mcp_mixin.py +14 -2
- fastmcp/experimental/sampling/handlers/openai.py +2 -2
- fastmcp/experimental/server/openapi/__init__.py +5 -8
- fastmcp/experimental/server/openapi/components.py +11 -7
- fastmcp/experimental/server/openapi/routing.py +2 -2
- fastmcp/experimental/utilities/openapi/__init__.py +10 -15
- fastmcp/experimental/utilities/openapi/director.py +14 -15
- fastmcp/experimental/utilities/openapi/json_schema_converter.py +6 -2
- fastmcp/experimental/utilities/openapi/models.py +3 -3
- fastmcp/experimental/utilities/openapi/parser.py +37 -16
- fastmcp/experimental/utilities/openapi/schemas.py +2 -2
- fastmcp/mcp_config.py +3 -4
- fastmcp/prompts/__init__.py +1 -1
- fastmcp/prompts/prompt.py +22 -19
- fastmcp/prompts/prompt_manager.py +16 -101
- fastmcp/resources/__init__.py +5 -5
- fastmcp/resources/resource.py +14 -9
- fastmcp/resources/resource_manager.py +9 -168
- fastmcp/resources/template.py +107 -17
- fastmcp/resources/types.py +30 -24
- fastmcp/server/__init__.py +1 -1
- fastmcp/server/auth/__init__.py +9 -5
- fastmcp/server/auth/auth.py +70 -43
- fastmcp/server/auth/handlers/authorize.py +326 -0
- fastmcp/server/auth/jwt_issuer.py +236 -0
- fastmcp/server/auth/middleware.py +96 -0
- fastmcp/server/auth/oauth_proxy.py +1510 -289
- fastmcp/server/auth/oidc_proxy.py +84 -20
- fastmcp/server/auth/providers/auth0.py +40 -21
- fastmcp/server/auth/providers/aws.py +29 -3
- fastmcp/server/auth/providers/azure.py +312 -131
- fastmcp/server/auth/providers/bearer.py +1 -1
- fastmcp/server/auth/providers/debug.py +114 -0
- fastmcp/server/auth/providers/descope.py +86 -29
- fastmcp/server/auth/providers/discord.py +308 -0
- fastmcp/server/auth/providers/github.py +29 -8
- fastmcp/server/auth/providers/google.py +48 -9
- fastmcp/server/auth/providers/in_memory.py +27 -3
- fastmcp/server/auth/providers/introspection.py +281 -0
- fastmcp/server/auth/providers/jwt.py +48 -31
- fastmcp/server/auth/providers/oci.py +233 -0
- fastmcp/server/auth/providers/scalekit.py +238 -0
- fastmcp/server/auth/providers/supabase.py +188 -0
- fastmcp/server/auth/providers/workos.py +35 -17
- fastmcp/server/context.py +177 -51
- fastmcp/server/dependencies.py +39 -12
- fastmcp/server/elicitation.py +1 -1
- fastmcp/server/http.py +56 -17
- fastmcp/server/low_level.py +121 -2
- fastmcp/server/middleware/__init__.py +1 -1
- fastmcp/server/middleware/caching.py +476 -0
- fastmcp/server/middleware/error_handling.py +14 -10
- fastmcp/server/middleware/logging.py +50 -39
- fastmcp/server/middleware/middleware.py +29 -16
- fastmcp/server/middleware/rate_limiting.py +3 -3
- fastmcp/server/middleware/tool_injection.py +116 -0
- fastmcp/server/openapi.py +10 -6
- fastmcp/server/proxy.py +22 -11
- fastmcp/server/server.py +725 -242
- fastmcp/settings.py +24 -10
- fastmcp/tools/__init__.py +1 -1
- fastmcp/tools/tool.py +70 -23
- fastmcp/tools/tool_manager.py +30 -112
- fastmcp/tools/tool_transform.py +12 -10
- fastmcp/utilities/cli.py +67 -28
- fastmcp/utilities/components.py +7 -2
- fastmcp/utilities/inspect.py +79 -23
- fastmcp/utilities/json_schema.py +4 -4
- fastmcp/utilities/json_schema_type.py +4 -4
- fastmcp/utilities/logging.py +118 -8
- fastmcp/utilities/mcp_server_config/__init__.py +3 -3
- fastmcp/utilities/mcp_server_config/v1/environments/base.py +1 -2
- fastmcp/utilities/mcp_server_config/v1/environments/uv.py +6 -6
- fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +4 -4
- fastmcp/utilities/mcp_server_config/v1/schema.json +3 -0
- fastmcp/utilities/mcp_server_config/v1/sources/base.py +0 -1
- fastmcp/utilities/openapi.py +11 -11
- fastmcp/utilities/tests.py +85 -4
- fastmcp/utilities/types.py +78 -16
- fastmcp/utilities/ui.py +626 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.13.2.dist-info}/METADATA +22 -14
- fastmcp-2.13.2.dist-info/RECORD +144 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.13.2.dist-info}/WHEEL +1 -1
- fastmcp/cli/claude.py +0 -135
- fastmcp/utilities/storage.py +0 -204
- fastmcp-2.12.5.dist-info/RECORD +0 -134
- {fastmcp-2.12.5.dist-info → fastmcp-2.13.2.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.13.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -15,11 +15,11 @@ from fastmcp.utilities.mcp_server_config.v1.sources.base import Source
|
|
|
15
15
|
from fastmcp.utilities.mcp_server_config.v1.sources.filesystem import FileSystemSource
|
|
16
16
|
|
|
17
17
|
__all__ = [
|
|
18
|
-
"Source",
|
|
19
18
|
"Deployment",
|
|
20
19
|
"Environment",
|
|
21
|
-
"UVEnvironment",
|
|
22
|
-
"MCPServerConfig",
|
|
23
20
|
"FileSystemSource",
|
|
21
|
+
"MCPServerConfig",
|
|
22
|
+
"Source",
|
|
23
|
+
"UVEnvironment",
|
|
24
24
|
"generate_schema",
|
|
25
25
|
]
|
|
@@ -19,7 +19,6 @@ class Environment(BaseModel, ABC):
|
|
|
19
19
|
Returns:
|
|
20
20
|
Full command ready for subprocess execution
|
|
21
21
|
"""
|
|
22
|
-
pass
|
|
23
22
|
|
|
24
23
|
async def prepare(self, output_dir: Path | None = None) -> None:
|
|
25
24
|
"""Prepare the environment (optional, can be no-op).
|
|
@@ -27,4 +26,4 @@ class Environment(BaseModel, ABC):
|
|
|
27
26
|
Args:
|
|
28
27
|
output_dir: Directory for persistent environment setup
|
|
29
28
|
"""
|
|
30
|
-
|
|
29
|
+
# Default no-op implementation
|
|
@@ -28,19 +28,19 @@ class UVEnvironment(Environment):
|
|
|
28
28
|
examples=[["fastmcp>=2.0,<3", "httpx", "pandas>=2.0"]],
|
|
29
29
|
)
|
|
30
30
|
|
|
31
|
-
requirements:
|
|
31
|
+
requirements: Path | None = Field(
|
|
32
32
|
default=None,
|
|
33
33
|
description="Path to requirements.txt file",
|
|
34
34
|
examples=["requirements.txt", "../requirements/prod.txt"],
|
|
35
35
|
)
|
|
36
36
|
|
|
37
|
-
project:
|
|
37
|
+
project: Path | None = Field(
|
|
38
38
|
default=None,
|
|
39
39
|
description="Path to project directory containing pyproject.toml",
|
|
40
40
|
examples=[".", "../my-project"],
|
|
41
41
|
)
|
|
42
42
|
|
|
43
|
-
editable: list[
|
|
43
|
+
editable: list[Path] | None = Field(
|
|
44
44
|
default=None,
|
|
45
45
|
description="Directories to install in editable mode",
|
|
46
46
|
examples=[[".", "../my-package"], ["/path/to/package"]],
|
|
@@ -64,7 +64,7 @@ class UVEnvironment(Environment):
|
|
|
64
64
|
|
|
65
65
|
# Add project if specified
|
|
66
66
|
if self.project:
|
|
67
|
-
args.extend(["--project", str(self.project)])
|
|
67
|
+
args.extend(["--project", str(self.project.resolve())])
|
|
68
68
|
|
|
69
69
|
# Add Python version if specified (only if no project, as project has its own Python)
|
|
70
70
|
if self.python and not self.project:
|
|
@@ -78,12 +78,12 @@ class UVEnvironment(Environment):
|
|
|
78
78
|
|
|
79
79
|
# Add requirements file
|
|
80
80
|
if self.requirements:
|
|
81
|
-
args.extend(["--with-requirements", str(self.requirements)])
|
|
81
|
+
args.extend(["--with-requirements", str(self.requirements.resolve())])
|
|
82
82
|
|
|
83
83
|
# Add editable packages
|
|
84
84
|
if self.editable:
|
|
85
85
|
for editable_path in self.editable:
|
|
86
|
-
args.extend(["--with-editable", str(editable_path)])
|
|
86
|
+
args.extend(["--with-editable", str(editable_path.resolve())])
|
|
87
87
|
|
|
88
88
|
# Add the command
|
|
89
89
|
args.extend(command)
|
|
@@ -217,7 +217,7 @@ class MCPServerConfig(BaseModel):
|
|
|
217
217
|
"""
|
|
218
218
|
if isinstance(v, dict):
|
|
219
219
|
return Deployment(**v) # type: ignore[arg-type]
|
|
220
|
-
return cast(Deployment, v)
|
|
220
|
+
return cast(Deployment, v) # type: ignore[return-value]
|
|
221
221
|
|
|
222
222
|
@classmethod
|
|
223
223
|
def from_file(cls, file_path: Path) -> MCPServerConfig:
|
|
@@ -291,9 +291,9 @@ class MCPServerConfig(BaseModel):
|
|
|
291
291
|
environment = UVEnvironment(
|
|
292
292
|
python=python,
|
|
293
293
|
dependencies=dependencies,
|
|
294
|
-
requirements=requirements,
|
|
295
|
-
project=project,
|
|
296
|
-
editable=[editable] if editable else None,
|
|
294
|
+
requirements=Path(requirements) if requirements else None,
|
|
295
|
+
project=Path(project) if project else None,
|
|
296
|
+
editable=[Path(editable)] if editable else None,
|
|
297
297
|
)
|
|
298
298
|
|
|
299
299
|
# Build deployment config if any deployment args provided
|
|
@@ -250,6 +250,7 @@
|
|
|
250
250
|
"requirements": {
|
|
251
251
|
"anyOf": [
|
|
252
252
|
{
|
|
253
|
+
"format": "path",
|
|
253
254
|
"type": "string"
|
|
254
255
|
},
|
|
255
256
|
{
|
|
@@ -267,6 +268,7 @@
|
|
|
267
268
|
"project": {
|
|
268
269
|
"anyOf": [
|
|
269
270
|
{
|
|
271
|
+
"format": "path",
|
|
270
272
|
"type": "string"
|
|
271
273
|
},
|
|
272
274
|
{
|
|
@@ -285,6 +287,7 @@
|
|
|
285
287
|
"anyOf": [
|
|
286
288
|
{
|
|
287
289
|
"items": {
|
|
290
|
+
"format": "path",
|
|
288
291
|
"type": "string"
|
|
289
292
|
},
|
|
290
293
|
"type": "array"
|
fastmcp/utilities/openapi.py
CHANGED
|
@@ -175,16 +175,16 @@ class HTTPRoute(FastMCPBaseModel):
|
|
|
175
175
|
# Export public symbols
|
|
176
176
|
__all__ = [
|
|
177
177
|
"HTTPRoute",
|
|
178
|
+
"HttpMethod",
|
|
179
|
+
"JsonSchema",
|
|
178
180
|
"ParameterInfo",
|
|
181
|
+
"ParameterLocation",
|
|
179
182
|
"RequestBodyInfo",
|
|
180
183
|
"ResponseInfo",
|
|
181
|
-
"
|
|
182
|
-
"ParameterLocation",
|
|
183
|
-
"JsonSchema",
|
|
184
|
-
"parse_openapi_to_http_routes",
|
|
184
|
+
"_handle_nullable_fields",
|
|
185
185
|
"extract_output_schema_from_responses",
|
|
186
186
|
"format_deep_object_parameter",
|
|
187
|
-
"
|
|
187
|
+
"parse_openapi_to_http_routes",
|
|
188
188
|
]
|
|
189
189
|
|
|
190
190
|
# Type variables for generic parser
|
|
@@ -321,7 +321,7 @@ class OpenAPIParser(
|
|
|
321
321
|
else:
|
|
322
322
|
# Special handling for components
|
|
323
323
|
if part == "components" and hasattr(target, "components"):
|
|
324
|
-
target =
|
|
324
|
+
target = target.components
|
|
325
325
|
elif hasattr(target, part): # Fallback check
|
|
326
326
|
target = getattr(target, part, None)
|
|
327
327
|
else:
|
|
@@ -1178,10 +1178,10 @@ def _add_null_to_type(schema: dict[str, Any]) -> None:
|
|
|
1178
1178
|
elif isinstance(current_type, list):
|
|
1179
1179
|
# Add null to array if not already present
|
|
1180
1180
|
if "null" not in current_type:
|
|
1181
|
-
schema["type"] = current_type
|
|
1181
|
+
schema["type"] = [*current_type, "null"]
|
|
1182
1182
|
elif "oneOf" in schema:
|
|
1183
1183
|
# Convert oneOf to anyOf with null type
|
|
1184
|
-
schema["anyOf"] = schema.pop("oneOf")
|
|
1184
|
+
schema["anyOf"] = [*schema.pop("oneOf"), {"type": "null"}]
|
|
1185
1185
|
elif "anyOf" in schema:
|
|
1186
1186
|
# Add null type to anyOf if not already present
|
|
1187
1187
|
if not any(item.get("type") == "null" for item in schema["anyOf"]):
|
|
@@ -1233,7 +1233,7 @@ def _handle_nullable_fields(schema: dict[str, Any] | Any) -> dict[str, Any] | An
|
|
|
1233
1233
|
|
|
1234
1234
|
# Handle properties nullable fields
|
|
1235
1235
|
if has_property_nullable_field and "properties" in result:
|
|
1236
|
-
for
|
|
1236
|
+
for _prop_name, prop_schema in result["properties"].items():
|
|
1237
1237
|
if isinstance(prop_schema, dict) and "nullable" in prop_schema:
|
|
1238
1238
|
nullable_value = prop_schema.pop("nullable")
|
|
1239
1239
|
if nullable_value and (
|
|
@@ -1371,7 +1371,7 @@ def _combine_schemas(route: HTTPRoute) -> dict[str, Any]:
|
|
|
1371
1371
|
if used_refs:
|
|
1372
1372
|
result["$defs"] = {
|
|
1373
1373
|
name: def_schema
|
|
1374
|
-
for name, def_schema in result["$defs"].items()
|
|
1374
|
+
for name, def_schema in result["$defs"].items() # type: ignore[index]
|
|
1375
1375
|
if name in used_refs
|
|
1376
1376
|
}
|
|
1377
1377
|
else:
|
|
@@ -1556,7 +1556,7 @@ def extract_output_schema_from_responses(
|
|
|
1556
1556
|
if used_refs:
|
|
1557
1557
|
output_schema["$defs"] = {
|
|
1558
1558
|
name: def_schema
|
|
1559
|
-
for name, def_schema in output_schema["$defs"].items()
|
|
1559
|
+
for name, def_schema in output_schema["$defs"].items() # type: ignore[index]
|
|
1560
1560
|
if name in used_refs
|
|
1561
1561
|
}
|
|
1562
1562
|
else:
|
fastmcp/utilities/tests.py
CHANGED
|
@@ -5,8 +5,8 @@ import logging
|
|
|
5
5
|
import multiprocessing
|
|
6
6
|
import socket
|
|
7
7
|
import time
|
|
8
|
-
from collections.abc import Callable, Generator
|
|
9
|
-
from contextlib import contextmanager
|
|
8
|
+
from collections.abc import AsyncGenerator, Callable, Generator
|
|
9
|
+
from contextlib import asynccontextmanager, contextmanager, suppress
|
|
10
10
|
from typing import TYPE_CHECKING, Any, Literal
|
|
11
11
|
from urllib.parse import parse_qs, urlparse
|
|
12
12
|
|
|
@@ -66,6 +66,7 @@ def _run_server(mcp_server: FastMCP, transport: Literal["sse"], port: int) -> No
|
|
|
66
66
|
host="127.0.0.1",
|
|
67
67
|
port=port,
|
|
68
68
|
log_level="error",
|
|
69
|
+
ws="websockets-sansio",
|
|
69
70
|
)
|
|
70
71
|
)
|
|
71
72
|
uvicorn_server.run()
|
|
@@ -74,11 +75,11 @@ def _run_server(mcp_server: FastMCP, transport: Literal["sse"], port: int) -> No
|
|
|
74
75
|
@contextmanager
|
|
75
76
|
def run_server_in_process(
|
|
76
77
|
server_fn: Callable[..., None],
|
|
77
|
-
*args,
|
|
78
|
+
*args: Any,
|
|
78
79
|
provide_host_and_port: bool = True,
|
|
79
80
|
host: str = "127.0.0.1",
|
|
80
81
|
port: int | None = None,
|
|
81
|
-
**kwargs,
|
|
82
|
+
**kwargs: Any,
|
|
82
83
|
) -> Generator[str, None, None]:
|
|
83
84
|
"""
|
|
84
85
|
Context manager that runs a FastMCP server in a separate process and
|
|
@@ -139,6 +140,86 @@ def run_server_in_process(
|
|
|
139
140
|
raise RuntimeError("Server process failed to terminate even after kill")
|
|
140
141
|
|
|
141
142
|
|
|
143
|
+
@asynccontextmanager
|
|
144
|
+
async def run_server_async(
|
|
145
|
+
server: FastMCP,
|
|
146
|
+
port: int | None = None,
|
|
147
|
+
transport: Literal["http", "streamable-http", "sse"] = "http",
|
|
148
|
+
path: str = "/mcp",
|
|
149
|
+
host: str = "127.0.0.1",
|
|
150
|
+
) -> AsyncGenerator[str, None]:
|
|
151
|
+
"""
|
|
152
|
+
Start a FastMCP server as an asyncio task for in-process async testing.
|
|
153
|
+
|
|
154
|
+
This is the recommended way to test FastMCP servers. It runs the server
|
|
155
|
+
as an async task in the same process, eliminating subprocess coordination,
|
|
156
|
+
sleeps, and cleanup issues.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
server: FastMCP server instance
|
|
160
|
+
port: Port to bind to (default: find available port)
|
|
161
|
+
transport: Transport type ("http", "streamable-http", or "sse")
|
|
162
|
+
path: URL path for the server (default: "/mcp")
|
|
163
|
+
host: Host to bind to (default: "127.0.0.1")
|
|
164
|
+
|
|
165
|
+
Yields:
|
|
166
|
+
Server URL string
|
|
167
|
+
|
|
168
|
+
Example:
|
|
169
|
+
```python
|
|
170
|
+
import pytest
|
|
171
|
+
from fastmcp import FastMCP, Client
|
|
172
|
+
from fastmcp.client.transports import StreamableHttpTransport
|
|
173
|
+
from fastmcp.utilities.tests import run_server_async
|
|
174
|
+
|
|
175
|
+
@pytest.fixture
|
|
176
|
+
async def server():
|
|
177
|
+
mcp = FastMCP("test")
|
|
178
|
+
|
|
179
|
+
@mcp.tool()
|
|
180
|
+
def greet(name: str) -> str:
|
|
181
|
+
return f"Hello, {name}!"
|
|
182
|
+
|
|
183
|
+
async with run_server_async(mcp) as url:
|
|
184
|
+
yield url
|
|
185
|
+
|
|
186
|
+
async def test_greet(server: str):
|
|
187
|
+
async with Client(StreamableHttpTransport(server)) as client:
|
|
188
|
+
result = await client.call_tool("greet", {"name": "World"})
|
|
189
|
+
assert result.content[0].text == "Hello, World!"
|
|
190
|
+
```
|
|
191
|
+
"""
|
|
192
|
+
import asyncio
|
|
193
|
+
|
|
194
|
+
if port is None:
|
|
195
|
+
port = find_available_port()
|
|
196
|
+
|
|
197
|
+
# Wait a tiny bit for the port to be released if it was just used
|
|
198
|
+
await asyncio.sleep(0.01)
|
|
199
|
+
|
|
200
|
+
# Start server as a background task
|
|
201
|
+
server_task = asyncio.create_task(
|
|
202
|
+
server.run_http_async(
|
|
203
|
+
host=host,
|
|
204
|
+
port=port,
|
|
205
|
+
transport=transport,
|
|
206
|
+
path=path,
|
|
207
|
+
show_banner=False,
|
|
208
|
+
)
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
# Give the server a moment to start
|
|
212
|
+
await asyncio.sleep(0.1)
|
|
213
|
+
|
|
214
|
+
try:
|
|
215
|
+
yield f"http://{host}:{port}{path}"
|
|
216
|
+
finally:
|
|
217
|
+
# Cleanup: cancel the task
|
|
218
|
+
server_task.cancel()
|
|
219
|
+
with suppress(asyncio.CancelledError):
|
|
220
|
+
await server_task
|
|
221
|
+
|
|
222
|
+
|
|
142
223
|
@contextmanager
|
|
143
224
|
def caplog_for_fastmcp(caplog):
|
|
144
225
|
"""Context manager to capture logs from FastMCP loggers even when propagation is disabled."""
|
fastmcp/utilities/types.py
CHANGED
|
@@ -175,6 +175,55 @@ def find_kwarg_by_type(fn: Callable, kwarg_type: type) -> str | None:
|
|
|
175
175
|
return None
|
|
176
176
|
|
|
177
177
|
|
|
178
|
+
def create_function_without_params(
|
|
179
|
+
fn: Callable[..., Any], exclude_params: list[str]
|
|
180
|
+
) -> Callable[..., Any]:
|
|
181
|
+
"""
|
|
182
|
+
Create a new function with the same code but without the specified parameters in annotations.
|
|
183
|
+
|
|
184
|
+
This is used to exclude parameters from type adapter processing when they can't be serialized.
|
|
185
|
+
The excluded parameters are removed from the function's __annotations__ dictionary.
|
|
186
|
+
"""
|
|
187
|
+
import types
|
|
188
|
+
|
|
189
|
+
if inspect.ismethod(fn):
|
|
190
|
+
actual_func = fn.__func__
|
|
191
|
+
code = actual_func.__code__ # ty: ignore[unresolved-attribute]
|
|
192
|
+
globals_dict = actual_func.__globals__ # ty: ignore[unresolved-attribute]
|
|
193
|
+
name = actual_func.__name__ # ty: ignore[unresolved-attribute]
|
|
194
|
+
defaults = actual_func.__defaults__ # ty: ignore[unresolved-attribute]
|
|
195
|
+
closure = actual_func.__closure__ # ty: ignore[unresolved-attribute]
|
|
196
|
+
else:
|
|
197
|
+
code = fn.__code__ # ty: ignore[unresolved-attribute]
|
|
198
|
+
globals_dict = fn.__globals__ # ty: ignore[unresolved-attribute]
|
|
199
|
+
name = fn.__name__ # ty: ignore[unresolved-attribute]
|
|
200
|
+
defaults = fn.__defaults__ # ty: ignore[unresolved-attribute]
|
|
201
|
+
closure = fn.__closure__ # ty: ignore[unresolved-attribute]
|
|
202
|
+
|
|
203
|
+
# Create a copy of annotations without the excluded parameters
|
|
204
|
+
original_annotations = getattr(fn, "__annotations__", {})
|
|
205
|
+
new_annotations = {
|
|
206
|
+
k: v for k, v in original_annotations.items() if k not in exclude_params
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
new_func = types.FunctionType(
|
|
210
|
+
code,
|
|
211
|
+
globals_dict,
|
|
212
|
+
name,
|
|
213
|
+
defaults,
|
|
214
|
+
closure,
|
|
215
|
+
)
|
|
216
|
+
new_func.__dict__.update(fn.__dict__)
|
|
217
|
+
new_func.__module__ = fn.__module__
|
|
218
|
+
new_func.__qualname__ = getattr(fn, "__qualname__", fn.__name__) # ty: ignore[unresolved-attribute]
|
|
219
|
+
new_func.__annotations__ = new_annotations
|
|
220
|
+
|
|
221
|
+
if inspect.ismethod(fn):
|
|
222
|
+
return types.MethodType(new_func, fn.__self__)
|
|
223
|
+
else:
|
|
224
|
+
return new_func
|
|
225
|
+
|
|
226
|
+
|
|
178
227
|
class Image:
|
|
179
228
|
"""Helper class for returning images from tools."""
|
|
180
229
|
|
|
@@ -190,34 +239,33 @@ class Image:
|
|
|
190
239
|
if path is not None and data is not None:
|
|
191
240
|
raise ValueError("Only one of path or data can be provided")
|
|
192
241
|
|
|
193
|
-
self.path =
|
|
242
|
+
self.path = self._get_expanded_path(path)
|
|
194
243
|
self.data = data
|
|
195
244
|
self._format = format
|
|
196
245
|
self._mime_type = self._get_mime_type()
|
|
197
246
|
self.annotations = annotations
|
|
198
247
|
|
|
248
|
+
@staticmethod
|
|
249
|
+
def _get_expanded_path(path: str | Path | None) -> Path | None:
|
|
250
|
+
"""Expand environment variables and user home in path."""
|
|
251
|
+
return Path(os.path.expandvars(str(path))).expanduser() if path else None
|
|
252
|
+
|
|
199
253
|
def _get_mime_type(self) -> str:
|
|
200
254
|
"""Get MIME type from format or guess from file extension."""
|
|
201
255
|
if self._format:
|
|
202
256
|
return f"image/{self._format.lower()}"
|
|
203
257
|
|
|
204
258
|
if self.path:
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
".webp": "image/webp",
|
|
212
|
-
}.get(suffix, "application/octet-stream")
|
|
259
|
+
# Workaround for WEBP in Py3.10
|
|
260
|
+
mimetypes.add_type("image/webp", ".webp")
|
|
261
|
+
resp = mimetypes.guess_type(self.path, strict=False)
|
|
262
|
+
if resp and resp[0] is not None:
|
|
263
|
+
return resp[0]
|
|
264
|
+
return "application/octet-stream"
|
|
213
265
|
return "image/png" # default for raw binary data
|
|
214
266
|
|
|
215
|
-
def
|
|
216
|
-
|
|
217
|
-
mime_type: str | None = None,
|
|
218
|
-
annotations: Annotations | None = None,
|
|
219
|
-
) -> mcp.types.ImageContent:
|
|
220
|
-
"""Convert to MCP ImageContent."""
|
|
267
|
+
def _get_data(self) -> str:
|
|
268
|
+
"""Get raw image data as base64-encoded string."""
|
|
221
269
|
if self.path:
|
|
222
270
|
with open(self.path, "rb") as f:
|
|
223
271
|
data = base64.b64encode(f.read()).decode()
|
|
@@ -225,6 +273,15 @@ class Image:
|
|
|
225
273
|
data = base64.b64encode(self.data).decode()
|
|
226
274
|
else:
|
|
227
275
|
raise ValueError("No image data available")
|
|
276
|
+
return data
|
|
277
|
+
|
|
278
|
+
def to_image_content(
|
|
279
|
+
self,
|
|
280
|
+
mime_type: str | None = None,
|
|
281
|
+
annotations: Annotations | None = None,
|
|
282
|
+
) -> mcp.types.ImageContent:
|
|
283
|
+
"""Convert to MCP ImageContent."""
|
|
284
|
+
data = self._get_data()
|
|
228
285
|
|
|
229
286
|
return mcp.types.ImageContent(
|
|
230
287
|
type="image",
|
|
@@ -233,6 +290,11 @@ class Image:
|
|
|
233
290
|
annotations=annotations or self.annotations,
|
|
234
291
|
)
|
|
235
292
|
|
|
293
|
+
def to_data_uri(self, mime_type: str | None = None) -> str:
|
|
294
|
+
"""Get image as a data URI."""
|
|
295
|
+
data = self._get_data()
|
|
296
|
+
return f"data:{mime_type or self._mime_type};base64,{data}"
|
|
297
|
+
|
|
236
298
|
|
|
237
299
|
class Audio:
|
|
238
300
|
"""Helper class for returning audio from tools."""
|
|
@@ -293,7 +355,7 @@ class Audio:
|
|
|
293
355
|
|
|
294
356
|
|
|
295
357
|
class File:
|
|
296
|
-
"""Helper class for returning
|
|
358
|
+
"""Helper class for returning file data from tools."""
|
|
297
359
|
|
|
298
360
|
def __init__(
|
|
299
361
|
self,
|