fastmcp 2.8.1__py3-none-any.whl → 2.9.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.
- fastmcp/cli/cli.py +99 -1
- fastmcp/cli/run.py +1 -3
- fastmcp/client/auth/oauth.py +1 -2
- fastmcp/client/client.py +23 -7
- fastmcp/client/logging.py +1 -2
- fastmcp/client/messages.py +126 -0
- fastmcp/client/transports.py +17 -2
- fastmcp/contrib/mcp_mixin/README.md +79 -2
- fastmcp/contrib/mcp_mixin/mcp_mixin.py +14 -0
- fastmcp/prompts/prompt.py +109 -13
- fastmcp/prompts/prompt_manager.py +119 -43
- fastmcp/resources/resource.py +27 -1
- fastmcp/resources/resource_manager.py +249 -76
- fastmcp/resources/template.py +44 -2
- fastmcp/server/auth/providers/bearer.py +62 -13
- fastmcp/server/context.py +113 -10
- fastmcp/server/http.py +8 -0
- fastmcp/server/low_level.py +35 -0
- fastmcp/server/middleware/__init__.py +6 -0
- fastmcp/server/middleware/error_handling.py +206 -0
- fastmcp/server/middleware/logging.py +165 -0
- fastmcp/server/middleware/middleware.py +236 -0
- fastmcp/server/middleware/rate_limiting.py +231 -0
- fastmcp/server/middleware/timing.py +156 -0
- fastmcp/server/proxy.py +250 -140
- fastmcp/server/server.py +446 -280
- fastmcp/settings.py +2 -2
- fastmcp/tools/tool.py +22 -2
- fastmcp/tools/tool_manager.py +114 -45
- fastmcp/tools/tool_transform.py +42 -16
- fastmcp/utilities/components.py +22 -2
- fastmcp/utilities/inspect.py +326 -0
- fastmcp/utilities/json_schema.py +67 -23
- fastmcp/utilities/mcp_config.py +13 -7
- fastmcp/utilities/openapi.py +75 -5
- fastmcp/utilities/tests.py +1 -1
- fastmcp/utilities/types.py +90 -1
- {fastmcp-2.8.1.dist-info → fastmcp-2.9.1.dist-info}/METADATA +2 -2
- fastmcp-2.9.1.dist-info/RECORD +78 -0
- fastmcp-2.8.1.dist-info/RECORD +0 -69
- {fastmcp-2.8.1.dist-info → fastmcp-2.9.1.dist-info}/WHEEL +0 -0
- {fastmcp-2.8.1.dist-info → fastmcp-2.9.1.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.8.1.dist-info → fastmcp-2.9.1.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,
|
|
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
|
|
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."""
|
fastmcp/client/auth/oauth.py
CHANGED
|
@@ -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
|
|
@@ -14,10 +15,10 @@ from pydantic import AnyUrl
|
|
|
14
15
|
import fastmcp
|
|
15
16
|
from fastmcp.client.logging import (
|
|
16
17
|
LogHandler,
|
|
17
|
-
MessageHandler,
|
|
18
18
|
create_log_callback,
|
|
19
19
|
default_log_handler,
|
|
20
20
|
)
|
|
21
|
+
from fastmcp.client.messages import MessageHandler, MessageHandlerT
|
|
21
22
|
from fastmcp.client.progress import ProgressHandler, default_progress_handler
|
|
22
23
|
from fastmcp.client.roots import (
|
|
23
24
|
RootsHandler,
|
|
@@ -142,7 +143,7 @@ class Client(Generic[ClientTransportT]):
|
|
|
142
143
|
roots: RootsList | RootsHandler | None = None,
|
|
143
144
|
sampling_handler: SamplingHandler | None = None,
|
|
144
145
|
log_handler: LogHandler | None = None,
|
|
145
|
-
message_handler: MessageHandler | None = None,
|
|
146
|
+
message_handler: MessageHandlerT | MessageHandler | None = None,
|
|
146
147
|
progress_handler: ProgressHandler | None = None,
|
|
147
148
|
timeout: datetime.timedelta | float | int | None = None,
|
|
148
149
|
init_timeout: datetime.timedelta | float | int | None = None,
|
|
@@ -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,
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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,
|
fastmcp/client/logging.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from collections.abc import Awaitable, Callable
|
|
2
2
|
from typing import TypeAlias
|
|
3
3
|
|
|
4
|
-
from mcp.client.session import LoggingFnT
|
|
4
|
+
from mcp.client.session import LoggingFnT
|
|
5
5
|
from mcp.types import LoggingMessageNotificationParams
|
|
6
6
|
|
|
7
7
|
from fastmcp.utilities.logging import get_logger
|
|
@@ -10,7 +10,6 @@ logger = get_logger(__name__)
|
|
|
10
10
|
|
|
11
11
|
LogMessage: TypeAlias = LoggingMessageNotificationParams
|
|
12
12
|
LogHandler: TypeAlias = Callable[[LogMessage], Awaitable[None]]
|
|
13
|
-
MessageHandler: TypeAlias = MessageHandlerFnT
|
|
14
13
|
|
|
15
14
|
|
|
16
15
|
async def default_log_handler(message: LogMessage) -> None:
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
from typing import TypeAlias
|
|
2
|
+
|
|
3
|
+
import mcp.types
|
|
4
|
+
from mcp.client.session import MessageHandlerFnT
|
|
5
|
+
from mcp.shared.session import RequestResponder
|
|
6
|
+
|
|
7
|
+
Message: TypeAlias = (
|
|
8
|
+
RequestResponder[mcp.types.ServerRequest, mcp.types.ClientResult]
|
|
9
|
+
| mcp.types.ServerNotification
|
|
10
|
+
| Exception
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
MessageHandlerT: TypeAlias = MessageHandlerFnT
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class MessageHandler:
|
|
17
|
+
"""
|
|
18
|
+
This class is used to handle MCP messages sent to the client. It is used to handle all messages,
|
|
19
|
+
requests, notifications, and exceptions. Users can override any of the hooks
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
async def __call__(
|
|
23
|
+
self,
|
|
24
|
+
message: RequestResponder[mcp.types.ServerRequest, mcp.types.ClientResult]
|
|
25
|
+
| mcp.types.ServerNotification
|
|
26
|
+
| Exception,
|
|
27
|
+
) -> None:
|
|
28
|
+
return await self.dispatch(message)
|
|
29
|
+
|
|
30
|
+
async def dispatch(self, message: Message) -> None:
|
|
31
|
+
# handle all messages
|
|
32
|
+
await self.on_message(message)
|
|
33
|
+
|
|
34
|
+
match message:
|
|
35
|
+
# requests
|
|
36
|
+
case RequestResponder():
|
|
37
|
+
# handle all requests
|
|
38
|
+
await self.on_request(message)
|
|
39
|
+
|
|
40
|
+
# handle specific requests
|
|
41
|
+
match message.request.root:
|
|
42
|
+
case mcp.types.PingRequest():
|
|
43
|
+
await self.on_ping(message.request.root)
|
|
44
|
+
case mcp.types.ListRootsRequest():
|
|
45
|
+
await self.on_list_roots(message.request.root)
|
|
46
|
+
case mcp.types.CreateMessageRequest():
|
|
47
|
+
await self.on_create_message(message.request.root)
|
|
48
|
+
|
|
49
|
+
# notifications
|
|
50
|
+
case mcp.types.ServerNotification():
|
|
51
|
+
# handle all notifications
|
|
52
|
+
await self.on_notification(message)
|
|
53
|
+
|
|
54
|
+
# handle specific notifications
|
|
55
|
+
match message.root:
|
|
56
|
+
case mcp.types.CancelledNotification():
|
|
57
|
+
await self.on_cancelled(message.root)
|
|
58
|
+
case mcp.types.ProgressNotification():
|
|
59
|
+
await self.on_progress(message.root)
|
|
60
|
+
case mcp.types.LoggingMessageNotification():
|
|
61
|
+
await self.on_logging_message(message.root)
|
|
62
|
+
case mcp.types.ToolListChangedNotification():
|
|
63
|
+
await self.on_tool_list_changed(message.root)
|
|
64
|
+
case mcp.types.ResourceListChangedNotification():
|
|
65
|
+
await self.on_resource_list_changed(message.root)
|
|
66
|
+
case mcp.types.PromptListChangedNotification():
|
|
67
|
+
await self.on_prompt_list_changed(message.root)
|
|
68
|
+
case mcp.types.ResourceUpdatedNotification():
|
|
69
|
+
await self.on_resource_updated(message.root)
|
|
70
|
+
|
|
71
|
+
case Exception():
|
|
72
|
+
await self.on_exception(message)
|
|
73
|
+
|
|
74
|
+
async def on_message(self, message: Message) -> None:
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
async def on_request(
|
|
78
|
+
self, message: RequestResponder[mcp.types.ServerRequest, mcp.types.ClientResult]
|
|
79
|
+
) -> None:
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
async def on_ping(self, message: mcp.types.PingRequest) -> None:
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
async def on_list_roots(self, message: mcp.types.ListRootsRequest) -> None:
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
async def on_create_message(self, message: mcp.types.CreateMessageRequest) -> None:
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
async def on_notification(self, message: mcp.types.ServerNotification) -> None:
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
async def on_exception(self, message: Exception) -> None:
|
|
95
|
+
pass
|
|
96
|
+
|
|
97
|
+
async def on_progress(self, message: mcp.types.ProgressNotification) -> None:
|
|
98
|
+
pass
|
|
99
|
+
|
|
100
|
+
async def on_logging_message(
|
|
101
|
+
self, message: mcp.types.LoggingMessageNotification
|
|
102
|
+
) -> None:
|
|
103
|
+
pass
|
|
104
|
+
|
|
105
|
+
async def on_tool_list_changed(
|
|
106
|
+
self, message: mcp.types.ToolListChangedNotification
|
|
107
|
+
) -> None:
|
|
108
|
+
pass
|
|
109
|
+
|
|
110
|
+
async def on_resource_list_changed(
|
|
111
|
+
self, message: mcp.types.ResourceListChangedNotification
|
|
112
|
+
) -> None:
|
|
113
|
+
pass
|
|
114
|
+
|
|
115
|
+
async def on_prompt_list_changed(
|
|
116
|
+
self, message: mcp.types.PromptListChangedNotification
|
|
117
|
+
) -> None:
|
|
118
|
+
pass
|
|
119
|
+
|
|
120
|
+
async def on_resource_updated(
|
|
121
|
+
self, message: mcp.types.ResourceUpdatedNotification
|
|
122
|
+
) -> None:
|
|
123
|
+
pass
|
|
124
|
+
|
|
125
|
+
async def on_cancelled(self, message: mcp.types.CancelledNotification) -> None:
|
|
126
|
+
pass
|
fastmcp/client/transports.py
CHANGED
|
@@ -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": "
|
|
739
|
+
"transport": "http"
|
|
725
740
|
},
|
|
726
741
|
"calendar": {
|
|
727
742
|
"url": "https://calendar-api.example.com/mcp",
|
|
728
|
-
"transport": "
|
|
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}
|