mcp-hangar 0.2.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.
- mcp_hangar/__init__.py +139 -0
- mcp_hangar/application/__init__.py +1 -0
- mcp_hangar/application/commands/__init__.py +67 -0
- mcp_hangar/application/commands/auth_commands.py +118 -0
- mcp_hangar/application/commands/auth_handlers.py +296 -0
- mcp_hangar/application/commands/commands.py +59 -0
- mcp_hangar/application/commands/handlers.py +189 -0
- mcp_hangar/application/discovery/__init__.py +21 -0
- mcp_hangar/application/discovery/discovery_metrics.py +283 -0
- mcp_hangar/application/discovery/discovery_orchestrator.py +497 -0
- mcp_hangar/application/discovery/lifecycle_manager.py +315 -0
- mcp_hangar/application/discovery/security_validator.py +414 -0
- mcp_hangar/application/event_handlers/__init__.py +50 -0
- mcp_hangar/application/event_handlers/alert_handler.py +191 -0
- mcp_hangar/application/event_handlers/audit_handler.py +203 -0
- mcp_hangar/application/event_handlers/knowledge_base_handler.py +120 -0
- mcp_hangar/application/event_handlers/logging_handler.py +69 -0
- mcp_hangar/application/event_handlers/metrics_handler.py +152 -0
- mcp_hangar/application/event_handlers/persistent_audit_store.py +217 -0
- mcp_hangar/application/event_handlers/security_handler.py +604 -0
- mcp_hangar/application/mcp/tooling.py +158 -0
- mcp_hangar/application/ports/__init__.py +9 -0
- mcp_hangar/application/ports/observability.py +237 -0
- mcp_hangar/application/queries/__init__.py +52 -0
- mcp_hangar/application/queries/auth_handlers.py +237 -0
- mcp_hangar/application/queries/auth_queries.py +118 -0
- mcp_hangar/application/queries/handlers.py +227 -0
- mcp_hangar/application/read_models/__init__.py +11 -0
- mcp_hangar/application/read_models/provider_views.py +139 -0
- mcp_hangar/application/sagas/__init__.py +11 -0
- mcp_hangar/application/sagas/group_rebalance_saga.py +137 -0
- mcp_hangar/application/sagas/provider_failover_saga.py +266 -0
- mcp_hangar/application/sagas/provider_recovery_saga.py +172 -0
- mcp_hangar/application/services/__init__.py +9 -0
- mcp_hangar/application/services/provider_service.py +208 -0
- mcp_hangar/application/services/traced_provider_service.py +211 -0
- mcp_hangar/bootstrap/runtime.py +328 -0
- mcp_hangar/context.py +178 -0
- mcp_hangar/domain/__init__.py +117 -0
- mcp_hangar/domain/contracts/__init__.py +57 -0
- mcp_hangar/domain/contracts/authentication.py +225 -0
- mcp_hangar/domain/contracts/authorization.py +229 -0
- mcp_hangar/domain/contracts/event_store.py +178 -0
- mcp_hangar/domain/contracts/metrics_publisher.py +59 -0
- mcp_hangar/domain/contracts/persistence.py +383 -0
- mcp_hangar/domain/contracts/provider_runtime.py +146 -0
- mcp_hangar/domain/discovery/__init__.py +20 -0
- mcp_hangar/domain/discovery/conflict_resolver.py +267 -0
- mcp_hangar/domain/discovery/discovered_provider.py +185 -0
- mcp_hangar/domain/discovery/discovery_service.py +412 -0
- mcp_hangar/domain/discovery/discovery_source.py +192 -0
- mcp_hangar/domain/events.py +433 -0
- mcp_hangar/domain/exceptions.py +525 -0
- mcp_hangar/domain/model/__init__.py +70 -0
- mcp_hangar/domain/model/aggregate.py +58 -0
- mcp_hangar/domain/model/circuit_breaker.py +152 -0
- mcp_hangar/domain/model/event_sourced_api_key.py +413 -0
- mcp_hangar/domain/model/event_sourced_provider.py +423 -0
- mcp_hangar/domain/model/event_sourced_role_assignment.py +268 -0
- mcp_hangar/domain/model/health_tracker.py +183 -0
- mcp_hangar/domain/model/load_balancer.py +185 -0
- mcp_hangar/domain/model/provider.py +810 -0
- mcp_hangar/domain/model/provider_group.py +656 -0
- mcp_hangar/domain/model/tool_catalog.py +105 -0
- mcp_hangar/domain/policies/__init__.py +19 -0
- mcp_hangar/domain/policies/provider_health.py +187 -0
- mcp_hangar/domain/repository.py +249 -0
- mcp_hangar/domain/security/__init__.py +85 -0
- mcp_hangar/domain/security/input_validator.py +710 -0
- mcp_hangar/domain/security/rate_limiter.py +387 -0
- mcp_hangar/domain/security/roles.py +237 -0
- mcp_hangar/domain/security/sanitizer.py +387 -0
- mcp_hangar/domain/security/secrets.py +501 -0
- mcp_hangar/domain/services/__init__.py +20 -0
- mcp_hangar/domain/services/audit_service.py +376 -0
- mcp_hangar/domain/services/image_builder.py +328 -0
- mcp_hangar/domain/services/provider_launcher.py +1046 -0
- mcp_hangar/domain/value_objects.py +1138 -0
- mcp_hangar/errors.py +818 -0
- mcp_hangar/fastmcp_server.py +1105 -0
- mcp_hangar/gc.py +134 -0
- mcp_hangar/infrastructure/__init__.py +79 -0
- mcp_hangar/infrastructure/async_executor.py +133 -0
- mcp_hangar/infrastructure/auth/__init__.py +37 -0
- mcp_hangar/infrastructure/auth/api_key_authenticator.py +388 -0
- mcp_hangar/infrastructure/auth/event_sourced_store.py +567 -0
- mcp_hangar/infrastructure/auth/jwt_authenticator.py +360 -0
- mcp_hangar/infrastructure/auth/middleware.py +340 -0
- mcp_hangar/infrastructure/auth/opa_authorizer.py +243 -0
- mcp_hangar/infrastructure/auth/postgres_store.py +659 -0
- mcp_hangar/infrastructure/auth/projections.py +366 -0
- mcp_hangar/infrastructure/auth/rate_limiter.py +311 -0
- mcp_hangar/infrastructure/auth/rbac_authorizer.py +323 -0
- mcp_hangar/infrastructure/auth/sqlite_store.py +624 -0
- mcp_hangar/infrastructure/command_bus.py +112 -0
- mcp_hangar/infrastructure/discovery/__init__.py +110 -0
- mcp_hangar/infrastructure/discovery/docker_source.py +289 -0
- mcp_hangar/infrastructure/discovery/entrypoint_source.py +249 -0
- mcp_hangar/infrastructure/discovery/filesystem_source.py +383 -0
- mcp_hangar/infrastructure/discovery/kubernetes_source.py +247 -0
- mcp_hangar/infrastructure/event_bus.py +260 -0
- mcp_hangar/infrastructure/event_sourced_repository.py +443 -0
- mcp_hangar/infrastructure/event_store.py +396 -0
- mcp_hangar/infrastructure/knowledge_base/__init__.py +259 -0
- mcp_hangar/infrastructure/knowledge_base/contracts.py +202 -0
- mcp_hangar/infrastructure/knowledge_base/memory.py +177 -0
- mcp_hangar/infrastructure/knowledge_base/postgres.py +545 -0
- mcp_hangar/infrastructure/knowledge_base/sqlite.py +513 -0
- mcp_hangar/infrastructure/metrics_publisher.py +36 -0
- mcp_hangar/infrastructure/observability/__init__.py +10 -0
- mcp_hangar/infrastructure/observability/langfuse_adapter.py +534 -0
- mcp_hangar/infrastructure/persistence/__init__.py +33 -0
- mcp_hangar/infrastructure/persistence/audit_repository.py +371 -0
- mcp_hangar/infrastructure/persistence/config_repository.py +398 -0
- mcp_hangar/infrastructure/persistence/database.py +333 -0
- mcp_hangar/infrastructure/persistence/database_common.py +330 -0
- mcp_hangar/infrastructure/persistence/event_serializer.py +280 -0
- mcp_hangar/infrastructure/persistence/event_upcaster.py +166 -0
- mcp_hangar/infrastructure/persistence/in_memory_event_store.py +150 -0
- mcp_hangar/infrastructure/persistence/recovery_service.py +312 -0
- mcp_hangar/infrastructure/persistence/sqlite_event_store.py +386 -0
- mcp_hangar/infrastructure/persistence/unit_of_work.py +409 -0
- mcp_hangar/infrastructure/persistence/upcasters/README.md +13 -0
- mcp_hangar/infrastructure/persistence/upcasters/__init__.py +7 -0
- mcp_hangar/infrastructure/query_bus.py +153 -0
- mcp_hangar/infrastructure/saga_manager.py +401 -0
- mcp_hangar/logging_config.py +209 -0
- mcp_hangar/metrics.py +1007 -0
- mcp_hangar/models.py +31 -0
- mcp_hangar/observability/__init__.py +54 -0
- mcp_hangar/observability/health.py +487 -0
- mcp_hangar/observability/metrics.py +319 -0
- mcp_hangar/observability/tracing.py +433 -0
- mcp_hangar/progress.py +542 -0
- mcp_hangar/retry.py +613 -0
- mcp_hangar/server/__init__.py +120 -0
- mcp_hangar/server/__main__.py +6 -0
- mcp_hangar/server/auth_bootstrap.py +340 -0
- mcp_hangar/server/auth_cli.py +335 -0
- mcp_hangar/server/auth_config.py +305 -0
- mcp_hangar/server/bootstrap.py +735 -0
- mcp_hangar/server/cli.py +161 -0
- mcp_hangar/server/config.py +224 -0
- mcp_hangar/server/context.py +215 -0
- mcp_hangar/server/http_auth_middleware.py +165 -0
- mcp_hangar/server/lifecycle.py +467 -0
- mcp_hangar/server/state.py +117 -0
- mcp_hangar/server/tools/__init__.py +16 -0
- mcp_hangar/server/tools/discovery.py +186 -0
- mcp_hangar/server/tools/groups.py +75 -0
- mcp_hangar/server/tools/health.py +301 -0
- mcp_hangar/server/tools/provider.py +939 -0
- mcp_hangar/server/tools/registry.py +320 -0
- mcp_hangar/server/validation.py +113 -0
- mcp_hangar/stdio_client.py +229 -0
- mcp_hangar-0.2.0.dist-info/METADATA +347 -0
- mcp_hangar-0.2.0.dist-info/RECORD +160 -0
- mcp_hangar-0.2.0.dist-info/WHEEL +4 -0
- mcp_hangar-0.2.0.dist-info/entry_points.txt +2 -0
- mcp_hangar-0.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,1105 @@
|
|
|
1
|
+
"""MCP HTTP Server using FastMCP.
|
|
2
|
+
|
|
3
|
+
Provides MCP-over-HTTP with proper dependency injection.
|
|
4
|
+
No global state — all dependencies passed via constructor.
|
|
5
|
+
|
|
6
|
+
Endpoints (HTTP mode):
|
|
7
|
+
- /health : liveness (cheap ping)
|
|
8
|
+
- /ready : readiness (checks internal registry wiring + basic runtime state)
|
|
9
|
+
- /metrics: prometheus metrics
|
|
10
|
+
- /mcp : MCP streamable HTTP endpoint
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
# Recommended: Use MCPServerFactory
|
|
14
|
+
from mcp_hangar.fastmcp_server import MCPServerFactory, RegistryFunctions
|
|
15
|
+
|
|
16
|
+
registry = RegistryFunctions(
|
|
17
|
+
list=my_list_fn,
|
|
18
|
+
start=my_start_fn,
|
|
19
|
+
stop=my_stop_fn,
|
|
20
|
+
invoke=my_invoke_fn,
|
|
21
|
+
tools=my_tools_fn,
|
|
22
|
+
details=my_details_fn,
|
|
23
|
+
health=my_health_fn,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
factory = MCPServerFactory(registry)
|
|
27
|
+
app = factory.create_asgi_app()
|
|
28
|
+
|
|
29
|
+
# Or use the builder pattern:
|
|
30
|
+
factory = (MCPServerFactory.builder()
|
|
31
|
+
.with_registry(list_fn, start_fn, stop_fn, invoke_fn, tools_fn, details_fn, health_fn)
|
|
32
|
+
.with_discovery(discover_fn=discover_fn)
|
|
33
|
+
.with_config(port=9000)
|
|
34
|
+
.build())
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
from dataclasses import dataclass
|
|
38
|
+
from typing import Any, Dict, Optional, Protocol, TYPE_CHECKING
|
|
39
|
+
|
|
40
|
+
from mcp.server.fastmcp import FastMCP
|
|
41
|
+
|
|
42
|
+
from .logging_config import get_logger
|
|
43
|
+
|
|
44
|
+
if TYPE_CHECKING:
|
|
45
|
+
from .server.auth_bootstrap import AuthComponents
|
|
46
|
+
|
|
47
|
+
logger = get_logger(__name__)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# =============================================================================
|
|
51
|
+
# Protocols for Type Safety
|
|
52
|
+
# =============================================================================
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class RegistryListFn(Protocol):
|
|
56
|
+
"""Protocol for registry_list function."""
|
|
57
|
+
|
|
58
|
+
def __call__(self, state_filter: Optional[str] = None) -> Dict[str, Any]:
|
|
59
|
+
"""List all providers with status and metadata."""
|
|
60
|
+
...
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class RegistryStartFn(Protocol):
|
|
64
|
+
"""Protocol for registry_start function."""
|
|
65
|
+
|
|
66
|
+
def __call__(self, provider: str) -> Dict[str, Any]:
|
|
67
|
+
"""Start a provider and discover tools."""
|
|
68
|
+
...
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class RegistryStopFn(Protocol):
|
|
72
|
+
"""Protocol for registry_stop function."""
|
|
73
|
+
|
|
74
|
+
def __call__(self, provider: str) -> Dict[str, Any]:
|
|
75
|
+
"""Stop a provider."""
|
|
76
|
+
...
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class RegistryInvokeFn(Protocol):
|
|
80
|
+
"""Protocol for registry_invoke function."""
|
|
81
|
+
|
|
82
|
+
def __call__(
|
|
83
|
+
self,
|
|
84
|
+
provider: str,
|
|
85
|
+
tool: str,
|
|
86
|
+
arguments: Dict[str, Any],
|
|
87
|
+
timeout: float = 30.0,
|
|
88
|
+
) -> Dict[str, Any]:
|
|
89
|
+
"""Invoke a tool on a provider."""
|
|
90
|
+
...
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class RegistryToolsFn(Protocol):
|
|
94
|
+
"""Protocol for registry_tools function."""
|
|
95
|
+
|
|
96
|
+
def __call__(self, provider: str) -> Dict[str, Any]:
|
|
97
|
+
"""Get tool schemas for a provider."""
|
|
98
|
+
...
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class RegistryDetailsFn(Protocol):
|
|
102
|
+
"""Protocol for registry_details function."""
|
|
103
|
+
|
|
104
|
+
def __call__(self, provider: str) -> Dict[str, Any]:
|
|
105
|
+
"""Get detailed provider information."""
|
|
106
|
+
...
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class RegistryHealthFn(Protocol):
|
|
110
|
+
"""Protocol for registry_health function."""
|
|
111
|
+
|
|
112
|
+
def __call__(self) -> Dict[str, Any]:
|
|
113
|
+
"""Get registry health status."""
|
|
114
|
+
...
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# Discovery protocols (optional)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class RegistryDiscoverFn(Protocol):
|
|
121
|
+
"""Protocol for registry_discover function (async)."""
|
|
122
|
+
|
|
123
|
+
async def __call__(self) -> Dict[str, Any]:
|
|
124
|
+
"""Trigger immediate discovery cycle."""
|
|
125
|
+
...
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class RegistryDiscoveredFn(Protocol):
|
|
129
|
+
"""Protocol for registry_discovered function."""
|
|
130
|
+
|
|
131
|
+
def __call__(self) -> Dict[str, Any]:
|
|
132
|
+
"""List discovered providers pending registration."""
|
|
133
|
+
...
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class RegistryQuarantineFn(Protocol):
|
|
137
|
+
"""Protocol for registry_quarantine function."""
|
|
138
|
+
|
|
139
|
+
def __call__(self) -> Dict[str, Any]:
|
|
140
|
+
"""List quarantined providers."""
|
|
141
|
+
...
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class RegistryApproveFn(Protocol):
|
|
145
|
+
"""Protocol for registry_approve function (async)."""
|
|
146
|
+
|
|
147
|
+
async def __call__(self, provider: str) -> Dict[str, Any]:
|
|
148
|
+
"""Approve a quarantined provider."""
|
|
149
|
+
...
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class RegistrySourcesFn(Protocol):
|
|
153
|
+
"""Protocol for registry_sources function."""
|
|
154
|
+
|
|
155
|
+
def __call__(self) -> Dict[str, Any]:
|
|
156
|
+
"""List discovery sources with status."""
|
|
157
|
+
...
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class RegistryMetricsFn(Protocol):
|
|
161
|
+
"""Protocol for registry_metrics function."""
|
|
162
|
+
|
|
163
|
+
def __call__(self, format: str = "summary") -> Dict[str, Any]:
|
|
164
|
+
"""Get registry metrics."""
|
|
165
|
+
...
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# =============================================================================
|
|
169
|
+
# Configuration
|
|
170
|
+
# =============================================================================
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@dataclass(frozen=True)
|
|
174
|
+
class RegistryFunctions:
|
|
175
|
+
"""Container for all registry function dependencies.
|
|
176
|
+
|
|
177
|
+
Core functions are required. Discovery functions are optional
|
|
178
|
+
and will return appropriate errors if not provided.
|
|
179
|
+
|
|
180
|
+
Attributes:
|
|
181
|
+
list: Function to list all providers.
|
|
182
|
+
start: Function to start a provider.
|
|
183
|
+
stop: Function to stop a provider.
|
|
184
|
+
invoke: Function to invoke a tool on a provider.
|
|
185
|
+
tools: Function to get tool schemas.
|
|
186
|
+
details: Function to get provider details.
|
|
187
|
+
health: Function to get registry health.
|
|
188
|
+
discover: Optional async function to trigger discovery.
|
|
189
|
+
discovered: Optional function to list discovered providers.
|
|
190
|
+
quarantine: Optional function to list quarantined providers.
|
|
191
|
+
approve: Optional async function to approve a quarantined provider.
|
|
192
|
+
sources: Optional function to list discovery sources.
|
|
193
|
+
metrics: Optional function to get registry metrics.
|
|
194
|
+
"""
|
|
195
|
+
|
|
196
|
+
# Core (required)
|
|
197
|
+
list: RegistryListFn
|
|
198
|
+
start: RegistryStartFn
|
|
199
|
+
stop: RegistryStopFn
|
|
200
|
+
invoke: RegistryInvokeFn
|
|
201
|
+
tools: RegistryToolsFn
|
|
202
|
+
details: RegistryDetailsFn
|
|
203
|
+
health: RegistryHealthFn
|
|
204
|
+
|
|
205
|
+
# Discovery (optional)
|
|
206
|
+
discover: Optional[RegistryDiscoverFn] = None
|
|
207
|
+
discovered: Optional[RegistryDiscoveredFn] = None
|
|
208
|
+
quarantine: Optional[RegistryQuarantineFn] = None
|
|
209
|
+
approve: Optional[RegistryApproveFn] = None
|
|
210
|
+
sources: Optional[RegistrySourcesFn] = None
|
|
211
|
+
metrics: Optional[RegistryMetricsFn] = None
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@dataclass(frozen=True)
|
|
215
|
+
class ServerConfig:
|
|
216
|
+
"""HTTP server configuration.
|
|
217
|
+
|
|
218
|
+
Attributes:
|
|
219
|
+
host: Host to bind to.
|
|
220
|
+
port: Port to bind to.
|
|
221
|
+
streamable_http_path: Path for MCP streamable HTTP endpoint.
|
|
222
|
+
sse_path: Path for SSE endpoint.
|
|
223
|
+
message_path: Path for message endpoint.
|
|
224
|
+
auth_enabled: Whether authentication is enabled (opt-in, default False).
|
|
225
|
+
auth_skip_paths: Paths to skip authentication (health, metrics, etc.).
|
|
226
|
+
trusted_proxies: Set of trusted proxy IPs for X-Forwarded-For.
|
|
227
|
+
"""
|
|
228
|
+
|
|
229
|
+
host: str = "0.0.0.0"
|
|
230
|
+
port: int = 8000
|
|
231
|
+
streamable_http_path: str = "/mcp"
|
|
232
|
+
sse_path: str = "/sse"
|
|
233
|
+
message_path: str = "/messages/"
|
|
234
|
+
# Auth configuration (opt-in)
|
|
235
|
+
auth_enabled: bool = False
|
|
236
|
+
auth_skip_paths: tuple[str, ...] = ("/health", "/ready", "/_ready", "/metrics")
|
|
237
|
+
trusted_proxies: frozenset[str] = frozenset(["127.0.0.1", "::1"])
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
# =============================================================================
|
|
241
|
+
# Factory
|
|
242
|
+
# =============================================================================
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
class MCPServerFactory:
|
|
246
|
+
"""Factory for creating configured FastMCP servers.
|
|
247
|
+
|
|
248
|
+
This factory encapsulates all dependencies needed to create an MCP server,
|
|
249
|
+
enabling proper dependency injection and testability.
|
|
250
|
+
|
|
251
|
+
Usage:
|
|
252
|
+
# Direct instantiation
|
|
253
|
+
factory = MCPServerFactory(registry_functions)
|
|
254
|
+
mcp = factory.create_server()
|
|
255
|
+
app = factory.create_asgi_app()
|
|
256
|
+
|
|
257
|
+
# With authentication (opt-in)
|
|
258
|
+
factory = MCPServerFactory(
|
|
259
|
+
registry_functions,
|
|
260
|
+
auth_components=auth_components,
|
|
261
|
+
config=ServerConfig(auth_enabled=True),
|
|
262
|
+
)
|
|
263
|
+
app = factory.create_asgi_app()
|
|
264
|
+
|
|
265
|
+
# Or use the builder pattern
|
|
266
|
+
factory = (MCPServerFactory.builder()
|
|
267
|
+
.with_registry(list_fn, start_fn, ...)
|
|
268
|
+
.with_discovery(discover_fn, ...)
|
|
269
|
+
.with_auth(auth_components)
|
|
270
|
+
.with_config(host="0.0.0.0", port=9000, auth_enabled=True)
|
|
271
|
+
.build())
|
|
272
|
+
"""
|
|
273
|
+
|
|
274
|
+
def __init__(
|
|
275
|
+
self,
|
|
276
|
+
registry: RegistryFunctions,
|
|
277
|
+
config: Optional[ServerConfig] = None,
|
|
278
|
+
auth_components: Optional["AuthComponents"] = None,
|
|
279
|
+
):
|
|
280
|
+
"""Initialize factory with dependencies.
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
registry: Registry function implementations.
|
|
284
|
+
config: Server configuration (uses defaults if None).
|
|
285
|
+
auth_components: Optional auth components for authentication/authorization.
|
|
286
|
+
"""
|
|
287
|
+
self._registry = registry
|
|
288
|
+
self._config = config or ServerConfig()
|
|
289
|
+
self._auth_components = auth_components
|
|
290
|
+
self._mcp: Optional[FastMCP] = None
|
|
291
|
+
|
|
292
|
+
@classmethod
|
|
293
|
+
def builder(cls) -> "MCPServerFactoryBuilder":
|
|
294
|
+
"""Create a builder for fluent configuration.
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
MCPServerFactoryBuilder instance.
|
|
298
|
+
"""
|
|
299
|
+
return MCPServerFactoryBuilder()
|
|
300
|
+
|
|
301
|
+
@property
|
|
302
|
+
def registry(self) -> RegistryFunctions:
|
|
303
|
+
"""Get the registry functions."""
|
|
304
|
+
return self._registry
|
|
305
|
+
|
|
306
|
+
@property
|
|
307
|
+
def config(self) -> ServerConfig:
|
|
308
|
+
"""Get the server configuration."""
|
|
309
|
+
return self._config
|
|
310
|
+
|
|
311
|
+
def create_server(self) -> FastMCP:
|
|
312
|
+
"""Create and configure FastMCP server instance.
|
|
313
|
+
|
|
314
|
+
The server is cached — repeated calls return the same instance.
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
Configured FastMCP server with all tools registered.
|
|
318
|
+
"""
|
|
319
|
+
if self._mcp is not None:
|
|
320
|
+
return self._mcp
|
|
321
|
+
|
|
322
|
+
mcp = FastMCP(
|
|
323
|
+
name="mcp-registry",
|
|
324
|
+
host=self._config.host,
|
|
325
|
+
port=self._config.port,
|
|
326
|
+
streamable_http_path=self._config.streamable_http_path,
|
|
327
|
+
sse_path=self._config.sse_path,
|
|
328
|
+
message_path=self._config.message_path,
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
self._register_core_tools(mcp)
|
|
332
|
+
self._register_discovery_tools(mcp)
|
|
333
|
+
|
|
334
|
+
self._mcp = mcp
|
|
335
|
+
logger.info(
|
|
336
|
+
"fastmcp_server_created",
|
|
337
|
+
host=self._config.host,
|
|
338
|
+
port=self._config.port,
|
|
339
|
+
discovery_enabled=self._registry.discover is not None,
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
return mcp
|
|
343
|
+
|
|
344
|
+
def create_asgi_app(self):
|
|
345
|
+
"""Create ASGI application with metrics/health endpoints.
|
|
346
|
+
|
|
347
|
+
Creates a combined ASGI app that handles:
|
|
348
|
+
- /health: Liveness endpoint
|
|
349
|
+
- /ready: Readiness endpoint with internal checks
|
|
350
|
+
- /metrics: Prometheus metrics
|
|
351
|
+
- /mcp: MCP streamable HTTP endpoint (and related paths)
|
|
352
|
+
|
|
353
|
+
If auth is enabled (config.auth_enabled=True and auth_components provided),
|
|
354
|
+
the auth middleware will be applied to protect MCP endpoints.
|
|
355
|
+
|
|
356
|
+
Returns:
|
|
357
|
+
Combined ASGI app callable.
|
|
358
|
+
"""
|
|
359
|
+
from starlette.applications import Starlette
|
|
360
|
+
from starlette.responses import JSONResponse, PlainTextResponse
|
|
361
|
+
from starlette.routing import Route
|
|
362
|
+
|
|
363
|
+
from .metrics import get_metrics
|
|
364
|
+
|
|
365
|
+
mcp = self.create_server()
|
|
366
|
+
mcp_app = mcp.streamable_http_app()
|
|
367
|
+
|
|
368
|
+
# Log if auth is configured (actual wrapping happens in _create_auth_combined_app)
|
|
369
|
+
if self._config.auth_enabled and self._auth_components:
|
|
370
|
+
|
|
371
|
+
logger.info(
|
|
372
|
+
"auth_middleware_enabled",
|
|
373
|
+
skip_paths=self._config.auth_skip_paths,
|
|
374
|
+
trusted_proxies=list(self._config.trusted_proxies),
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
# Health endpoint (liveness)
|
|
378
|
+
async def health_endpoint(request):
|
|
379
|
+
"""Liveness endpoint (cheap ping)."""
|
|
380
|
+
return JSONResponse({"status": "ok", "service": "mcp-registry"})
|
|
381
|
+
|
|
382
|
+
# Readiness endpoint
|
|
383
|
+
async def ready_endpoint(request):
|
|
384
|
+
"""Readiness endpoint with internal checks."""
|
|
385
|
+
checks = self._run_readiness_checks()
|
|
386
|
+
ready = all(v is True for k, v in checks.items() if isinstance(v, bool))
|
|
387
|
+
return JSONResponse(
|
|
388
|
+
{"ready": ready, "service": "mcp-registry", "checks": checks},
|
|
389
|
+
status_code=200 if ready else 503,
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
# Metrics endpoint
|
|
393
|
+
async def metrics_endpoint(request):
|
|
394
|
+
"""Prometheus metrics endpoint."""
|
|
395
|
+
self._update_metrics()
|
|
396
|
+
return PlainTextResponse(
|
|
397
|
+
get_metrics(),
|
|
398
|
+
media_type="text/plain; version=0.0.4; charset=utf-8",
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
routes = [
|
|
402
|
+
Route("/health", health_endpoint, methods=["GET"]),
|
|
403
|
+
Route("/ready", ready_endpoint, methods=["GET"]),
|
|
404
|
+
Route("/metrics", metrics_endpoint, methods=["GET"]),
|
|
405
|
+
]
|
|
406
|
+
|
|
407
|
+
aux_app = Starlette(routes=routes)
|
|
408
|
+
|
|
409
|
+
# Create auth-aware combined app
|
|
410
|
+
if self._config.auth_enabled and self._auth_components:
|
|
411
|
+
combined_app = self._create_auth_combined_app(aux_app, mcp_app)
|
|
412
|
+
else:
|
|
413
|
+
|
|
414
|
+
async def combined_app(scope, receive, send):
|
|
415
|
+
"""Combined ASGI app that routes to metrics/health or MCP."""
|
|
416
|
+
if scope["type"] == "http":
|
|
417
|
+
path = scope.get("path", "")
|
|
418
|
+
if path in ("/health", "/ready", "/metrics"):
|
|
419
|
+
await aux_app(scope, receive, send)
|
|
420
|
+
return
|
|
421
|
+
await mcp_app(scope, receive, send)
|
|
422
|
+
|
|
423
|
+
return combined_app
|
|
424
|
+
|
|
425
|
+
def _create_auth_combined_app(self, aux_app, mcp_app):
|
|
426
|
+
"""Create auth-enabled combined ASGI app.
|
|
427
|
+
|
|
428
|
+
This wraps the MCP app with authentication middleware while
|
|
429
|
+
keeping health/metrics endpoints unprotected.
|
|
430
|
+
|
|
431
|
+
Args:
|
|
432
|
+
aux_app: Starlette app for health/metrics endpoints.
|
|
433
|
+
mcp_app: FastMCP ASGI app.
|
|
434
|
+
|
|
435
|
+
Returns:
|
|
436
|
+
Combined ASGI app with auth middleware.
|
|
437
|
+
"""
|
|
438
|
+
from starlette.responses import JSONResponse
|
|
439
|
+
|
|
440
|
+
from .domain.contracts.authentication import AuthRequest
|
|
441
|
+
from .domain.exceptions import AccessDeniedError, AuthenticationError
|
|
442
|
+
|
|
443
|
+
auth_components = self._auth_components
|
|
444
|
+
skip_paths = set(self._config.auth_skip_paths)
|
|
445
|
+
trusted_proxies = self._config.trusted_proxies
|
|
446
|
+
|
|
447
|
+
async def auth_combined_app(scope, receive, send):
|
|
448
|
+
"""Combined ASGI app with authentication for MCP endpoints."""
|
|
449
|
+
if scope["type"] != "http":
|
|
450
|
+
# Non-HTTP (e.g., lifespan) - pass through
|
|
451
|
+
await mcp_app(scope, receive, send)
|
|
452
|
+
return
|
|
453
|
+
|
|
454
|
+
path = scope.get("path", "")
|
|
455
|
+
|
|
456
|
+
# Skip auth for health/metrics endpoints
|
|
457
|
+
if path in skip_paths:
|
|
458
|
+
await aux_app(scope, receive, send)
|
|
459
|
+
return
|
|
460
|
+
|
|
461
|
+
# For MCP endpoints, apply authentication
|
|
462
|
+
# Build headers dict from scope
|
|
463
|
+
headers = {}
|
|
464
|
+
for key, value in scope.get("headers", []):
|
|
465
|
+
headers[key.decode("latin-1").lower()] = value.decode("latin-1")
|
|
466
|
+
|
|
467
|
+
# Get client IP
|
|
468
|
+
client = scope.get("client")
|
|
469
|
+
source_ip = client[0] if client else "unknown"
|
|
470
|
+
|
|
471
|
+
# Trust X-Forwarded-For only from trusted proxies
|
|
472
|
+
if source_ip in trusted_proxies:
|
|
473
|
+
forwarded_for = headers.get("x-forwarded-for")
|
|
474
|
+
if forwarded_for:
|
|
475
|
+
source_ip = forwarded_for.split(",")[0].strip()
|
|
476
|
+
|
|
477
|
+
# Create auth request
|
|
478
|
+
auth_request = AuthRequest(
|
|
479
|
+
headers=headers,
|
|
480
|
+
source_ip=source_ip,
|
|
481
|
+
method=scope.get("method", ""),
|
|
482
|
+
path=path,
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
try:
|
|
486
|
+
# Authenticate
|
|
487
|
+
auth_context = auth_components.authn_middleware.authenticate(auth_request)
|
|
488
|
+
|
|
489
|
+
# Store auth context in scope for downstream handlers
|
|
490
|
+
scope["auth"] = auth_context
|
|
491
|
+
|
|
492
|
+
# Pass to MCP app
|
|
493
|
+
await mcp_app(scope, receive, send)
|
|
494
|
+
|
|
495
|
+
except AuthenticationError as e:
|
|
496
|
+
response = JSONResponse(
|
|
497
|
+
status_code=401,
|
|
498
|
+
content={
|
|
499
|
+
"error": "authentication_failed",
|
|
500
|
+
"message": e.message,
|
|
501
|
+
},
|
|
502
|
+
headers={"WWW-Authenticate": "Bearer, ApiKey"},
|
|
503
|
+
)
|
|
504
|
+
await response(scope, receive, send)
|
|
505
|
+
|
|
506
|
+
except AccessDeniedError as e:
|
|
507
|
+
response = JSONResponse(
|
|
508
|
+
status_code=403,
|
|
509
|
+
content={
|
|
510
|
+
"error": "access_denied",
|
|
511
|
+
"message": str(e),
|
|
512
|
+
},
|
|
513
|
+
)
|
|
514
|
+
await response(scope, receive, send)
|
|
515
|
+
|
|
516
|
+
return auth_combined_app
|
|
517
|
+
|
|
518
|
+
def _register_core_tools(self, mcp: FastMCP) -> None:
|
|
519
|
+
"""Register core registry tools.
|
|
520
|
+
|
|
521
|
+
Args:
|
|
522
|
+
mcp: FastMCP server instance.
|
|
523
|
+
"""
|
|
524
|
+
reg = self._registry
|
|
525
|
+
|
|
526
|
+
@mcp.tool()
|
|
527
|
+
def registry_list(state_filter: str = None) -> dict:
|
|
528
|
+
"""List all providers with status and metadata.
|
|
529
|
+
|
|
530
|
+
Args:
|
|
531
|
+
state_filter: Optional filter by state (cold, ready, degraded, dead)
|
|
532
|
+
"""
|
|
533
|
+
return reg.list(state_filter=state_filter)
|
|
534
|
+
|
|
535
|
+
@mcp.tool()
|
|
536
|
+
def registry_start(provider: str) -> dict:
|
|
537
|
+
"""Explicitly start a provider and discover tools.
|
|
538
|
+
|
|
539
|
+
Args:
|
|
540
|
+
provider: Provider ID to start
|
|
541
|
+
"""
|
|
542
|
+
return reg.start(provider=provider)
|
|
543
|
+
|
|
544
|
+
@mcp.tool()
|
|
545
|
+
def registry_stop(provider: str) -> dict:
|
|
546
|
+
"""Stop a provider.
|
|
547
|
+
|
|
548
|
+
Args:
|
|
549
|
+
provider: Provider ID to stop
|
|
550
|
+
"""
|
|
551
|
+
return reg.stop(provider=provider)
|
|
552
|
+
|
|
553
|
+
@mcp.tool()
|
|
554
|
+
def registry_invoke(
|
|
555
|
+
provider: str,
|
|
556
|
+
tool: str,
|
|
557
|
+
arguments: Optional[dict] = None,
|
|
558
|
+
timeout: float = 30.0,
|
|
559
|
+
) -> dict:
|
|
560
|
+
"""Invoke a tool on a provider.
|
|
561
|
+
|
|
562
|
+
Args:
|
|
563
|
+
provider: Provider ID
|
|
564
|
+
tool: Tool name to invoke
|
|
565
|
+
arguments: Tool arguments as dictionary (default: empty)
|
|
566
|
+
timeout: Timeout in seconds (default 30)
|
|
567
|
+
"""
|
|
568
|
+
return reg.invoke(
|
|
569
|
+
provider=provider,
|
|
570
|
+
tool=tool,
|
|
571
|
+
arguments=arguments or {},
|
|
572
|
+
timeout=timeout,
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
@mcp.tool()
|
|
576
|
+
def registry_tools(provider: str) -> dict:
|
|
577
|
+
"""Get detailed tool schemas for a provider.
|
|
578
|
+
|
|
579
|
+
Args:
|
|
580
|
+
provider: Provider ID
|
|
581
|
+
"""
|
|
582
|
+
return reg.tools(provider=provider)
|
|
583
|
+
|
|
584
|
+
@mcp.tool()
|
|
585
|
+
def registry_details(provider: str) -> dict:
|
|
586
|
+
"""Get detailed information about a provider.
|
|
587
|
+
|
|
588
|
+
Args:
|
|
589
|
+
provider: Provider ID
|
|
590
|
+
"""
|
|
591
|
+
return reg.details(provider=provider)
|
|
592
|
+
|
|
593
|
+
@mcp.tool()
|
|
594
|
+
def registry_health() -> dict:
|
|
595
|
+
"""Get registry health status including provider counts and metrics."""
|
|
596
|
+
return reg.health()
|
|
597
|
+
|
|
598
|
+
def _register_discovery_tools(self, mcp: FastMCP) -> None:
|
|
599
|
+
"""Register discovery tools (if enabled).
|
|
600
|
+
|
|
601
|
+
Args:
|
|
602
|
+
mcp: FastMCP server instance.
|
|
603
|
+
"""
|
|
604
|
+
reg = self._registry
|
|
605
|
+
|
|
606
|
+
@mcp.tool()
|
|
607
|
+
async def registry_discover() -> dict:
|
|
608
|
+
"""Trigger immediate discovery cycle.
|
|
609
|
+
|
|
610
|
+
Runs discovery across all configured sources and returns
|
|
611
|
+
statistics about discovered, registered, and quarantined providers.
|
|
612
|
+
"""
|
|
613
|
+
if reg.discover is None:
|
|
614
|
+
return {"error": "Discovery not configured"}
|
|
615
|
+
return await reg.discover()
|
|
616
|
+
|
|
617
|
+
@mcp.tool()
|
|
618
|
+
def registry_discovered() -> dict:
|
|
619
|
+
"""List all discovered providers pending registration.
|
|
620
|
+
|
|
621
|
+
Shows providers found by discovery but not yet registered,
|
|
622
|
+
typically due to auto_register=false or pending approval.
|
|
623
|
+
"""
|
|
624
|
+
if reg.discovered is None:
|
|
625
|
+
return {"error": "Discovery not configured"}
|
|
626
|
+
return reg.discovered()
|
|
627
|
+
|
|
628
|
+
@mcp.tool()
|
|
629
|
+
def registry_quarantine() -> dict:
|
|
630
|
+
"""List quarantined providers with failure reasons.
|
|
631
|
+
|
|
632
|
+
Shows providers that failed validation and are waiting
|
|
633
|
+
for manual approval or rejection.
|
|
634
|
+
"""
|
|
635
|
+
if reg.quarantine is None:
|
|
636
|
+
return {"error": "Discovery not configured"}
|
|
637
|
+
return reg.quarantine()
|
|
638
|
+
|
|
639
|
+
@mcp.tool()
|
|
640
|
+
async def registry_approve(provider: str) -> dict:
|
|
641
|
+
"""Approve a quarantined provider for registration.
|
|
642
|
+
|
|
643
|
+
Args:
|
|
644
|
+
provider: Name of the quarantined provider to approve
|
|
645
|
+
"""
|
|
646
|
+
if reg.approve is None:
|
|
647
|
+
return {"error": "Discovery not configured"}
|
|
648
|
+
return await reg.approve(provider=provider)
|
|
649
|
+
|
|
650
|
+
@mcp.tool()
|
|
651
|
+
def registry_sources() -> dict:
|
|
652
|
+
"""List configured discovery sources with health status.
|
|
653
|
+
|
|
654
|
+
Shows all discovery sources (kubernetes, docker, filesystem, entrypoint)
|
|
655
|
+
with their current health and last discovery timestamp.
|
|
656
|
+
"""
|
|
657
|
+
if reg.sources is None:
|
|
658
|
+
return {"error": "Discovery not configured"}
|
|
659
|
+
return reg.sources()
|
|
660
|
+
|
|
661
|
+
@mcp.tool()
|
|
662
|
+
def registry_metrics(format: str = "summary") -> dict:
|
|
663
|
+
"""Get registry metrics and statistics.
|
|
664
|
+
|
|
665
|
+
Args:
|
|
666
|
+
format: Output format - "summary" (default), "prometheus", or "detailed"
|
|
667
|
+
|
|
668
|
+
Returns metrics including provider states, tool call counts, errors,
|
|
669
|
+
discovery statistics, and performance data.
|
|
670
|
+
"""
|
|
671
|
+
if reg.metrics is None:
|
|
672
|
+
return {"error": "Metrics not available"}
|
|
673
|
+
return reg.metrics(format=format)
|
|
674
|
+
|
|
675
|
+
def _run_readiness_checks(self) -> Dict[str, Any]:
|
|
676
|
+
"""Run readiness checks.
|
|
677
|
+
|
|
678
|
+
Returns:
|
|
679
|
+
Dictionary of check names to results.
|
|
680
|
+
"""
|
|
681
|
+
checks: Dict[str, Any] = {}
|
|
682
|
+
|
|
683
|
+
# Check registry wiring
|
|
684
|
+
checks["registry_wired"] = True
|
|
685
|
+
|
|
686
|
+
# Check registry list
|
|
687
|
+
try:
|
|
688
|
+
data = self._registry.list()
|
|
689
|
+
checks["registry_list_ok"] = isinstance(data, dict) and "providers" in data
|
|
690
|
+
except Exception as e:
|
|
691
|
+
checks["registry_list_ok"] = False
|
|
692
|
+
checks["registry_list_error"] = str(e)
|
|
693
|
+
|
|
694
|
+
# Check registry health
|
|
695
|
+
try:
|
|
696
|
+
h = self._registry.health()
|
|
697
|
+
checks["registry_health_ok"] = isinstance(h, dict) and "status" in h
|
|
698
|
+
except Exception as e:
|
|
699
|
+
checks["registry_health_ok"] = False
|
|
700
|
+
checks["registry_health_error"] = str(e)
|
|
701
|
+
|
|
702
|
+
return checks
|
|
703
|
+
|
|
704
|
+
def _update_metrics(self) -> None:
|
|
705
|
+
"""Update provider state metrics."""
|
|
706
|
+
from .metrics import update_provider_state
|
|
707
|
+
|
|
708
|
+
try:
|
|
709
|
+
data = self._registry.list()
|
|
710
|
+
if isinstance(data, dict) and "providers" in data:
|
|
711
|
+
for p in data.get("providers", []):
|
|
712
|
+
pid = p.get("provider_id") or p.get("name") or p.get("id")
|
|
713
|
+
if pid:
|
|
714
|
+
update_provider_state(
|
|
715
|
+
pid,
|
|
716
|
+
p.get("state", "cold"),
|
|
717
|
+
p.get("mode", "subprocess"),
|
|
718
|
+
)
|
|
719
|
+
except Exception as e:
|
|
720
|
+
logger.debug("metrics_update_failed", error=str(e))
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
# =============================================================================
|
|
724
|
+
# Builder (Optional Fluent API)
|
|
725
|
+
# =============================================================================
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
class MCPServerFactoryBuilder:
|
|
729
|
+
"""Builder for MCPServerFactory with fluent API.
|
|
730
|
+
|
|
731
|
+
Provides a convenient way to construct an MCPServerFactory
|
|
732
|
+
with optional components.
|
|
733
|
+
|
|
734
|
+
Usage:
|
|
735
|
+
factory = (MCPServerFactory.builder()
|
|
736
|
+
.with_registry(list_fn, start_fn, stop_fn, invoke_fn, tools_fn, details_fn, health_fn)
|
|
737
|
+
.with_discovery(discover_fn=my_discover)
|
|
738
|
+
.with_config(port=9000)
|
|
739
|
+
.build())
|
|
740
|
+
"""
|
|
741
|
+
|
|
742
|
+
def __init__(self):
|
|
743
|
+
"""Initialize builder with empty state."""
|
|
744
|
+
self._list_fn: Optional[RegistryListFn] = None
|
|
745
|
+
self._start_fn: Optional[RegistryStartFn] = None
|
|
746
|
+
self._stop_fn: Optional[RegistryStopFn] = None
|
|
747
|
+
self._invoke_fn: Optional[RegistryInvokeFn] = None
|
|
748
|
+
self._tools_fn: Optional[RegistryToolsFn] = None
|
|
749
|
+
self._details_fn: Optional[RegistryDetailsFn] = None
|
|
750
|
+
self._health_fn: Optional[RegistryHealthFn] = None
|
|
751
|
+
|
|
752
|
+
self._discover_fn: Optional[RegistryDiscoverFn] = None
|
|
753
|
+
self._discovered_fn: Optional[RegistryDiscoveredFn] = None
|
|
754
|
+
self._quarantine_fn: Optional[RegistryQuarantineFn] = None
|
|
755
|
+
self._approve_fn: Optional[RegistryApproveFn] = None
|
|
756
|
+
self._sources_fn: Optional[RegistrySourcesFn] = None
|
|
757
|
+
self._metrics_fn: Optional[RegistryMetricsFn] = None
|
|
758
|
+
|
|
759
|
+
self._config: Optional[ServerConfig] = None
|
|
760
|
+
self._auth_components: Optional["AuthComponents"] = None
|
|
761
|
+
|
|
762
|
+
def with_registry(
|
|
763
|
+
self,
|
|
764
|
+
list_fn: RegistryListFn,
|
|
765
|
+
start_fn: RegistryStartFn,
|
|
766
|
+
stop_fn: RegistryStopFn,
|
|
767
|
+
invoke_fn: RegistryInvokeFn,
|
|
768
|
+
tools_fn: RegistryToolsFn,
|
|
769
|
+
details_fn: RegistryDetailsFn,
|
|
770
|
+
health_fn: RegistryHealthFn,
|
|
771
|
+
) -> "MCPServerFactoryBuilder":
|
|
772
|
+
"""Set core registry functions.
|
|
773
|
+
|
|
774
|
+
Args:
|
|
775
|
+
list_fn: Function to list providers.
|
|
776
|
+
start_fn: Function to start a provider.
|
|
777
|
+
stop_fn: Function to stop a provider.
|
|
778
|
+
invoke_fn: Function to invoke a tool.
|
|
779
|
+
tools_fn: Function to get tool schemas.
|
|
780
|
+
details_fn: Function to get provider details.
|
|
781
|
+
health_fn: Function to get registry health.
|
|
782
|
+
|
|
783
|
+
Returns:
|
|
784
|
+
Self for chaining.
|
|
785
|
+
"""
|
|
786
|
+
self._list_fn = list_fn
|
|
787
|
+
self._start_fn = start_fn
|
|
788
|
+
self._stop_fn = stop_fn
|
|
789
|
+
self._invoke_fn = invoke_fn
|
|
790
|
+
self._tools_fn = tools_fn
|
|
791
|
+
self._details_fn = details_fn
|
|
792
|
+
self._health_fn = health_fn
|
|
793
|
+
return self
|
|
794
|
+
|
|
795
|
+
def with_discovery(
|
|
796
|
+
self,
|
|
797
|
+
discover_fn: Optional[RegistryDiscoverFn] = None,
|
|
798
|
+
discovered_fn: Optional[RegistryDiscoveredFn] = None,
|
|
799
|
+
quarantine_fn: Optional[RegistryQuarantineFn] = None,
|
|
800
|
+
approve_fn: Optional[RegistryApproveFn] = None,
|
|
801
|
+
sources_fn: Optional[RegistrySourcesFn] = None,
|
|
802
|
+
metrics_fn: Optional[RegistryMetricsFn] = None,
|
|
803
|
+
) -> "MCPServerFactoryBuilder":
|
|
804
|
+
"""Set discovery functions (all optional).
|
|
805
|
+
|
|
806
|
+
Args:
|
|
807
|
+
discover_fn: Async function to trigger discovery.
|
|
808
|
+
discovered_fn: Function to list discovered providers.
|
|
809
|
+
quarantine_fn: Function to list quarantined providers.
|
|
810
|
+
approve_fn: Async function to approve a provider.
|
|
811
|
+
sources_fn: Function to list discovery sources.
|
|
812
|
+
metrics_fn: Function to get metrics.
|
|
813
|
+
|
|
814
|
+
Returns:
|
|
815
|
+
Self for chaining.
|
|
816
|
+
"""
|
|
817
|
+
self._discover_fn = discover_fn
|
|
818
|
+
self._discovered_fn = discovered_fn
|
|
819
|
+
self._quarantine_fn = quarantine_fn
|
|
820
|
+
self._approve_fn = approve_fn
|
|
821
|
+
self._sources_fn = sources_fn
|
|
822
|
+
self._metrics_fn = metrics_fn
|
|
823
|
+
return self
|
|
824
|
+
|
|
825
|
+
def with_config(
|
|
826
|
+
self,
|
|
827
|
+
host: str = "0.0.0.0",
|
|
828
|
+
port: int = 8000,
|
|
829
|
+
streamable_http_path: str = "/mcp",
|
|
830
|
+
sse_path: str = "/sse",
|
|
831
|
+
message_path: str = "/messages/",
|
|
832
|
+
auth_enabled: bool = False,
|
|
833
|
+
auth_skip_paths: tuple[str, ...] = ("/health", "/ready", "/_ready", "/metrics"),
|
|
834
|
+
trusted_proxies: frozenset[str] = frozenset(["127.0.0.1", "::1"]),
|
|
835
|
+
) -> "MCPServerFactoryBuilder":
|
|
836
|
+
"""Set server configuration.
|
|
837
|
+
|
|
838
|
+
Args:
|
|
839
|
+
host: Host to bind to.
|
|
840
|
+
port: Port to bind to.
|
|
841
|
+
streamable_http_path: Path for MCP streamable HTTP endpoint.
|
|
842
|
+
sse_path: Path for SSE endpoint.
|
|
843
|
+
message_path: Path for message endpoint.
|
|
844
|
+
auth_enabled: Whether to enable authentication (default: False).
|
|
845
|
+
auth_skip_paths: Paths to skip authentication.
|
|
846
|
+
trusted_proxies: Trusted proxy IPs for X-Forwarded-For.
|
|
847
|
+
|
|
848
|
+
Returns:
|
|
849
|
+
Self for chaining.
|
|
850
|
+
"""
|
|
851
|
+
self._config = ServerConfig(
|
|
852
|
+
host=host,
|
|
853
|
+
port=port,
|
|
854
|
+
streamable_http_path=streamable_http_path,
|
|
855
|
+
sse_path=sse_path,
|
|
856
|
+
message_path=message_path,
|
|
857
|
+
auth_enabled=auth_enabled,
|
|
858
|
+
auth_skip_paths=auth_skip_paths,
|
|
859
|
+
trusted_proxies=trusted_proxies,
|
|
860
|
+
)
|
|
861
|
+
return self
|
|
862
|
+
|
|
863
|
+
def with_auth(
|
|
864
|
+
self,
|
|
865
|
+
auth_components: "AuthComponents",
|
|
866
|
+
) -> "MCPServerFactoryBuilder":
|
|
867
|
+
"""Set authentication components.
|
|
868
|
+
|
|
869
|
+
Args:
|
|
870
|
+
auth_components: Auth components from bootstrap_auth().
|
|
871
|
+
|
|
872
|
+
Returns:
|
|
873
|
+
Self for chaining.
|
|
874
|
+
|
|
875
|
+
Note:
|
|
876
|
+
You also need to set auth_enabled=True in with_config() for
|
|
877
|
+
authentication to be active.
|
|
878
|
+
"""
|
|
879
|
+
self._auth_components = auth_components
|
|
880
|
+
return self
|
|
881
|
+
|
|
882
|
+
def build(self) -> MCPServerFactory:
|
|
883
|
+
"""Build the factory.
|
|
884
|
+
|
|
885
|
+
Returns:
|
|
886
|
+
Configured MCPServerFactory instance.
|
|
887
|
+
|
|
888
|
+
Raises:
|
|
889
|
+
ValueError: If required registry functions not provided.
|
|
890
|
+
"""
|
|
891
|
+
if not all(
|
|
892
|
+
[
|
|
893
|
+
self._list_fn,
|
|
894
|
+
self._start_fn,
|
|
895
|
+
self._stop_fn,
|
|
896
|
+
self._invoke_fn,
|
|
897
|
+
self._tools_fn,
|
|
898
|
+
self._details_fn,
|
|
899
|
+
self._health_fn,
|
|
900
|
+
]
|
|
901
|
+
):
|
|
902
|
+
raise ValueError("All core registry functions must be provided via with_registry()")
|
|
903
|
+
|
|
904
|
+
registry = RegistryFunctions(
|
|
905
|
+
list=self._list_fn,
|
|
906
|
+
start=self._start_fn,
|
|
907
|
+
stop=self._stop_fn,
|
|
908
|
+
invoke=self._invoke_fn,
|
|
909
|
+
tools=self._tools_fn,
|
|
910
|
+
details=self._details_fn,
|
|
911
|
+
health=self._health_fn,
|
|
912
|
+
discover=self._discover_fn,
|
|
913
|
+
discovered=self._discovered_fn,
|
|
914
|
+
quarantine=self._quarantine_fn,
|
|
915
|
+
approve=self._approve_fn,
|
|
916
|
+
sources=self._sources_fn,
|
|
917
|
+
metrics=self._metrics_fn,
|
|
918
|
+
)
|
|
919
|
+
|
|
920
|
+
return MCPServerFactory(registry, self._config, self._auth_components)
|
|
921
|
+
|
|
922
|
+
|
|
923
|
+
# =============================================================================
|
|
924
|
+
# Backward Compatibility Layer
|
|
925
|
+
# DEPRECATED: Will be removed in v0.3.0
|
|
926
|
+
# =============================================================================
|
|
927
|
+
|
|
928
|
+
_compat_factory: Optional[MCPServerFactory] = None
|
|
929
|
+
|
|
930
|
+
|
|
931
|
+
def setup_fastmcp_server(
|
|
932
|
+
registry_list_fn,
|
|
933
|
+
registry_start_fn,
|
|
934
|
+
registry_stop_fn,
|
|
935
|
+
registry_tools_fn,
|
|
936
|
+
registry_invoke_fn,
|
|
937
|
+
registry_details_fn,
|
|
938
|
+
registry_health_fn,
|
|
939
|
+
# Discovery functions (optional)
|
|
940
|
+
registry_discover_fn=None,
|
|
941
|
+
registry_discovered_fn=None,
|
|
942
|
+
registry_quarantine_fn=None,
|
|
943
|
+
registry_approve_fn=None,
|
|
944
|
+
registry_sources_fn=None,
|
|
945
|
+
registry_metrics_fn=None,
|
|
946
|
+
):
|
|
947
|
+
"""DEPRECATED: Use MCPServerFactory instead.
|
|
948
|
+
|
|
949
|
+
This function exists for backward compatibility only.
|
|
950
|
+
Will be removed in v0.3.0.
|
|
951
|
+
|
|
952
|
+
Args:
|
|
953
|
+
registry_list_fn: Function to list providers.
|
|
954
|
+
registry_start_fn: Function to start a provider.
|
|
955
|
+
registry_stop_fn: Function to stop a provider.
|
|
956
|
+
registry_tools_fn: Function to get tool schemas.
|
|
957
|
+
registry_invoke_fn: Function to invoke a tool.
|
|
958
|
+
registry_details_fn: Function to get provider details.
|
|
959
|
+
registry_health_fn: Function to get registry health.
|
|
960
|
+
registry_discover_fn: Optional async function to trigger discovery.
|
|
961
|
+
registry_discovered_fn: Optional function to list discovered providers.
|
|
962
|
+
registry_quarantine_fn: Optional function to list quarantined providers.
|
|
963
|
+
registry_approve_fn: Optional async function to approve a provider.
|
|
964
|
+
registry_sources_fn: Optional function to list discovery sources.
|
|
965
|
+
registry_metrics_fn: Optional function to get metrics.
|
|
966
|
+
"""
|
|
967
|
+
import warnings
|
|
968
|
+
|
|
969
|
+
warnings.warn(
|
|
970
|
+
"setup_fastmcp_server() is deprecated. Use MCPServerFactory instead.",
|
|
971
|
+
DeprecationWarning,
|
|
972
|
+
stacklevel=2,
|
|
973
|
+
)
|
|
974
|
+
|
|
975
|
+
global _compat_factory
|
|
976
|
+
|
|
977
|
+
registry = RegistryFunctions(
|
|
978
|
+
list=registry_list_fn,
|
|
979
|
+
start=registry_start_fn,
|
|
980
|
+
stop=registry_stop_fn,
|
|
981
|
+
invoke=registry_invoke_fn,
|
|
982
|
+
tools=registry_tools_fn,
|
|
983
|
+
details=registry_details_fn,
|
|
984
|
+
health=registry_health_fn,
|
|
985
|
+
discover=registry_discover_fn,
|
|
986
|
+
discovered=registry_discovered_fn,
|
|
987
|
+
quarantine=registry_quarantine_fn,
|
|
988
|
+
approve=registry_approve_fn,
|
|
989
|
+
sources=registry_sources_fn,
|
|
990
|
+
metrics=registry_metrics_fn,
|
|
991
|
+
)
|
|
992
|
+
|
|
993
|
+
_compat_factory = MCPServerFactory(registry)
|
|
994
|
+
logger.info("fastmcp_server_configured_via_deprecated_api")
|
|
995
|
+
|
|
996
|
+
|
|
997
|
+
def create_fastmcp_server():
|
|
998
|
+
"""DEPRECATED: Use MCPServerFactory.create_server() instead.
|
|
999
|
+
|
|
1000
|
+
Returns:
|
|
1001
|
+
Configured FastMCP server instance.
|
|
1002
|
+
|
|
1003
|
+
Raises:
|
|
1004
|
+
RuntimeError: If setup_fastmcp_server() was not called first.
|
|
1005
|
+
"""
|
|
1006
|
+
import warnings
|
|
1007
|
+
|
|
1008
|
+
warnings.warn(
|
|
1009
|
+
"create_fastmcp_server() is deprecated. Use MCPServerFactory instead.",
|
|
1010
|
+
DeprecationWarning,
|
|
1011
|
+
stacklevel=2,
|
|
1012
|
+
)
|
|
1013
|
+
|
|
1014
|
+
if _compat_factory is None:
|
|
1015
|
+
raise RuntimeError(
|
|
1016
|
+
"setup_fastmcp_server() must be called before create_fastmcp_server(). "
|
|
1017
|
+
"Consider migrating to MCPServerFactory."
|
|
1018
|
+
)
|
|
1019
|
+
|
|
1020
|
+
return _compat_factory.create_server()
|
|
1021
|
+
|
|
1022
|
+
|
|
1023
|
+
def run_fastmcp_server():
|
|
1024
|
+
"""DEPRECATED: Use MCPServerFactory.create_asgi_app() with uvicorn.
|
|
1025
|
+
|
|
1026
|
+
Runs the FastMCP HTTP server. Blocks until shutdown.
|
|
1027
|
+
|
|
1028
|
+
Raises:
|
|
1029
|
+
RuntimeError: If setup_fastmcp_server() was not called first.
|
|
1030
|
+
"""
|
|
1031
|
+
import warnings
|
|
1032
|
+
|
|
1033
|
+
warnings.warn(
|
|
1034
|
+
"run_fastmcp_server() is deprecated. Use MCPServerFactory instead.",
|
|
1035
|
+
DeprecationWarning,
|
|
1036
|
+
stacklevel=2,
|
|
1037
|
+
)
|
|
1038
|
+
|
|
1039
|
+
import uvicorn
|
|
1040
|
+
|
|
1041
|
+
from .metrics import init_metrics
|
|
1042
|
+
|
|
1043
|
+
if _compat_factory is None:
|
|
1044
|
+
raise RuntimeError(
|
|
1045
|
+
"setup_fastmcp_server() must be called before run_fastmcp_server(). Consider migrating to MCPServerFactory."
|
|
1046
|
+
)
|
|
1047
|
+
|
|
1048
|
+
logger.info(
|
|
1049
|
+
"fastmcp_http_server_starting",
|
|
1050
|
+
host=_compat_factory.config.host,
|
|
1051
|
+
port=_compat_factory.config.port,
|
|
1052
|
+
streamable_http_path=_compat_factory.config.streamable_http_path,
|
|
1053
|
+
metrics_path="/metrics",
|
|
1054
|
+
)
|
|
1055
|
+
|
|
1056
|
+
init_metrics(version="1.0.0")
|
|
1057
|
+
app = _compat_factory.create_asgi_app()
|
|
1058
|
+
|
|
1059
|
+
uvicorn.run(
|
|
1060
|
+
app,
|
|
1061
|
+
host=_compat_factory.config.host,
|
|
1062
|
+
port=_compat_factory.config.port,
|
|
1063
|
+
log_level="warning",
|
|
1064
|
+
access_log=False,
|
|
1065
|
+
)
|
|
1066
|
+
|
|
1067
|
+
|
|
1068
|
+
# =============================================================================
|
|
1069
|
+
# Module exports
|
|
1070
|
+
# =============================================================================
|
|
1071
|
+
|
|
1072
|
+
__all__ = [
|
|
1073
|
+
# New API
|
|
1074
|
+
"MCPServerFactory",
|
|
1075
|
+
"MCPServerFactoryBuilder",
|
|
1076
|
+
"RegistryFunctions",
|
|
1077
|
+
"ServerConfig",
|
|
1078
|
+
# Protocols
|
|
1079
|
+
"RegistryListFn",
|
|
1080
|
+
"RegistryStartFn",
|
|
1081
|
+
"RegistryStopFn",
|
|
1082
|
+
"RegistryInvokeFn",
|
|
1083
|
+
"RegistryToolsFn",
|
|
1084
|
+
"RegistryDetailsFn",
|
|
1085
|
+
"RegistryHealthFn",
|
|
1086
|
+
"RegistryDiscoverFn",
|
|
1087
|
+
"RegistryDiscoveredFn",
|
|
1088
|
+
"RegistryQuarantineFn",
|
|
1089
|
+
"RegistryApproveFn",
|
|
1090
|
+
"RegistrySourcesFn",
|
|
1091
|
+
"RegistryMetricsFn",
|
|
1092
|
+
# Deprecated (backward compatibility)
|
|
1093
|
+
"setup_fastmcp_server",
|
|
1094
|
+
"create_fastmcp_server",
|
|
1095
|
+
"run_fastmcp_server",
|
|
1096
|
+
]
|
|
1097
|
+
|
|
1098
|
+
|
|
1099
|
+
if __name__ == "__main__":
|
|
1100
|
+
from .logging_config import setup_logging
|
|
1101
|
+
|
|
1102
|
+
setup_logging(level="INFO", json_format=False)
|
|
1103
|
+
|
|
1104
|
+
# Example with deprecated API (will emit warning)
|
|
1105
|
+
run_fastmcp_server()
|