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.
Files changed (108) hide show
  1. fastmcp/__init__.py +2 -2
  2. fastmcp/cli/cli.py +11 -11
  3. fastmcp/cli/install/claude_code.py +6 -6
  4. fastmcp/cli/install/claude_desktop.py +3 -3
  5. fastmcp/cli/install/cursor.py +18 -12
  6. fastmcp/cli/install/gemini_cli.py +3 -3
  7. fastmcp/cli/install/mcp_json.py +3 -3
  8. fastmcp/cli/run.py +13 -8
  9. fastmcp/client/__init__.py +9 -9
  10. fastmcp/client/auth/oauth.py +115 -217
  11. fastmcp/client/client.py +105 -39
  12. fastmcp/client/logging.py +18 -14
  13. fastmcp/client/oauth_callback.py +85 -171
  14. fastmcp/client/sampling.py +1 -1
  15. fastmcp/client/transports.py +80 -25
  16. fastmcp/contrib/component_manager/__init__.py +1 -1
  17. fastmcp/contrib/component_manager/component_manager.py +2 -2
  18. fastmcp/contrib/component_manager/component_service.py +6 -6
  19. fastmcp/contrib/mcp_mixin/README.md +32 -1
  20. fastmcp/contrib/mcp_mixin/__init__.py +2 -2
  21. fastmcp/contrib/mcp_mixin/mcp_mixin.py +14 -2
  22. fastmcp/experimental/sampling/handlers/openai.py +2 -2
  23. fastmcp/experimental/server/openapi/__init__.py +5 -8
  24. fastmcp/experimental/server/openapi/components.py +11 -7
  25. fastmcp/experimental/server/openapi/routing.py +2 -2
  26. fastmcp/experimental/utilities/openapi/__init__.py +10 -15
  27. fastmcp/experimental/utilities/openapi/director.py +14 -15
  28. fastmcp/experimental/utilities/openapi/json_schema_converter.py +6 -2
  29. fastmcp/experimental/utilities/openapi/models.py +3 -3
  30. fastmcp/experimental/utilities/openapi/parser.py +37 -16
  31. fastmcp/experimental/utilities/openapi/schemas.py +2 -2
  32. fastmcp/mcp_config.py +3 -4
  33. fastmcp/prompts/__init__.py +1 -1
  34. fastmcp/prompts/prompt.py +22 -19
  35. fastmcp/prompts/prompt_manager.py +16 -101
  36. fastmcp/resources/__init__.py +5 -5
  37. fastmcp/resources/resource.py +14 -9
  38. fastmcp/resources/resource_manager.py +9 -168
  39. fastmcp/resources/template.py +107 -17
  40. fastmcp/resources/types.py +30 -24
  41. fastmcp/server/__init__.py +1 -1
  42. fastmcp/server/auth/__init__.py +9 -5
  43. fastmcp/server/auth/auth.py +70 -43
  44. fastmcp/server/auth/handlers/authorize.py +326 -0
  45. fastmcp/server/auth/jwt_issuer.py +236 -0
  46. fastmcp/server/auth/middleware.py +96 -0
  47. fastmcp/server/auth/oauth_proxy.py +1510 -289
  48. fastmcp/server/auth/oidc_proxy.py +84 -20
  49. fastmcp/server/auth/providers/auth0.py +40 -21
  50. fastmcp/server/auth/providers/aws.py +29 -3
  51. fastmcp/server/auth/providers/azure.py +312 -131
  52. fastmcp/server/auth/providers/bearer.py +1 -1
  53. fastmcp/server/auth/providers/debug.py +114 -0
  54. fastmcp/server/auth/providers/descope.py +86 -29
  55. fastmcp/server/auth/providers/discord.py +308 -0
  56. fastmcp/server/auth/providers/github.py +29 -8
  57. fastmcp/server/auth/providers/google.py +48 -9
  58. fastmcp/server/auth/providers/in_memory.py +27 -3
  59. fastmcp/server/auth/providers/introspection.py +281 -0
  60. fastmcp/server/auth/providers/jwt.py +48 -31
  61. fastmcp/server/auth/providers/oci.py +233 -0
  62. fastmcp/server/auth/providers/scalekit.py +238 -0
  63. fastmcp/server/auth/providers/supabase.py +188 -0
  64. fastmcp/server/auth/providers/workos.py +35 -17
  65. fastmcp/server/context.py +177 -51
  66. fastmcp/server/dependencies.py +39 -12
  67. fastmcp/server/elicitation.py +1 -1
  68. fastmcp/server/http.py +56 -17
  69. fastmcp/server/low_level.py +121 -2
  70. fastmcp/server/middleware/__init__.py +1 -1
  71. fastmcp/server/middleware/caching.py +476 -0
  72. fastmcp/server/middleware/error_handling.py +14 -10
  73. fastmcp/server/middleware/logging.py +50 -39
  74. fastmcp/server/middleware/middleware.py +29 -16
  75. fastmcp/server/middleware/rate_limiting.py +3 -3
  76. fastmcp/server/middleware/tool_injection.py +116 -0
  77. fastmcp/server/openapi.py +10 -6
  78. fastmcp/server/proxy.py +22 -11
  79. fastmcp/server/server.py +725 -242
  80. fastmcp/settings.py +24 -10
  81. fastmcp/tools/__init__.py +1 -1
  82. fastmcp/tools/tool.py +70 -23
  83. fastmcp/tools/tool_manager.py +30 -112
  84. fastmcp/tools/tool_transform.py +12 -10
  85. fastmcp/utilities/cli.py +67 -28
  86. fastmcp/utilities/components.py +7 -2
  87. fastmcp/utilities/inspect.py +79 -23
  88. fastmcp/utilities/json_schema.py +4 -4
  89. fastmcp/utilities/json_schema_type.py +4 -4
  90. fastmcp/utilities/logging.py +118 -8
  91. fastmcp/utilities/mcp_server_config/__init__.py +3 -3
  92. fastmcp/utilities/mcp_server_config/v1/environments/base.py +1 -2
  93. fastmcp/utilities/mcp_server_config/v1/environments/uv.py +6 -6
  94. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +4 -4
  95. fastmcp/utilities/mcp_server_config/v1/schema.json +3 -0
  96. fastmcp/utilities/mcp_server_config/v1/sources/base.py +0 -1
  97. fastmcp/utilities/openapi.py +11 -11
  98. fastmcp/utilities/tests.py +85 -4
  99. fastmcp/utilities/types.py +78 -16
  100. fastmcp/utilities/ui.py +626 -0
  101. {fastmcp-2.12.5.dist-info → fastmcp-2.13.2.dist-info}/METADATA +22 -14
  102. fastmcp-2.13.2.dist-info/RECORD +144 -0
  103. {fastmcp-2.12.5.dist-info → fastmcp-2.13.2.dist-info}/WHEEL +1 -1
  104. fastmcp/cli/claude.py +0 -135
  105. fastmcp/utilities/storage.py +0 -204
  106. fastmcp-2.12.5.dist-info/RECORD +0 -134
  107. {fastmcp-2.12.5.dist-info → fastmcp-2.13.2.dist-info}/entry_points.txt +0 -0
  108. {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
- pass # Default no-op implementation
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: str | None = Field(
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: str | None = Field(
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[str] | None = Field(
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"
@@ -17,7 +17,6 @@ class Source(BaseModel, ABC):
17
17
  need preparation (e.g., local files), this is a no-op.
18
18
  """
19
19
  # Default implementation for sources that don't need preparation
20
- pass
21
20
 
22
21
  @abstractmethod
23
22
  async def load_server(self) -> Any:
@@ -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
- "HttpMethod",
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
- "_handle_nullable_fields",
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 = getattr(target, "components")
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 + ["null"]
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") + [{"type": "null"}]
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 prop_name, prop_schema in result["properties"].items():
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:
@@ -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."""
@@ -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 = Path(os.path.expandvars(str(path))).expanduser() if path else None
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
- suffix = self.path.suffix.lower()
206
- return {
207
- ".png": "image/png",
208
- ".jpg": "image/jpeg",
209
- ".jpeg": "image/jpeg",
210
- ".gif": "image/gif",
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 to_image_content(
216
- self,
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 audio from tools."""
358
+ """Helper class for returning file data from tools."""
297
359
 
298
360
  def __init__(
299
361
  self,