Goro commited on
Commit
8430481
·
1 Parent(s): e9d44f5

Refactor duplicated logic

Browse files
src/fastmcp/contrib/component_manager/README.md ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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.component_manager import set_up_component_manager
30
+
31
+ mcp = FastMCP("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
+ By default, 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
+ * Works with template URIs too
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
+
70
+ ## ⚙️ Configuration Options
71
+
72
+ ### Custom Root Path
73
+
74
+ To mount the API under a different path:
75
+
76
+ ```python
77
+ set_up_component_manager(server=mcp, path="/admin")
78
+ ```
79
+
80
+ ### Securing Endpoints with Auth Scopes
81
+
82
+ If your server uses authentication:
83
+
84
+ ```python
85
+ mcp = FastMCP("Component Manager", instructions="This is a test server with component manager.", auth=auth)
86
+ set_up_component_manager(server=mcp, required_scopes=["tools:write", "tools:read"])
87
+ ```
88
+
89
+ ---
90
+
91
+ ## 🧪 Example: Enabling a Tool with Curl
92
+
93
+ ```bash
94
+ curl -X POST \
95
+ -H "Authorization: Bearer YOUR_TOKEN_HERE" \
96
+ -H "Content-Type: application/json" \
97
+ http://localhost:8001/tools/example_tool/enable
98
+ ```
99
+
100
+ ---
101
+
102
+ ## ⚙️ How It Works
103
+
104
+ - `set_up_component_manager()` registers API routes for tools, resources, and prompts.
105
+ - The `ComponentService` class exposes async methods to enable/disable components.
106
+ - Each endpoint returns a success message in JSON or a 404 error if the component isn't found.
107
+
108
+ ---
109
+
110
+ ## 🧩 Extending
111
+
112
+ You can subclass `ComponentService` for custom behavior or mount its routes elsewhere as needed.
113
+
114
+ ---
115
+
116
+ ## Maintenance Notice
117
+
118
+ This module is not officially maintained by the core FastMCP team. It is an independent extension developed by [gorocode](https://github.com/gorocode).
119
+
120
+ 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.
121
+
122
+
123
+ ## 📄 License
124
+
125
+ This module follows the license of the main [FastMCP](https://github.com/jlowin/fastmcp) project.
src/fastmcp/contrib/component_manager/__init__.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ from .component_manager import set_up_component_manager
2
+ from .component_service import ComponentService
3
+
4
+ __all__ = [
5
+ "set_up_component_manager",
6
+ "ComponentService"
7
+ ]
src/fastmcp/contrib/component_manager/component_manager.py CHANGED
@@ -14,11 +14,12 @@ from typing import Any
14
  from fastmcp.server.server import FastMCP
15
 
16
  def set_up_component_manager(
17
- server: FastMCP, root_path: str = "/", required_scopes: list[str] | None = None
18
  ):
19
  """Set up routes for enabling/disabling tools, resources, and prompts.
20
  Args:
21
  server: The FastMCP server instance
 
22
  required_scopes: Optional list of scopes required for these routes
23
  Returns:
24
  A list of routes or mounts for component management
@@ -47,13 +48,13 @@ def set_up_component_manager(
47
 
48
  if required_scopes is None:
49
  routes.extend(
50
- build_component_manager_enpoints(route_configs, root_path)
51
  )
52
  else:
53
- if root_path != "/":
54
  mounts.append(
55
  build_component_manager_mount(
56
- route_configs, root_path, required_scopes
57
  ))
58
  else:
59
  mounts.append(
@@ -73,78 +74,52 @@ def set_up_component_manager(
73
  server._additional_http_routes.extend(mounts)
74
 
75
 
76
- def build_component_manager_enpoints(route_configs, root_path, required_scopes=None) -> list[Route]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  component_management_routes: list[Route] = []
78
 
79
  for component in route_configs:
80
  config: dict[str, Any] = route_configs[component]
81
  for action in ["enable", "disable"]:
82
-
83
- async def endpoint(
84
- request: Request,
85
- action: str = action,
86
- component: str = component,
87
- config: dict[str, Any] = config,
88
- ):
89
- name = request.path_params[config["param"].split(":")[0]]
90
-
91
- try:
92
- await config[action](name)
93
- return JSONResponse(
94
- {"message": f"{action.capitalize()}d {component}: {name}"}
95
- )
96
- except NotFoundError:
97
- raise StarletteHTTPException(
98
- status_code=404,
99
- detail=f"Unknown {component}: {name}",
100
- )
101
-
102
- if required_scopes is not None and root_path in ["/tools", "/resources", "/prompts"]:
103
- path = f"/{{{config['param']}}}/{action}"
104
- else:
105
- path = f"/{component}s/{{{config['param']}}}/{action}"
106
-
107
- route = Route(path, endpoint=endpoint, methods=["POST"])
108
- component_management_routes.append(route)
109
 
110
  return component_management_routes
111
 
 
112
  def build_component_manager_mount(route_configs, root_path, required_scopes) -> Mount:
113
  component_management_routes: list[Route] = []
114
 
115
  for component in route_configs:
116
  config: dict[str, Any] = route_configs[component]
117
  for action in ["enable", "disable"]:
118
-
119
- async def endpoint(
120
- request: Request,
121
- action: str = action,
122
- component: str = component,
123
- config: dict[str, Any] = config,
124
- ):
125
- name = request.path_params[config["param"].split(":")[0]]
126
-
127
- try:
128
- await config[action](name)
129
- return JSONResponse(
130
- {"message": f"{action.capitalize()}d {component}: {name}"}
131
- )
132
- except NotFoundError:
133
- raise StarletteHTTPException(
134
- status_code=404,
135
- detail=f"Unknown {component}: {name}",
136
- )
137
-
138
- if required_scopes is not None and root_path in ["/tools", "/resources", "/prompts"]:
139
- path = f"/{{{config['param']}}}/{action}"
140
- else:
141
- path = f"/{component}s/{{{config['param']}}}/{action}"
142
-
143
- route = Route(path, endpoint=endpoint, methods=["POST"])
144
- component_management_routes.append(route)
145
 
146
  return Mount(
147
  f"{root_path}",
148
- app=RequireAuthMiddleware(Starlette(routes=component_management_routes),
149
- required_scopes)
150
- )
 
14
  from fastmcp.server.server import FastMCP
15
 
16
  def set_up_component_manager(
17
+ server: FastMCP, path: str = "/", required_scopes: list[str] | None = None
18
  ):
19
  """Set up routes for enabling/disabling tools, resources, and prompts.
20
  Args:
21
  server: The FastMCP server instance
22
+ root_path: Path used to mount all component-related routes on the server
23
  required_scopes: Optional list of scopes required for these routes
24
  Returns:
25
  A list of routes or mounts for component management
 
48
 
49
  if required_scopes is None:
50
  routes.extend(
51
+ build_component_manager_endpoints(route_configs, path)
52
  )
53
  else:
54
+ if path != "/":
55
  mounts.append(
56
  build_component_manager_mount(
57
+ route_configs, path, required_scopes
58
  ))
59
  else:
60
  mounts.append(
 
74
  server._additional_http_routes.extend(mounts)
75
 
76
 
77
+ def make_endpoint(action, component, config):
78
+ async def endpoint(request: Request):
79
+ name = request.path_params[config["param"].split(":")[0]]
80
+
81
+ try:
82
+ await config[action](name)
83
+ return JSONResponse(
84
+ {"message": f"{action.capitalize()}d {component}: {name}"}
85
+ )
86
+ except NotFoundError:
87
+ raise StarletteHTTPException(
88
+ status_code=404,
89
+ detail=f"Unknown {component}: {name}",
90
+ )
91
+ return endpoint
92
+
93
+ def make_route(action, component, config, required_scopes, root_path) -> Route:
94
+ endpoint = make_endpoint(action, component, config)
95
+
96
+ if required_scopes is not None and root_path in ["/tools", "/resources", "/prompts"]:
97
+ path = f"/{{{config['param']}}}/{action}"
98
+ else:
99
+ path = f"/{component}s/{{{config['param']}}}/{action}"
100
+
101
+ return Route(path, endpoint=endpoint, methods=["POST"])
102
+
103
+ def build_component_manager_endpoints(route_configs, root_path, required_scopes=None) -> list[Route]:
104
  component_management_routes: list[Route] = []
105
 
106
  for component in route_configs:
107
  config: dict[str, Any] = route_configs[component]
108
  for action in ["enable", "disable"]:
109
+ component_management_routes.append(make_route(action, component, config, required_scopes, root_path))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
 
111
  return component_management_routes
112
 
113
+
114
  def build_component_manager_mount(route_configs, root_path, required_scopes) -> Mount:
115
  component_management_routes: list[Route] = []
116
 
117
  for component in route_configs:
118
  config: dict[str, Any] = route_configs[component]
119
  for action in ["enable", "disable"]:
120
+ component_management_routes.append(make_route(action, component, config, required_scopes, root_path))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
 
122
  return Mount(
123
  f"{root_path}",
124
+ app=RequireAuthMiddleware(Starlette(routes=component_management_routes), required_scopes)
125
+ )
 
tests/contrib/test_component_manager.py CHANGED
@@ -3,7 +3,7 @@ from starlette import status
3
  from starlette.testclient import TestClient
4
 
5
  from fastmcp import FastMCP
6
- from fastmcp.contrib.component_manager.component_manager import set_up_component_manager
7
  from fastmcp.server.auth.providers.bearer import BearerAuthProvider, RSAKeyPair
8
 
9
 
 
3
  from starlette.testclient import TestClient
4
 
5
  from fastmcp import FastMCP
6
+ from fastmcp.contrib.component_manager import set_up_component_manager
7
  from fastmcp.server.auth.providers.bearer import BearerAuthProvider, RSAKeyPair
8
 
9