fastmcp 2.8.1__py3-none-any.whl → 2.9.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.
Files changed (38) hide show
  1. fastmcp/cli/cli.py +99 -1
  2. fastmcp/cli/run.py +1 -3
  3. fastmcp/client/auth/oauth.py +1 -2
  4. fastmcp/client/client.py +21 -5
  5. fastmcp/client/transports.py +17 -2
  6. fastmcp/contrib/mcp_mixin/README.md +79 -2
  7. fastmcp/contrib/mcp_mixin/mcp_mixin.py +14 -0
  8. fastmcp/prompts/prompt.py +91 -11
  9. fastmcp/prompts/prompt_manager.py +119 -43
  10. fastmcp/resources/resource.py +11 -1
  11. fastmcp/resources/resource_manager.py +249 -76
  12. fastmcp/resources/template.py +27 -1
  13. fastmcp/server/auth/providers/bearer.py +32 -10
  14. fastmcp/server/context.py +41 -2
  15. fastmcp/server/http.py +8 -0
  16. fastmcp/server/middleware/__init__.py +6 -0
  17. fastmcp/server/middleware/error_handling.py +206 -0
  18. fastmcp/server/middleware/logging.py +165 -0
  19. fastmcp/server/middleware/middleware.py +236 -0
  20. fastmcp/server/middleware/rate_limiting.py +231 -0
  21. fastmcp/server/middleware/timing.py +156 -0
  22. fastmcp/server/proxy.py +250 -140
  23. fastmcp/server/server.py +320 -242
  24. fastmcp/settings.py +2 -2
  25. fastmcp/tools/tool.py +6 -2
  26. fastmcp/tools/tool_manager.py +114 -45
  27. fastmcp/utilities/components.py +22 -2
  28. fastmcp/utilities/inspect.py +326 -0
  29. fastmcp/utilities/json_schema.py +67 -23
  30. fastmcp/utilities/mcp_config.py +13 -7
  31. fastmcp/utilities/openapi.py +5 -3
  32. fastmcp/utilities/tests.py +1 -1
  33. fastmcp/utilities/types.py +90 -1
  34. {fastmcp-2.8.1.dist-info → fastmcp-2.9.0.dist-info}/METADATA +2 -2
  35. {fastmcp-2.8.1.dist-info → fastmcp-2.9.0.dist-info}/RECORD +38 -31
  36. {fastmcp-2.8.1.dist-info → fastmcp-2.9.0.dist-info}/WHEEL +0 -0
  37. {fastmcp-2.8.1.dist-info → fastmcp-2.9.0.dist-info}/entry_points.txt +0 -0
  38. {fastmcp-2.8.1.dist-info → fastmcp-2.9.0.dist-info}/licenses/LICENSE +0 -0
fastmcp/cli/cli.py CHANGED
@@ -1,5 +1,6 @@
1
1
  """FastMCP CLI tools."""
2
2
 
3
+ import asyncio
3
4
  import importlib.metadata
4
5
  import importlib.util
5
6
  import os
@@ -11,6 +12,7 @@ from typing import Annotated
11
12
 
12
13
  import dotenv
13
14
  import typer
15
+ from pydantic import TypeAdapter
14
16
  from rich.console import Console
15
17
  from rich.table import Table
16
18
  from typer import Context, Exit
@@ -19,6 +21,7 @@ import fastmcp
19
21
  from fastmcp.cli import claude
20
22
  from fastmcp.cli import run as run_module
21
23
  from fastmcp.server.server import FastMCP
24
+ from fastmcp.utilities.inspect import FastMCPInfo, inspect_fastmcp
22
25
  from fastmcp.utilities.logging import get_logger
23
26
 
24
27
  logger = get_logger("cli")
@@ -232,7 +235,7 @@ def run(
232
235
  typer.Option(
233
236
  "--transport",
234
237
  "-t",
235
- help="Transport protocol to use (stdio, streamable-http, or sse)",
238
+ help="Transport protocol to use (stdio, http, or sse)",
236
239
  ),
237
240
  ] = None,
