fastmcp 2.9.1__py3-none-any.whl → 2.10.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.
- fastmcp/cli/cli.py +16 -1
- fastmcp/cli/run.py +4 -0
- fastmcp/client/auth/oauth.py +5 -82
- fastmcp/client/client.py +114 -24
- fastmcp/client/elicitation.py +63 -0
- fastmcp/client/transports.py +50 -36
- fastmcp/contrib/component_manager/README.md +170 -0
- fastmcp/contrib/component_manager/__init__.py +4 -0
- fastmcp/contrib/component_manager/component_manager.py +186 -0
- fastmcp/contrib/component_manager/component_service.py +225 -0
- fastmcp/contrib/component_manager/example.py +59 -0
- fastmcp/prompts/prompt.py +12 -4
- fastmcp/resources/resource.py +8 -3
- fastmcp/resources/template.py +5 -0
- fastmcp/server/auth/auth.py +15 -0
- fastmcp/server/auth/providers/bearer.py +41 -3
- fastmcp/server/auth/providers/bearer_env.py +4 -0
- fastmcp/server/auth/providers/in_memory.py +15 -0
- fastmcp/server/context.py +144 -4
- fastmcp/server/elicitation.py +160 -0
- fastmcp/server/http.py +1 -9
- fastmcp/server/low_level.py +4 -2
- fastmcp/server/middleware/__init__.py +14 -1
- fastmcp/server/middleware/logging.py +11 -0
- fastmcp/server/middleware/middleware.py +10 -6
- fastmcp/server/openapi.py +19 -77
- fastmcp/server/proxy.py +13 -6
- fastmcp/server/server.py +76 -11
- fastmcp/settings.py +0 -17
- fastmcp/tools/tool.py +209 -57
- fastmcp/tools/tool_manager.py +2 -3
- fastmcp/tools/tool_transform.py +125 -26
- fastmcp/utilities/cli.py +106 -0
- fastmcp/utilities/components.py +5 -1
- fastmcp/utilities/json_schema_type.py +648 -0
- fastmcp/utilities/openapi.py +69 -0
- fastmcp/utilities/types.py +50 -19
- {fastmcp-2.9.1.dist-info → fastmcp-2.10.0.dist-info}/METADATA +3 -2
- {fastmcp-2.9.1.dist-info → fastmcp-2.10.0.dist-info}/RECORD +42 -33
- {fastmcp-2.9.1.dist-info → fastmcp-2.10.0.dist-info}/WHEEL +0 -0
- {fastmcp-2.9.1.dist-info → fastmcp-2.10.0.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.9.1.dist-info → fastmcp-2.10.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# Component Manager – Contrib Module for FastMCP
|
|
2
|
+
|
|
3
|
+
The **Component Manager** provides a unified API for enabling and disabling tools, resources, and prompts at runtime in a FastMCP server. This module is useful for dynamic control over which components are active, enabling advanced features like feature toggling, admin interfaces, or automation workflows.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 🔧 Features
|
|
8
|
+
|
|
9
|
+
- Enable/disable **tools**, **resources**, and **prompts** via HTTP endpoints.
|
|
10
|
+
- Supports **local** and **mounted (server)** components.
|
|
11
|
+
- Customizable **API root path**.
|
|
12
|
+
- Optional **Auth scopes** for secured access.
|
|
13
|
+
- Fully integrates with FastMCP with minimal configuration.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 📦 Installation
|
|
18
|
+
|
|
19
|
+
This module is part of the `fastmcp.contrib` package. No separate installation is required if you're already using **FastMCP**.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 🚀 Usage
|
|
24
|
+
|
|
25
|
+
### Basic Setup
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from fastmcp import FastMCP
|
|
29
|
+
from fastmcp.contrib.component_manager import set_up_component_manager
|
|
30
|
+
|
|
31
|
+
mcp = FastMCP(name="Component Manager", instructions="This is a test server with component manager.")
|
|
32
|
+
set_up_component_manager(server=mcp)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 🔗 API Endpoints
|
|
38
|
+
|
|
39
|
+
All endpoints are registered at `/` by default, or under the custom path if one is provided.
|
|
40
|
+
|
|
41
|
+
### Tools
|
|
42
|
+
|
|
43
|
+
```http
|
|
44
|
+
POST /tools/{tool_name}/enable
|
|
45
|
+
POST /tools/{tool_name}/disable
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Resources
|
|
49
|
+
|
|
50
|
+
```http
|
|
51
|
+
POST /resources/{uri:path}/enable
|
|
52
|
+
POST /resources/{uri:path}/disable
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
* Supports template URIs as well
|
|
56
|
+
```http
|
|
57
|
+
POST /resources/example://test/{id}/enable
|
|
58
|
+
POST /resources/example://test/{id}/disable
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Prompts
|
|
62
|
+
|
|
63
|
+
```http
|
|
64
|
+
POST /prompts/{prompt_name}/enable
|
|
65
|
+
POST /prompts/{prompt_name}/disable
|
|
66
|
+
```
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
#### 🧪 Example Response
|
|
70
|
+
|
|
71
|
+
```http
|
|
72
|
+
HTTP/1.1 200 OK
|
|
73
|
+
Content-Type: application/json
|
|
74
|
+
|
|
75
|
+
{
|
|
76
|
+
"message": "Disabled tool: example_tool"
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## ⚙️ Configuration Options
|
|
84
|
+
|
|
85
|
+
### Custom Root Path
|
|
86
|
+
|
|
87
|
+
To mount the API under a different path:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
set_up_component_manager(server=mcp, path="/admin")
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Securing Endpoints with Auth Scopes
|
|
94
|
+
|
|
95
|
+
If your server uses authentication:
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
mcp = FastMCP(name="Component Manager", instructions="This is a test server with component manager.", auth=auth)
|
|
99
|
+
set_up_component_manager(server=mcp, required_scopes=["write", "read"])
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## 🧪 Example: Enabling a Tool with Curl
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
curl -X POST \
|
|
108
|
+
-H "Authorization: Bearer YOUR_TOKEN_HERE" \
|
|
109
|
+
-H "Content-Type: application/json" \
|
|
110
|
+
http://localhost:8001/tools/example_tool/enable
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## 🧱 Working with Mounted Servers
|
|
116
|
+
|
|
117
|
+
You can also combine different configurations when working with mounted servers — for example, using different scopes:
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
mcp = FastMCP(name="Component Manager", instructions="This is a test server with component manager.", auth=auth)
|
|
121
|
+
set_up_component_manager(server=mcp, required_scopes=["mcp:write"])
|
|
122
|
+
|
|
123
|
+
mounted = FastMCP(name="Component Manager", instructions="This is a test server with component manager.", auth=auth)
|
|
124
|
+
set_up_component_manager(server=mounted, required_scopes=["mounted:write"])
|
|
125
|
+
|
|
126
|
+
mcp.mount(server=mounted, prefix="mo")
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
This allows you to grant different levels of access:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
# Accessing the main server gives you control over both local and mounted components
|
|
133
|
+
curl -X POST \
|
|
134
|
+
-H "Authorization: Bearer YOUR_TOKEN_HERE" \
|
|
135
|
+
-H "Content-Type: application/json" \
|
|
136
|
+
http://localhost:8001/tools/mo_example_tool/enable
|
|
137
|
+
|
|
138
|
+
# Accessing the mounted server gives you control only over its own components
|
|
139
|
+
curl -X POST \
|
|
140
|
+
-H "Authorization: Bearer YOUR_TOKEN_HERE" \
|
|
141
|
+
-H "Content-Type: application/json" \
|
|
142
|
+
http://localhost:8002/tools/example_tool/enable
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## ⚙️ How It Works
|
|
148
|
+
|
|
149
|
+
- `set_up_component_manager()` registers API routes for tools, resources, and prompts.
|
|
150
|
+
- The `ComponentService` class exposes async methods to enable/disable components.
|
|
151
|
+
- Each endpoint returns a success message in JSON or a 404 error if the component isn't found.
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## 🧩 Extending
|
|
156
|
+
|
|
157
|
+
You can subclass `ComponentService` for custom behavior or mount its routes elsewhere as needed.
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Maintenance Notice
|
|
162
|
+
|
|
163
|
+
This module is not officially maintained by the core FastMCP team. It is an independent extension developed by [gorocode](https://github.com/gorocode).
|
|
164
|
+
|
|
165
|
+
If you encounter any issues or wish to contribute, please feel free to open an issue or submit a pull request, and kindly notify me. I'd love to stay up to date.
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
## 📄 License
|
|
169
|
+
|
|
170
|
+
This module follows the license of the main [FastMCP](https://github.com/jlowin/fastmcp) project.
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Routes and helpers for managing tools, resources, and prompts in FastMCP.
|
|
3
|
+
Provides endpoints for enabling/disabling components via HTTP, with optional authentication scopes.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from mcp.server.auth.middleware.bearer_auth import RequireAuthMiddleware
|
|
9
|
+
from starlette.applications import Starlette
|
|
10
|
+
from starlette.exceptions import HTTPException as StarletteHTTPException
|
|
11
|
+
from starlette.requests import Request
|
|
12
|
+
from starlette.responses import JSONResponse
|
|
13
|
+
from starlette.routing import Mount, Route
|
|
14
|
+
|
|
15
|
+
from fastmcp.contrib.component_manager.component_service import ComponentService
|
|
16
|
+
from fastmcp.exceptions import NotFoundError
|
|
17
|
+
from fastmcp.server.server import FastMCP
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def set_up_component_manager(
|
|
21
|
+
server: FastMCP, path: str = "/", required_scopes: list[str] | None = None
|
|
22
|
+
):
|
|
23
|
+
"""Set up routes for enabling/disabling tools, resources, and prompts.
|
|
24
|
+
Args:
|
|
25
|
+
server: The FastMCP server instance
|
|
26
|
+
path: Path used to mount all component-related routes on the server
|
|
27
|
+
required_scopes: Optional list of scopes required for these routes. Applies only if authentication is enabled.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
service = ComponentService(server)
|
|
31
|
+
routes: list[Route] = []
|
|
32
|
+
mounts: list[Mount] = []
|
|
33
|
+
route_configs = {
|
|
34
|
+
"tool": {
|
|
35
|
+
"param": "tool_name",
|
|
36
|
+
"enable": service._enable_tool,
|
|
37
|
+
"disable": service._disable_tool,
|
|
38
|
+
},
|
|
39
|
+
"resource": {
|
|
40
|
+
"param": "uri:path",
|
|
41
|
+
"enable": service._enable_resource,
|
|
42
|
+
"disable": service._disable_resource,
|
|
43
|
+
},
|
|
44
|
+
"prompt": {
|
|
45
|
+
"param": "prompt_name",
|
|
46
|
+
"enable": service._enable_prompt,
|
|
47
|
+
"disable": service._disable_prompt,
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if required_scopes is None:
|
|
52
|
+
routes.extend(build_component_manager_endpoints(route_configs, path))
|
|
53
|
+
else:
|
|
54
|
+
if path != "/":
|
|
55
|
+
mounts.append(
|
|
56
|
+
build_component_manager_mount(route_configs, path, required_scopes)
|
|
57
|
+
)
|
|
58
|
+
else:
|
|
59
|
+
mounts.append(
|
|
60
|
+
build_component_manager_mount(
|
|
61
|
+
{"tool": route_configs["tool"]}, "/tools", required_scopes
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
mounts.append(
|
|
65
|
+
build_component_manager_mount(
|
|
66
|
+
{"resource": route_configs["resource"]},
|
|
67
|
+
"/resources",
|
|
68
|
+
required_scopes,
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
mounts.append(
|
|
72
|
+
build_component_manager_mount(
|
|
73
|
+
{"prompt": route_configs["prompt"]}, "/prompts", required_scopes
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
server._additional_http_routes.extend(routes)
|
|
78
|
+
server._additional_http_routes.extend(mounts)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def make_endpoint(action, component, config):
|
|
82
|
+
"""
|
|
83
|
+
Factory for creating Starlette endpoint functions for enabling/disabling a component.
|
|
84
|
+
Args:
|
|
85
|
+
action: 'enable' or 'disable'
|
|
86
|
+
component: The component type (e.g., 'tool', 'resource', or 'prompt')
|
|
87
|
+
config: Dict with param and handler functions for the component
|
|
88
|
+
Returns:
|
|
89
|
+
An async endpoint function for Starlette.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
async def endpoint(request: Request):
|
|
93
|
+
name = request.path_params[config["param"].split(":")[0]]
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
await config[action](name)
|
|
97
|
+
return JSONResponse(
|
|
98
|
+
{"message": f"{action.capitalize()}d {component}: {name}"}
|
|
99
|
+
)
|
|
100
|
+
except NotFoundError:
|
|
101
|
+
raise StarletteHTTPException(
|
|
102
|
+
status_code=404,
|
|
103
|
+
detail=f"Unknown {component}: {name}",
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
return endpoint
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def make_route(action, component, config, required_scopes, root_path) -> Route:
|
|
110
|
+
"""
|
|
111
|
+
Creates a Starlette Route for enabling/disabling a component.
|
|
112
|
+
Args:
|
|
113
|
+
action: 'enable' or 'disable'
|
|
114
|
+
component: The component type
|
|
115
|
+
config: Dict with param and handler functions
|
|
116
|
+
required_scopes: Optional list of required auth scopes
|
|
117
|
+
root_path: The base path for the route
|
|
118
|
+
Returns:
|
|
119
|
+
A Starlette Route object.
|
|
120
|
+
"""
|
|
121
|
+
endpoint = make_endpoint(action, component, config)
|
|
122
|
+
|
|
123
|
+
if required_scopes is not None and root_path in [
|
|
124
|
+
"/tools",
|
|
125
|
+
"/resources",
|
|
126
|
+
"/prompts",
|
|
127
|
+
]:
|
|
128
|
+
path = f"/{{{config['param']}}}/{action}"
|
|
129
|
+
else:
|
|
130
|
+
if root_path != "/" and required_scopes is None:
|
|
131
|
+
path = f"{root_path}/{component}s/{{{config['param']}}}/{action}"
|
|
132
|
+
else:
|
|
133
|
+
path = f"/{component}s/{{{config['param']}}}/{action}"
|
|
134
|
+
|
|
135
|
+
return Route(path, endpoint=endpoint, methods=["POST"])
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def build_component_manager_endpoints(
|
|
139
|
+
route_configs, root_path, required_scopes=None
|
|
140
|
+
) -> list[Route]:
|
|
141
|
+
"""
|
|
142
|
+
Build a list of Starlette Route objects for all components/actions.
|
|
143
|
+
Args:
|
|
144
|
+
route_configs: Dict describing component types and their handlers
|
|
145
|
+
root_path: The base path for the routes
|
|
146
|
+
required_scopes: Optional list of required auth scopes
|
|
147
|
+
Returns:
|
|
148
|
+
List of Starlette Route objects for component management.
|
|
149
|
+
"""
|
|
150
|
+
component_management_routes: list[Route] = []
|
|
151
|
+
|
|
152
|
+
for component in route_configs:
|
|
153
|
+
config: dict[str, Any] = route_configs[component]
|
|
154
|
+
for action in ["enable", "disable"]:
|
|
155
|
+
component_management_routes.append(
|
|
156
|
+
make_route(action, component, config, required_scopes, root_path)
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
return component_management_routes
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def build_component_manager_mount(route_configs, root_path, required_scopes) -> Mount:
|
|
163
|
+
"""
|
|
164
|
+
Build a Starlette Mount with authentication for component management routes.
|
|
165
|
+
Args:
|
|
166
|
+
route_configs: Dict describing component types and their handlers
|
|
167
|
+
root_path: The base path for the mount
|
|
168
|
+
required_scopes: List of required auth scopes
|
|
169
|
+
Returns:
|
|
170
|
+
A Starlette Mount object with authentication middleware.
|
|
171
|
+
"""
|
|
172
|
+
component_management_routes: list[Route] = []
|
|
173
|
+
|
|
174
|
+
for component in route_configs:
|
|
175
|
+
config: dict[str, Any] = route_configs[component]
|
|
176
|
+
for action in ["enable", "disable"]:
|
|
177
|
+
component_management_routes.append(
|
|
178
|
+
make_route(action, component, config, required_scopes, root_path)
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
return Mount(
|
|
182
|
+
f"{root_path}",
|
|
183
|
+
app=RequireAuthMiddleware(
|
|
184
|
+
Starlette(routes=component_management_routes), required_scopes
|
|
185
|
+
),
|
|
186
|
+
)
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ComponentService: Provides async management of tools, resources, and prompts for FastMCP servers.
|
|
3
|
+
Handles enabling/disabling components both locally and across mounted servers.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from fastmcp.exceptions import NotFoundError
|
|
7
|
+
from fastmcp.prompts.prompt import Prompt
|
|
8
|
+
from fastmcp.resources.resource import Resource
|
|
9
|
+
from fastmcp.resources.template import ResourceTemplate
|
|
10
|
+
from fastmcp.server.server import FastMCP, has_resource_prefix, remove_resource_prefix
|
|
11
|
+
from fastmcp.tools.tool import Tool
|
|
12
|
+
from fastmcp.utilities.logging import get_logger
|
|
13
|
+
|
|
14
|
+
logger = get_logger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ComponentService:
|
|
18
|
+
"""Service for managing components like tools, resources, and prompts."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, server: FastMCP):
|
|
21
|
+
self._server = server
|
|
22
|
+
self._tool_manager = server._tool_manager
|
|
23
|
+
self._resource_manager = server._resource_manager
|
|
24
|
+
self._prompt_manager = server._prompt_manager
|
|
25
|
+
|
|
26
|
+
async def _enable_tool(self, key: str) -> Tool:
|
|
27
|
+
"""Handle 'enableTool' requests.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
key: The key of the tool to enable
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
The tool that was enabled
|
|
34
|
+
"""
|
|
35
|
+
logger.debug("Enabling tool: %s", key)
|
|
36
|
+
|
|
37
|
+
# 1. Check local tools first. The server will have already applied its filter.
|
|
38
|
+
if key in self._server._tool_manager._tools:
|
|
39
|
+
tool: Tool = await self._server.get_tool(key)
|
|
40
|
+
tool.enable()
|
|
41
|
+
return tool
|
|
42
|
+
|
|
43
|
+
# 2. Check mounted servers using the filtered protocol path.
|
|
44
|
+
for mounted in reversed(self._tool_manager._mounted_servers):
|
|
45
|
+
if mounted.prefix:
|
|
46
|
+
if key.startswith(f"{mounted.prefix}_"):
|
|
47
|
+
tool_key = key.removeprefix(f"{mounted.prefix}_")
|
|
48
|
+
mounted_service = ComponentService(mounted.server)
|
|
49
|
+
tool = await mounted_service._enable_tool(tool_key)
|
|
50
|
+
return tool
|
|
51
|
+
else:
|
|
52
|
+
continue
|
|
53
|
+
raise NotFoundError(f"Unknown tool: {key}")
|
|
54
|
+
|
|
55
|
+
async def _disable_tool(self, key: str) -> Tool:
|
|
56
|
+
"""Handle 'disableTool' requests.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
key: The key of the tool to disable
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
The tool that was disabled
|
|
63
|
+
"""
|
|
64
|
+
logger.debug("Disable tool: %s", key)
|
|
65
|
+
|
|
66
|
+
# 1. Check local tools first. The server will have already applied its filter.
|
|
67
|
+
if key in self._server._tool_manager._tools:
|
|
68
|
+
tool: Tool = await self._server.get_tool(key)
|
|
69
|
+
tool.disable()
|
|
70
|
+
return tool
|
|
71
|
+
|
|
72
|
+
# 2. Check mounted servers using the filtered protocol path.
|
|
73
|
+
for mounted in reversed(self._tool_manager._mounted_servers):
|
|
74
|
+
if mounted.prefix:
|
|
75
|
+
if key.startswith(f"{mounted.prefix}_"):
|
|
76
|
+
tool_key = key.removeprefix(f"{mounted.prefix}_")
|
|
77
|
+
mounted_service = ComponentService(mounted.server)
|
|
78
|
+
tool = await mounted_service._disable_tool(tool_key)
|
|
79
|
+
return tool
|
|
80
|
+
else:
|
|
81
|
+
continue
|
|
82
|
+
raise NotFoundError(f"Unknown tool: {key}")
|
|
83
|
+
|
|
84
|
+
async def _enable_resource(self, key: str) -> Resource | ResourceTemplate:
|
|
85
|
+
"""Handle 'enableResource' requests.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
key: The key of the resource to enable
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
The resource that was enabled
|
|
92
|
+
"""
|
|
93
|
+
logger.debug("Enabling resource: %s", key)
|
|
94
|
+
|
|
95
|
+
# 1. Check local resources first. The server will have already applied its filter.
|
|
96
|
+
if key in self._resource_manager._resources:
|
|
97
|
+
resource: Resource = await self._server.get_resource(key)
|
|
98
|
+
resource.enable()
|
|
99
|
+
return resource
|
|
100
|
+
if key in self._resource_manager._templates:
|
|
101
|
+
template: ResourceTemplate = await self._server.get_resource_template(key)
|
|
102
|
+
template.enable()
|
|
103
|
+
return template
|
|
104
|
+
|
|
105
|
+
# 2. Check mounted servers using the filtered protocol path.
|
|
106
|
+
for mounted in reversed(self._resource_manager._mounted_servers):
|
|
107
|
+
if mounted.prefix:
|
|
108
|
+
if has_resource_prefix(
|
|
109
|
+
key,
|
|
110
|
+
mounted.prefix,
|
|
111
|
+
mounted.resource_prefix_format,
|
|
112
|
+
):
|
|
113
|
+
key = remove_resource_prefix(
|
|
114
|
+
key,
|
|
115
|
+
mounted.prefix,
|
|
116
|
+
mounted.resource_prefix_format,
|
|
117
|
+
)
|
|
118
|
+
mounted_service = ComponentService(mounted.server)
|
|
119
|
+
mounted_resource: (
|
|
120
|
+
Resource | ResourceTemplate
|
|
121
|
+
) = await mounted_service._enable_resource(key)
|
|
122
|
+
return mounted_resource
|
|
123
|
+
else:
|
|
124
|
+
continue
|
|
125
|
+
raise NotFoundError(f"Unknown resource: {key}")
|
|
126
|
+
|
|
127
|
+
async def _disable_resource(self, key: str) -> Resource | ResourceTemplate:
|
|
128
|
+
"""Handle 'disableResource' requests.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
key: The key of the resource to disable
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
The resource that was disabled
|
|
135
|
+
"""
|
|
136
|
+
logger.debug("Disable resource: %s", key)
|
|
137
|
+
|
|
138
|
+
# 1. Check local resources first. The server will have already applied its filter.
|
|
139
|
+
if key in self._resource_manager._resources:
|
|
140
|
+
resource: Resource = await self._server.get_resource(key)
|
|
141
|
+
resource.disable()
|
|
142
|
+
return resource
|
|
143
|
+
if key in self._resource_manager._templates:
|
|
144
|
+
template: ResourceTemplate = await self._server.get_resource_template(key)
|
|
145
|
+
template.disable()
|
|
146
|
+
return template
|
|
147
|
+
|
|
148
|
+
# 2. Check mounted servers using the filtered protocol path.
|
|
149
|
+
for mounted in reversed(self._resource_manager._mounted_servers):
|
|
150
|
+
if mounted.prefix:
|
|
151
|
+
if has_resource_prefix(
|
|
152
|
+
key,
|
|
153
|
+
mounted.prefix,
|
|
154
|
+
mounted.resource_prefix_format,
|
|
155
|
+
):
|
|
156
|
+
key = remove_resource_prefix(
|
|
157
|
+
key,
|
|
158
|
+
mounted.prefix,
|
|
159
|
+
mounted.resource_prefix_format,
|
|
160
|
+
)
|
|
161
|
+
mounted_service = ComponentService(mounted.server)
|
|
162
|
+
mounted_resource: (
|
|
163
|
+
Resource | ResourceTemplate
|
|
164
|
+
) = await mounted_service._disable_resource(key)
|
|
165
|
+
return mounted_resource
|
|
166
|
+
else:
|
|
167
|
+
continue
|
|
168
|
+
raise NotFoundError(f"Unknown resource: {key}")
|
|
169
|
+
|
|
170
|
+
async def _enable_prompt(self, key: str) -> Prompt:
|
|
171
|
+
"""Handle 'enablePrompt' requests.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
key: The key of the prompt to enable
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
The prompt that was enable
|
|
178
|
+
"""
|
|
179
|
+
logger.debug("Enabling prompt: %s", key)
|
|
180
|
+
|
|
181
|
+
# 1. Check local prompts first. The server will have already applied its filter.
|
|
182
|
+
if key in self._server._prompt_manager._prompts:
|
|
183
|
+
prompt: Prompt = await self._server.get_prompt(key)
|
|
184
|
+
prompt.enable()
|
|
185
|
+
return prompt
|
|
186
|
+
|
|
187
|
+
# 2. Check mounted servers using the filtered protocol path.
|
|
188
|
+
for mounted in reversed(self._prompt_manager._mounted_servers):
|
|
189
|
+
if mounted.prefix:
|
|
190
|
+
if key.startswith(f"{mounted.prefix}_"):
|
|
191
|
+
prompt_key = key.removeprefix(f"{mounted.prefix}_")
|
|
192
|
+
mounted_service = ComponentService(mounted.server)
|
|
193
|
+
prompt = await mounted_service._enable_prompt(prompt_key)
|
|
194
|
+
return prompt
|
|
195
|
+
else:
|
|
196
|
+
continue
|
|
197
|
+
raise NotFoundError(f"Unknown prompt: {key}")
|
|
198
|
+
|
|
199
|
+
async def _disable_prompt(self, key: str) -> Prompt:
|
|
200
|
+
"""Handle 'disablePrompt' requests.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
key: The key of the prompt to disable
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
The prompt that was disabled
|
|
207
|
+
"""
|
|
208
|
+
|
|
209
|
+
# 1. Check local prompts first. The server will have already applied its filter.
|
|
210
|
+
if key in self._server._prompt_manager._prompts:
|
|
211
|
+
prompt: Prompt = await self._server.get_prompt(key)
|
|
212
|
+
prompt.disable()
|
|
213
|
+
return prompt
|
|
214
|
+
|
|
215
|
+
# 2. Check mounted servers using the filtered protocol path.
|
|
216
|
+
for mounted in reversed(self._prompt_manager._mounted_servers):
|
|
217
|
+
if mounted.prefix:
|
|
218
|
+
if key.startswith(f"{mounted.prefix}_"):
|
|
219
|
+
prompt_key = key.removeprefix(f"{mounted.prefix}_")
|
|
220
|
+
mounted_service = ComponentService(mounted.server)
|
|
221
|
+
prompt = await mounted_service._disable_prompt(prompt_key)
|
|
222
|
+
return prompt
|
|
223
|
+
else:
|
|
224
|
+
continue
|
|
225
|
+
raise NotFoundError(f"Unknown prompt: {key}")
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from fastmcp import FastMCP
|
|
2
|
+
from fastmcp.contrib.component_manager import set_up_component_manager
|
|
3
|
+
from fastmcp.server.auth.providers.bearer import BearerAuthProvider, RSAKeyPair
|
|
4
|
+
|
|
5
|
+
key_pair = RSAKeyPair.generate()
|
|
6
|
+
|
|
7
|
+
auth = BearerAuthProvider(
|
|
8
|
+
public_key=key_pair.public_key,
|
|
9
|
+
issuer="https://dev.example.com",
|
|
10
|
+
audience="my-dev-server",
|
|
11
|
+
required_scopes=["mcp:read"],
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
# Build main server
|
|
15
|
+
mcp_token = key_pair.create_token(
|
|
16
|
+
subject="dev-user",
|
|
17
|
+
issuer="https://dev.example.com",
|
|
18
|
+
audience="my-dev-server",
|
|
19
|
+
scopes=["mcp:write", "mcp:read"],
|
|
20
|
+
)
|
|
21
|
+
mcp = FastMCP(
|
|
22
|
+
name="Component Manager",
|
|
23
|
+
instructions="This is a test server with component manager.",
|
|
24
|
+
auth=auth,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# Set up main server component manager
|
|
28
|
+
set_up_component_manager(server=mcp, required_scopes=["mcp:write"])
|
|
29
|
+
|
|
30
|
+
# Build mounted server
|
|
31
|
+
mounted_token = key_pair.create_token(
|
|
32
|
+
subject="dev-user",
|
|
33
|
+
issuer="https://dev.example.com",
|
|
34
|
+
audience="my-dev-server",
|
|
35
|
+
scopes=["mounted:write", "mcp:read"],
|
|
36
|
+
)
|
|
37
|
+
mounted = FastMCP(
|
|
38
|
+
name="Component Manager",
|
|
39
|
+
instructions="This is a test server with component manager.",
|
|
40
|
+
auth=auth,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# Set up mounted server component manager
|
|
44
|
+
set_up_component_manager(server=mounted, required_scopes=["mounted:write"])
|
|
45
|
+
|
|
46
|
+
# Mount
|
|
47
|
+
mcp.mount(server=mounted, prefix="mo")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@mcp.resource("resource://greeting")
|
|
51
|
+
def get_greeting() -> str:
|
|
52
|
+
"""Provides a simple greeting message."""
|
|
53
|
+
return "Hello from FastMCP Resources!"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@mounted.tool("greeting")
|
|
57
|
+
def get_info() -> str:
|
|
58
|
+
"""Provides a simple info."""
|
|
59
|
+
return "You are using component manager contrib module!"
|