238
241
  host: Annotated[
@@ -435,3 +438,98 @@ def install(
435
438
  else:
436
439
  logger.error(f"Failed to install {name} in Claude app")
437
440
  sys.exit(1)
441
+
442
+
443
+ @app.command()
444
+ def inspect(
445
+ server_spec: str = typer.Argument(
446
+ ...,
447
+ help="Python file to inspect, optionally with :object suffix",
448
+ ),
449
+ output: Annotated[
450
+ Path,
451
+ typer.Option(
452
+ "--output",
453
+ "-o",
454
+ help="Output file path for the JSON report (default: server-info.json)",
455
+ ),
456
+ ] = Path("server-info.json"),
457
+ ) -> None:
458
+ """Inspect a FastMCP server and generate a JSON report.
459
+
460
+ This command analyzes a FastMCP server (v1.x or v2.x) and generates
461
+ a comprehensive JSON report containing information about the server's
462
+ name, instructions, version, tools, prompts, resources, templates,
463
+ and capabilities.
464
+
465
+ Examples:
466
+ fastmcp inspect server.py
467
+ fastmcp inspect server.py -o report.json
468
+ fastmcp inspect server.py:mcp -o analysis.json
469
+ fastmcp inspect path/to/server.py:app -o /tmp/server-info.json
470
+ """
471
+
472
+ # Parse the server specification
473
+ file, server_object = run_module.parse_file_path(server_spec)
474
+
475
+ logger.debug(
476
+ "Inspecting server",
477
+ extra={
478
+ "file": str(file),
479
+ "server_object": server_object,
480
+ "output": str(output),
481
+ },
482
+ )
483
+
484
+ try:
485
+ # Import the server
486
+ server = run_module.import_server(file, server_object)
487
+
488
+ # Get server information
489
+ async def get_info():
490
+ return await inspect_fastmcp(server)
491
+
492
+ try:
493
+ # Try to use existing event loop if available
494
+ asyncio.get_running_loop()
495
+ # If there's already a loop running, we need to run in a thread
496
+ import concurrent.futures
497
+
498
+ with concurrent.futures.ThreadPoolExecutor() as executor:
499
+ future = executor.submit(asyncio.run, get_info())
500
+ info = future.result()
501
+ except RuntimeError:
502
+ # No running loop, safe to use asyncio.run
503
+ info = asyncio.run(get_info())
504
+
505
+ info_json = TypeAdapter(FastMCPInfo).dump_json(info, indent=2)
506
+
507
+ # Ensure output directory exists
508
+ output.parent.mkdir(parents=True, exist_ok=True)
509
+
510
+ # Write JSON report (always pretty-printed)
511
+ with output.open("w", encoding="utf-8") as f:
512
+ f.write(info_json.decode("utf-8"))
513
+
514
+ logger.info(f"Server inspection complete. Report saved to {output}")
515
+
516
+ # Print summary to console
517
+ console.print(
518
+ f"[bold green]✓[/bold green] Inspected server: [bold]{info.name}[/bold]"
519
+ )
520
+ console.print(f" Tools: {len(info.tools)}")
521
+ console.print(f" Prompts: {len(info.prompts)}")
522
+ console.print(f" Resources: {len(info.resources)}")
523
+ console.print(f" Templates: {len(info.templates)}")
524
+ console.print(f" Report saved to: [cyan]{output}[/cyan]")
525
+
526
+ except Exception as e:
527
+ logger.error(
528
+ f"Failed to inspect server: {e}",
529
+ extra={
530
+ "server_spec": server_spec,
531
+ "error": str(e),
532
+ },
533
+ )
534
+ console.print(f"[bold red]✗[/bold red] Failed to inspect server: {e}")
535
+ sys.exit(1)
fastmcp/cli/run.py CHANGED
@@ -4,14 +4,12 @@ import importlib.util
4
4
  import re
5
5
  import sys
6
6
  from pathlib import Path
7
- from typing import Any, Literal
7
+ from typing import Any
8
8
 
9
9
  from fastmcp.utilities.logging import get_logger
10
10
 
11
11
  logger = get_logger("cli.run")
12
12
 
13
- TransportType = Literal["stdio", "streamable-http", "sse"]
14
-
15
13
 
16
14
  def is_url(path: str) -> bool:
17
15
  """Check if a string is a URL."""
@@ -306,8 +306,7 @@ def OAuth(
306
306
  httpx.AsyncClient (or appropriate FastMCP client/transport instance)
307
307
 
308
308
  Args:
309
- mcp_url: Full URL to the MCP endpoint (e.g.,
310
- "http://host/mcp/sse")
309
+ mcp_url: Full URL to the MCP endpoint (e.g. "http://host/mcp/sse/")
311
310
  scopes: OAuth scopes to request. Can be a
312
311
  space-separated string or a list of strings.
313
312
  client_name: Name for this client during registration
fastmcp/client/client.py CHANGED
@@ -7,6 +7,7 @@ from typing import Any, Generic, Literal, cast, overload
7
7
  import anyio
8
8
  import httpx
9
9
  import mcp.types
10
+ import pydantic_core
10
11
  from exceptiongroup import catch
11
12
  from mcp import ClientSession
12
13
  from pydantic import AnyUrl
@@ -508,13 +509,13 @@ class Client(Generic[ClientTransportT]):
508
509
 
509
510
  # --- Prompt ---
510
511
  async def get_prompt_mcp(
511
- self, name: str, arguments: dict[str, str] | None = None
512
+ self, name: str, arguments: dict[str, Any] | None = None
512
513
  ) -> mcp.types.GetPromptResult:
513
514
  """Send a prompts/get request and return the complete MCP protocol result.
514
515
 
515
516
  Args:
516
517
  name (str): The name of the prompt to retrieve.
517
- arguments (dict[str, str] | None, optional): Arguments to pass to the prompt. Defaults to None.
518
+ arguments (dict[str, Any] | None, optional): Arguments to pass to the prompt. Defaults to None.
518
519
 
519
520
  Returns:
520
521
  mcp.types.GetPromptResult: The complete response object from the protocol,
@@ -523,17 +524,32 @@ class Client(Generic[ClientTransportT]):
523
524
  Raises:
524
525
  RuntimeError: If called while the client is not connected.
525
526
  """
526
- result = await self.session.get_prompt(name=name, arguments=arguments)
527
+ # Serialize arguments for MCP protocol - convert non-string values to JSON
528
+ serialized_arguments: dict[str, str] | None = None
529
+ if arguments:
530
+ serialized_arguments = {}
531
+ for key, value in arguments.items():
532
+ if isinstance(value, str):
533
+ serialized_arguments[key] = value
534
+ else:
535
+ # Use pydantic_core.to_json for consistent serialization
536
+ serialized_arguments[key] = pydantic_core.to_json(value).decode(
537
+ "utf-8"
538
+ )
539
+
540
+ result = await self.session.get_prompt(
541
+ name=name, arguments=serialized_arguments
542
+ )
527
543
  return result
528
544
 
529
545
  async def get_prompt(
530
- self, name: str, arguments: dict[str, str] | None = None
546
+ self, name: str, arguments: dict[str, Any] | None = None
531
547
  ) -> mcp.types.GetPromptResult:
532
548
  """Retrieve a rendered prompt message list from the server.
533
549
 
534
550
  Args:
535
551
  name (str): The name of the prompt to retrieve.
536
- arguments (dict[str, str] | None, optional): Arguments to pass to the prompt. Defaults to None.
552
+ arguments (dict[str, Any] | None, optional): Arguments to pass to the prompt. Defaults to None.
537
553
 
538
554
  Returns:
539
555
  mcp.types.GetPromptResult: The complete response object from the protocol,
@@ -9,6 +9,7 @@ import warnings
9
9
  from collections.abc import AsyncIterator, Callable
10
10
  from pathlib import Path
11
11
  from typing import Any, Literal, TypedDict, TypeVar, cast, overload
12
+ from urllib.parse import urlparse, urlunparse
12
13
 
13
14
  import anyio
14
15
  import httpx
@@ -159,6 +160,13 @@ class SSETransport(ClientTransport):
159
160
  url = str(url)
160
161
  if not isinstance(url, str) or not url.startswith("http"):
161
162
  raise ValueError("Invalid HTTP/S URL provided for SSE.")
163
+
164
+ # Ensure the URL path ends with a trailing slash to avoid automatic redirects
165
+ parsed = urlparse(url)
166
+ if not parsed.path.endswith("/"):
167
+ parsed = parsed._replace(path=parsed.path + "/")
168
+ url = urlunparse(parsed)
169
+
162
170
  self.url = url
163
171
  self.headers = headers or {}
164
172
  self._set_auth(auth)
@@ -227,6 +235,13 @@ class StreamableHttpTransport(ClientTransport):
227
235
  url = str(url)
228
236
  if not isinstance(url, str) or not url.startswith("http"):
229
237
  raise ValueError("Invalid HTTP/S URL provided for Streamable HTTP.")
238
+
239
+ # Ensure the URL path ends with a trailing slash to avoid automatic redirects
240
+ parsed = urlparse(url)
241
+ if not parsed.path.endswith("/"):
242
+ parsed = parsed._replace(path=parsed.path + "/")
243
+ url = urlunparse(parsed)
244
+
230
245
  self.url = url
231
246
  self.headers = headers or {}
232
247
  self._set_auth(auth)
@@ -721,11 +736,11 @@ class MCPConfigTransport(ClientTransport):
721
736
  "mcpServers": {
722
737
  "weather": {
723
738
  "url": "https://weather-api.example.com/mcp",
724
- "transport": "streamable-http"
739
+ "transport": "http"
725
740
  },
726
741
  "calendar": {
727
742
  "url": "https://calendar-api.example.com/mcp",
728
- "transport": "streamable-http"
743
+ "transport": "http"
729
744
  }
730
745
  }
731
746
  }
@@ -1,26 +1,103 @@
1
+ from mcp.types import ToolAnnotations
2
+
1
3
  # MCP Mixin
2
4
 
3
5
  This module provides the `MCPMixin` base class and associated decorators (`@mcp_tool`, `@mcp_resource`, `@mcp_prompt`).
4
6
 
5
7
  It allows developers to easily define classes whose methods can be registered as tools, resources, or prompts with a `FastMCP` server instance using the `register_all()`, `register_tools()`, `register_resources()`, or `register_prompts()` methods provided by the mixin.
6
8
 
9
+ Includes support for
10
+ Tools:
11
+ * [enable/disable](https://gofastmcp.com/servers/tools#disabling-tools)
12
+ * [annotations](https://gofastmcp.com/servers/tools#annotations-2)
13
+ * [excluded arguments](https://gofastmcp.com/servers/tools#excluding-arguments)
14
+
15
+ Prompts:
16
+ * [enable/disable](https://gofastmcp.com/servers/prompts#disabling-prompts)
17
+
18
+ Resources:
19
+ * [enable/disabe](https://gofastmcp.com/servers/resources#disabling-resources)
20
+
7
21
  ## Usage
8
22
 
9
23
  Inherit from `MCPMixin` and use the decorators on the methods you want to register.
10
24
 
11
25
  ```python
26
+ from mcp.types import ToolAnnotations
12
27
  from fastmcp import FastMCP
13
- from fastmcp.contrib.mcp_mixin import MCPMixin, mcp_tool, mcp_resource
28
+ from fastmcp.contrib.mcp_mixin import MCPMixin, mcp_tool, mcp_resource, mcp_prompt
14
29
 
15
30
  class MyComponent(MCPMixin):
16
31
  @mcp_tool(name="my_tool", description="Does something cool.")
17
32
  def tool_method(self):
18
33
  return "Tool executed!"
19
34
 
35
+ # example of disabled tool
36
+ @mcp_tool(name="my_tool", description="Does something cool.", enabled=False)
37
+ def disabled_tool_method(self):
38
+ # This function can't be called by client because it's disabled
39
+ return "You'll never get here!"
40
+
41
+ # example of excluded parameter tool
42
+ @mcp_tool(
43
+ name="my_tool", description="Does something cool.",
44
+ enabled=False, exclude_args=['delete_everything'],
45
+ )
46
+ def excluded_param_tool_method(self, delete_everything=False):
47
+ # MCP tool calls can't pass the "delete_everything" argument
48
+ if delete_everything:
49
+ return "Nothing to delete, I bet you're not a tool :)"
50
+ return "You might be a tool if..."
51
+
52
+ # example tool w/annotations
53
+ @mcp_tool(
54
+ name="my_tool", description="Does something cool.",
55
+ annotations=ToolAnnotations(
56
+ title="Attn LLM, use this tool first!",
57
+ readOnlyHint=False,
58
+ destructiveHint=False,
59
+ idempotentHint=False,
60
+ )
61
+ )
62
+ def tool_method(self):
63
+ return "Tool executed!"
64
+
65
+ # example tool w/everything
66
+ @mcp_tool(
67
+ name="my_tool", description="Does something cool.",
68
+ enabled=True,
69
+ exclude_args=['delete_all'],
70
+ annotations=ToolAnnotations(
71
+ title="Attn LLM, use this tool first!",
72
+ readOnlyHint=False,
73
+ destructiveHint=False,
74
+ idempotentHint=False,
75
+ )
76
+ )
77
+ def tool_method(self, delete_all=False):
78
+ if delete_all:
79
+ return "99 records deleted. I bet you're not a tool :)"
80
+ return "Tool executed, but you might be a tool!"
81
+
20
82
  @mcp_resource(uri="component://data")
21
83
  def resource_method(self):
22
84
  return {"data": "some data"}
23
85
 
86
+ # Disabled resource
87
+ @mcp_resource(uri="component://data", enabled=False)
88
+ def resource_method(self):
89
+ return {"data": "some data"}
90
+
91
+ # prompt
92
+ @mcp_prompt(name="A prompt")
93
+ def prompt_method(self, name):
94
+ return f"Whats up {name}?"
95
+
96
+ # disabled prompt
97
+ @mcp_prompt(name="A prompt", enabled=False)
98
+ def prompt_method(self, name):
99
+ return f"Whats up {name}?"
100
+
24
101
  mcp_server = FastMCP()
25
102
  component = MyComponent()
26
103
 
@@ -36,4 +113,4 @@ component.register_all(mcp_server, prefix="my_comp")
36
113
  # Or 'my_tool' and 'component://data' are registered (if no prefix used)
37
114
  ```
38
115
 
39
- The `prefix` argument in registration methods is optional. If omitted, methods are registered with their original decorated names/URIs. Individual separators (`tools_separator`, `resources_separator`, `prompts_separator`) can also be provided to `register_all` to change the separator for specific types.
116
+ The `prefix` argument in registration methods is optional. If omitted, methods are registered with their original decorated names/URIs. Individual separators (`tools_separator`, `resources_separator`, `prompts_separator`) can also be provided to `register_all` to change the separator for specific types.
@@ -3,6 +3,8 @@
3
3
  from collections.abc import Callable
4
4
  from typing import TYPE_CHECKING, Any
5
5
 
6
+ from mcp.types import ToolAnnotations
7
+
6
8
  from fastmcp.prompts.prompt import Prompt
7
9
  from fastmcp.resources.resource import Resource
8
10
  from fastmcp.tools.tool import Tool
@@ -23,6 +25,10 @@ def mcp_tool(
23
25
  name: str | None = None,
24
26
  description: str | None = None,
25
27
  tags: set[str] | None = None,
28
+ annotations: ToolAnnotations | dict[str, Any] | None = None,
29
+ exclude_args: list[str] | None = None,
30
+ serializer: Callable[[Any], str] | None = None,
31
+ enabled: bool | None = None,
26
32
  ) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
27
33
  """Decorator to mark a method as an MCP tool for later registration."""
28
34
 
@@ -31,6 +37,10 @@ def mcp_tool(
31
37
  "name": name or func.__name__,
32
38
  "description": description,
33
39
  "tags": tags,
40
+ "annotations": annotations,
41
+ "exclude_args": exclude_args,
42
+ "serializer": serializer,
43
+ "enabled": enabled,
34
44
  }
35
45
  call_args = {k: v for k, v in call_args.items() if v is not None}
36
46
  setattr(func, _MCP_REGISTRATION_TOOL_ATTR, call_args)
@@ -46,6 +56,7 @@ def mcp_resource(
46
56
  description: str | None = None,
47
57
  mime_type: str | None = None,
48
58
  tags: set[str] | None = None,
59
+ enabled: bool | None = None,
49
60
  ) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
50
61
  """Decorator to mark a method as an MCP resource for later registration."""
51
62
 
@@ -56,6 +67,7 @@ def mcp_resource(
56
67
  "description": description,
57
68
  "mime_type": mime_type,
58
69
  "tags": tags,
70
+ "enabled": enabled,
59
71
  }
60
72
  call_args = {k: v for k, v in call_args.items() if v is not None}
61
73
 
@@ -70,6 +82,7 @@ def mcp_prompt(
70
82
  name: str | None = None,
71
83
  description: str | None = None,
72
84
  tags: set[str] | None = None,
85
+ enabled: bool | None = None,
73
86
  ) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
74
87
  """Decorator to mark a method as an MCP prompt for later registration."""
75
88
 
@@ -78,6 +91,7 @@ def mcp_prompt(
78
91
  "name": name or func.__name__,
79
92
  "description": description,
80
93
  "tags": tags,
94
+ "enabled": enabled,
81
95
  }
82
96
 
83
97
  call_args = {k: v for k, v in call_args.items() if v is not None}
fastmcp/prompts/prompt.py CHANGED
@@ -3,15 +3,16 @@
3
3
  from __future__ import annotations as _annotations
4
4
 
5
5
  import inspect
6
+ import json
6
7
  from abc import ABC, abstractmethod
7
8
  from collections.abc import Awaitable, Callable, Sequence
8
- from typing import TYPE_CHECKING, Any
9
+ from typing import Any
9
10
 
10
11
  import pydantic_core
11
12
  from mcp.types import Prompt as MCPPrompt
12
13
  from mcp.types import PromptArgument as MCPPromptArgument
13
14
  from mcp.types import PromptMessage, Role, TextContent
14
- from pydantic import Field, TypeAdapter, validate_call
15
+ from pydantic import Field, TypeAdapter
15
16
 
16
17
  from fastmcp.exceptions import PromptError
17
18
  from fastmcp.server.dependencies import get_context
@@ -25,10 +26,6 @@ from fastmcp.utilities.types import (
25
26
  get_cached_typeadapter,
26
27
  )
27
28
 
28
- if TYPE_CHECKING:
29
- pass
30
-
31
-
32
29
  logger = get_logger(__name__)
33
30
 
34
31
 
@@ -155,7 +152,7 @@ class FunctionPrompt(Prompt):
155
152
  if param.kind == inspect.Parameter.VAR_KEYWORD:
156
153
  raise ValueError("Functions with **kwargs are not supported as prompts")
157
154
 
158
- description = description or fn.__doc__
155
+ description = description or inspect.getdoc(fn)
159
156
 
160
157
  # if the fn is a callable class, we need to get the __call__ method from here out
161
158
  if not inspect.isroutine(fn):
@@ -181,17 +178,43 @@ class FunctionPrompt(Prompt):
181
178
  arguments: list[PromptArgument] = []
182
179
  if "properties" in parameters:
183
180
  for param_name, param in parameters["properties"].items():
181
+ arg_description = param.get("description")
182
+
183
+ # For non-string parameters, append JSON schema info to help users
184
+ # understand the expected format when passing as strings (MCP requirement)
185
+ if param_name in sig.parameters:
186
+ sig_param = sig.parameters[param_name]
187
+ if (
188
+ sig_param.annotation != inspect.Parameter.empty
189
+ and sig_param.annotation is not str
190
+ and param_name != context_kwarg
191
+ ):
192
+ # Get the JSON schema for this specific parameter type
193
+ try:
194
+ param_adapter = get_cached_typeadapter(sig_param.annotation)
195
+ param_schema = param_adapter.json_schema()
196
+
197
+ # Create compact schema representation
198
+ schema_str = json.dumps(param_schema, separators=(",", ":"))
199
+
200
+ # Append schema info to description
201
+ schema_note = f"Provide as a JSON string matching the following schema: {schema_str}"
202
+ if arg_description:
203
+ arg_description = f"{arg_description}\n\n{schema_note}"
204
+ else:
205
+ arg_description = schema_note
206
+ except Exception:
207
+ # If schema generation fails, skip enhancement
208
+ pass
209
+
184
210
  arguments.append(
185
211
  PromptArgument(
186
212
  name=param_name,
187
- description=param.get("description"),
213
+ description=arg_description,
188
214
  required=param_name in parameters.get("required", []),
189
215
  )
190
216
  )
191
217
 
192
- # ensure the arguments are properly cast
193
- fn = validate_call(fn)
194
-
195
218
  return cls(
196
219
  name=func_name,
197
220
  description=description,
@@ -201,6 +224,60 @@ class FunctionPrompt(Prompt):
201
224
  fn=fn,
202
225
  )
203
226
 
227
+ def _convert_string_arguments(self, kwargs: dict[str, Any]) -> dict[str, Any]:
228
+ """Convert string arguments to expected types based on function signature."""
229
+ from fastmcp.server.context import Context
230
+
231
+ sig = inspect.signature(self.fn)
232
+ converted_kwargs = {}
233
+
234
+ # Find context parameter name if any
235
+ context_param_name = find_kwarg_by_type(self.fn, kwarg_type=Context)
236
+
237
+ for param_name, param_value in kwargs.items():
238
+ if param_name in sig.parameters:
239
+ param = sig.parameters[param_name]
240
+
241
+ # Skip Context parameters - they're handled separately
242
+ if param_name == context_param_name:
243
+ converted_kwargs[param_name] = param_value
244
+ continue
245
+
246
+ # If parameter has no annotation or annotation is str, pass as-is
247
+ if (
248
+ param.annotation == inspect.Parameter.empty
249
+ or param.annotation is str
250
+ ):
251
+ converted_kwargs[param_name] = param_value
252
+ # If argument is not a string, pass as-is (already properly typed)
253
+ elif not isinstance(param_value, str):
254
+ converted_kwargs[param_name] = param_value
255
+ else:
256
+ # Try to convert string argument using type adapter
257
+ try:
258
+ adapter = get_cached_typeadapter(param.annotation)
259
+ # Try JSON parsing first for complex types
260
+ try:
261
+ converted_kwargs[param_name] = adapter.validate_json(
262
+ param_value
263
+ )
264
+ except (ValueError, TypeError, pydantic_core.ValidationError):
265
+ # Fallback to direct validation
266
+ converted_kwargs[param_name] = adapter.validate_python(
267
+ param_value
268
+ )
269
+ except (ValueError, TypeError, pydantic_core.ValidationError) as e:
270
+ # If conversion fails, provide informative error
271
+ raise PromptError(
272
+ f"Could not convert argument '{param_name}' with value '{param_value}' "
273
+ f"to expected type {param.annotation}. Error: {e}"
274
+ )
275
+ else:
276
+ # Parameter not in function signature, pass as-is
277
+ converted_kwargs[param_name] = param_value
278
+
279
+ return converted_kwargs
280
+
204
281
  async def render(
205
282
  self,
206
283
  arguments: dict[str, Any] | None = None,
@@ -223,6 +300,9 @@ class FunctionPrompt(Prompt):
223
300
  if context_kwarg and context_kwarg not in kwargs:
224
301
  kwargs[context_kwarg] = get_context()
225
302
 
303
+ # Convert string arguments to expected types when needed
304
+ kwargs = self._convert_string_arguments(kwargs)
305
+
226
306
  # Call function and check if result is a coroutine
227
307
  result = self.fn(**kwargs)
228
308
  if inspect.iscoroutine(result):