mcp-server-core 0.2.0__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 imon
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,75 @@
1
+ Metadata-Version: 2.4
2
+ Name: mcp-server-core
3
+ Version: 0.2.0
4
+ Summary: Universal MCP Server Framework: Streamable HTTP ASGI, Cloudflare Tunnel, OAuth 2.1 & Process Supervisor
5
+ Author: yimmr
6
+ License-Expression: MIT
7
+ Classifier: Development Status :: 4 - Beta
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Requires-Python: >=3.10
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Requires-Dist: mcp<2,>=1.2.0
17
+ Requires-Dist: starlette>=0.36.0
18
+ Requires-Dist: uvicorn>=0.27.0
19
+ Requires-Dist: pywin32>=306; platform_system == "Windows"
20
+ Provides-Extra: test
21
+ Requires-Dist: httpx>=0.27.0; extra == "test"
22
+ Dynamic: license-file
23
+
24
+ # MCP Framework 🚀
25
+
26
+ **Universal MCP Server Framework for Streamable HTTP ASGI, Cloudflare Tunnel, OAuth 2.1 & Process Supervisor**
27
+
28
+ Decoupled, reusable transport and lifecycle layer for FastMCP services (e.g. Codexen, Literary Workflow).
29
+
30
+ ---
31
+
32
+ ## 📦 Features
33
+
34
+ - **ASGI Streamable HTTP Transport**: Starlette + FastMCP integration supporting SSE, streaming, and atomic JSON-RPC responses.
35
+ - **Cache-Busting & Anti-Stale Guarantees**: Built-in `NoCacheMiddleware` ensuring `Cache-Control: no-cache, no-store, must-revalidate` across all discovery and tool execution routes to prevent intermediate CDN/Cloudflare heuristic caching.
36
+ - **Embedded OAuth 2.1 Server**: Full RFC 9728 (Protected Resource Metadata), RFC 8414 (Authorization Server Metadata), RFC 7591 (Dynamic Client Registration), and PKCE S256 support for ChatGPT Developer Mode & third-party AI agents.
37
+ - **Cloudflare Tunnel Singleton Supervisor**: Outbound named tunnel daemon lifecycle management with automated health check and heartbeat recovery.
38
+ - **Windows Named Mutex & Process Lifecycle**: Prevents duplicate server port bindings, cleans up zombie PIDs, and handles cross-platform single-instance constraints.
39
+
40
+ ---
41
+
42
+ ## 🛠️ Installation & Usage
43
+
44
+ ### 1. From PyPI (Recommended for Production)
45
+ ```bash
46
+ pip install mcp-server-core
47
+ # or with uv
48
+ uv add mcp-server-core
49
+ ```
50
+ Or declare in `pyproject.toml`:
51
+ ```toml
52
+ dependencies = [
53
+ "mcp-server-core>=0.2.0",
54
+ ]
55
+ ```
56
+
57
+ ### 2. As a Git Dependency
58
+ ```toml
59
+ # In pyproject.toml
60
+ dependencies = [
61
+ "mcp-server-core @ git+https://github.com/yimmr/mcp-framework.git",
62
+ ]
63
+ ```
64
+
65
+ ### 3. Local Editable Development (Instant Sync across Projects)
66
+ In consumer projects (e.g. `Codexen` or `literary-workflow`):
67
+ ```bash
68
+ uv pip install -e path/to/mcp-framework
69
+ ```
70
+ Changes made in this repository will instantly reflect across all local consumer services without copying code.
71
+
72
+ ---
73
+
74
+ ## 📄 License
75
+ MIT License.
@@ -0,0 +1,52 @@
1
+ # MCP Framework 🚀
2
+
3
+ **Universal MCP Server Framework for Streamable HTTP ASGI, Cloudflare Tunnel, OAuth 2.1 & Process Supervisor**
4
+
5
+ Decoupled, reusable transport and lifecycle layer for FastMCP services (e.g. Codexen, Literary Workflow).
6
+
7
+ ---
8
+
9
+ ## 📦 Features
10
+
11
+ - **ASGI Streamable HTTP Transport**: Starlette + FastMCP integration supporting SSE, streaming, and atomic JSON-RPC responses.
12
+ - **Cache-Busting & Anti-Stale Guarantees**: Built-in `NoCacheMiddleware` ensuring `Cache-Control: no-cache, no-store, must-revalidate` across all discovery and tool execution routes to prevent intermediate CDN/Cloudflare heuristic caching.
13
+ - **Embedded OAuth 2.1 Server**: Full RFC 9728 (Protected Resource Metadata), RFC 8414 (Authorization Server Metadata), RFC 7591 (Dynamic Client Registration), and PKCE S256 support for ChatGPT Developer Mode & third-party AI agents.
14
+ - **Cloudflare Tunnel Singleton Supervisor**: Outbound named tunnel daemon lifecycle management with automated health check and heartbeat recovery.
15
+ - **Windows Named Mutex & Process Lifecycle**: Prevents duplicate server port bindings, cleans up zombie PIDs, and handles cross-platform single-instance constraints.
16
+
17
+ ---
18
+
19
+ ## 🛠️ Installation & Usage
20
+
21
+ ### 1. From PyPI (Recommended for Production)
22
+ ```bash
23
+ pip install mcp-server-core
24
+ # or with uv
25
+ uv add mcp-server-core
26
+ ```
27
+ Or declare in `pyproject.toml`:
28
+ ```toml
29
+ dependencies = [
30
+ "mcp-server-core>=0.2.0",
31
+ ]
32
+ ```
33
+
34
+ ### 2. As a Git Dependency
35
+ ```toml
36
+ # In pyproject.toml
37
+ dependencies = [
38
+ "mcp-server-core @ git+https://github.com/yimmr/mcp-framework.git",
39
+ ]
40
+ ```
41
+
42
+ ### 3. Local Editable Development (Instant Sync across Projects)
43
+ In consumer projects (e.g. `Codexen` or `literary-workflow`):
44
+ ```bash
45
+ uv pip install -e path/to/mcp-framework
46
+ ```
47
+ Changes made in this repository will instantly reflect across all local consumer services without copying code.
48
+
49
+ ---
50
+
51
+ ## 📄 License
52
+ MIT License.
@@ -0,0 +1,94 @@
1
+ """MCP Framework: Reusable platform foundation for local MCP services."""
2
+ from __future__ import annotations
3
+
4
+ from .env import DeploymentEnvConfig, get_runtime_dir, update_env_variable
5
+ from .http_server import (
6
+ HTTPAuthenticationMiddleware,
7
+ create_mcp_asgi_app,
8
+ get_local_ip,
9
+ mount_mcp_oauth_routes,
10
+ )
11
+ from .oauth import (
12
+ AuthRateLimiter,
13
+ LocalOAuthServer,
14
+ base64url_encode,
15
+ get_client_ip,
16
+ get_server_public_url,
17
+ verify_pkce,
18
+ )
19
+ from .process import (
20
+ check_service_status,
21
+ find_pid_by_port,
22
+ is_pid_alive,
23
+ is_port_in_use,
24
+ kill_pid,
25
+ kill_stale_server_process,
26
+ stop_mcp_service,
27
+ )
28
+ from .server import (
29
+ build_mcp_server_app,
30
+ record_pid,
31
+ cleanup_pid,
32
+ run_mcp_server,
33
+ serve_http,
34
+ serve_stdio,
35
+ )
36
+ from .tunnel import (
37
+ CloudflareTunnel,
38
+ TunnelHeartbeatMonitor,
39
+ check_tunnel_health,
40
+ ensure_tunnel_running,
41
+ find_cloudflared,
42
+ get_cloudflare_ingress_ports,
43
+ get_public_tunnel_url,
44
+ get_tunnel_token,
45
+ is_cloudflared_available,
46
+ is_port_listening,
47
+ is_tunnel_running,
48
+ restart_tunnel,
49
+ start_tunnel_heartbeat_monitor,
50
+ stop_tunnel_if_no_mcp,
51
+ )
52
+
53
+ __all__ = [
54
+ "DeploymentEnvConfig",
55
+ "get_runtime_dir",
56
+ "update_env_variable",
57
+ "HTTPAuthenticationMiddleware",
58
+ "create_mcp_asgi_app",
59
+ "get_local_ip",
60
+ "mount_mcp_oauth_routes",
61
+ "LocalOAuthServer",
62
+ "AuthRateLimiter",
63
+ "base64url_encode",
64
+ "get_client_ip",
65
+ "get_server_public_url",
66
+ "verify_pkce",
67
+ "build_mcp_server_app",
68
+ "record_pid",
69
+ "cleanup_pid",
70
+ "run_mcp_server",
71
+ "serve_http",
72
+ "serve_stdio",
73
+ "CloudflareTunnel",
74
+ "ensure_tunnel_running",
75
+ "find_cloudflared",
76
+ "get_cloudflare_ingress_ports",
77
+ "get_tunnel_token",
78
+ "is_cloudflared_available",
79
+ "is_port_listening",
80
+ "is_tunnel_running",
81
+ "stop_tunnel_if_no_mcp",
82
+ "get_public_tunnel_url",
83
+ "check_tunnel_health",
84
+ "restart_tunnel",
85
+ "TunnelHeartbeatMonitor",
86
+ "start_tunnel_heartbeat_monitor",
87
+ "check_service_status",
88
+ "find_pid_by_port",
89
+ "is_pid_alive",
90
+ "is_port_in_use",
91
+ "kill_pid",
92
+ "kill_stale_server_process",
93
+ "stop_mcp_service",
94
+ ]
@@ -0,0 +1,138 @@
1
+ """Generic MCP Server Environment & Deployment Configuration Module.
2
+
3
+ Independent of any domain-specific business logic, this module handles:
4
+ 1. System runtime directory management (.runtime / .mcp_runtime).
5
+ 2. Dynamic environment variable updates (.env persistence).
6
+ 3. Generic deployment configuration loading (ports, hosts, Cloudflare tunnels, auth keys, SQLite paths).
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ from dataclasses import dataclass
12
+ from pathlib import Path
13
+ import secrets
14
+ from typing import Any
15
+
16
+
17
+ def get_runtime_dir(base_dir: Path | None = None) -> Path:
18
+ """Get or create the centralized runtime directory for temporary/state files."""
19
+ root = (base_dir or Path.cwd()).resolve()
20
+ dir_name = os.getenv("MCP_RUNTIME_DIR", ".runtime")
21
+ runtime_dir = root / dir_name
22
+ runtime_dir.mkdir(parents=True, exist_ok=True)
23
+ return runtime_dir
24
+
25
+
26
+ def update_env_variable(key: str, value: str, env_path: Path | None = None) -> None:
27
+ """Update or append an environment variable in the target .env file."""
28
+ target = env_path or (Path.cwd() / ".env")
29
+ lines = []
30
+ found = False
31
+ if target.exists():
32
+ for line in target.read_text(encoding="utf-8").splitlines():
33
+ stripped = line.strip()
34
+ if stripped.startswith(f"{key}=") or stripped.startswith(f"export {key}="):
35
+ lines.append(f"{key}={value}")
36
+ found = True
37
+ else:
38
+ lines.append(line)
39
+ if not found:
40
+ lines.append(f"{key}={value}")
41
+ target.write_text("\n".join(lines) + "\n", encoding="utf-8")
42
+ os.environ[key] = value
43
+
44
+
45
+ @dataclass
46
+ class DeploymentEnvConfig:
47
+ """Generic MCP server deployment and runtime environment configuration (loaded from .env).
48
+
49
+ This class manages purely infrastructure parameters (ports, hosts, Cloudflare tunnel tokens,
50
+ API keys, and OAuth database paths) completely independent of any project business logic.
51
+ """
52
+ enabled: bool = True
53
+ host: str = "127.0.0.1"
54
+ port: int = 8788
55
+ path: str = "/mcp"
56
+ require_auth: bool = True
57
+ api_key: str = ""
58
+ allow_public_bind: bool = True
59
+ tunnel: bool = True
60
+ tunnel_token: str = ""
61
+ portal_url: str = ""
62
+ upstream_url: str = ""
63
+ allow_query_auth: bool = False
64
+ enable_public_oauth: bool = False
65
+ db_path: str = ""
66
+
67
+ @classmethod
68
+ def load(
69
+ cls,
70
+ base_dir: Path | None = None,
71
+ overrides: dict[str, Any] | None = None,
72
+ ) -> DeploymentEnvConfig:
73
+ """Load deployment and runtime configuration from .env and environment variables."""
74
+ root = (base_dir or Path.cwd()).resolve()
75
+ env_file = root / ".env"
76
+ if env_file.exists():
77
+ try:
78
+ from dotenv import load_dotenv
79
+ load_dotenv(env_file)
80
+ except ImportError:
81
+ pass
82
+
83
+ overrides = overrides or {}
84
+ runtime_dir = get_runtime_dir(root)
85
+
86
+ is_auth_req = bool(
87
+ overrides.get(
88
+ "require_auth",
89
+ os.getenv("MCP_REQUIRE_AUTH", "true").lower() == "true",
90
+ )
91
+ )
92
+ resolved_api_key = str(
93
+ overrides.get("api_key", os.getenv("MCP_API_KEY", ""))
94
+ ).strip()
95
+
96
+ # If authentication is required but no key configured anywhere, auto-generate and store in .env
97
+ if is_auth_req and not resolved_api_key:
98
+ resolved_api_key = f"sec_{secrets.token_urlsafe(16)}"
99
+ try:
100
+ update_env_variable("MCP_API_KEY", resolved_api_key, env_file)
101
+ except Exception:
102
+ pass
103
+
104
+ resolved_tunnel_token = str(
105
+ overrides.get(
106
+ "tunnel_token",
107
+ os.getenv("CLOUDFLARE_TUNNEL_TOKEN", os.getenv("MCP_TUNNEL_TOKEN", "")),
108
+ )
109
+ ).strip()
110
+
111
+ return cls(
112
+ enabled=bool(overrides.get("enabled", True)),
113
+ host=str(overrides.get("host", os.getenv("MCP_HOST", "127.0.0.1"))),
114
+ port=int(overrides.get("port", os.getenv("MCP_PORT", 8788))),
115
+ path=str(overrides.get("path", "/mcp")),
116
+ require_auth=is_auth_req,
117
+ api_key=resolved_api_key,
118
+ allow_public_bind=bool(overrides.get("allow_public_bind", True)),
119
+ tunnel=bool(overrides.get("tunnel", os.getenv("MCP_TUNNEL", "true").lower() == "true")),
120
+ tunnel_token=resolved_tunnel_token,
121
+ portal_url=str(overrides.get("portal_url", os.getenv("MCP_PORTAL_URL", ""))),
122
+ upstream_url=str(overrides.get("upstream_url", os.getenv("MCP_UPSTREAM_URL", ""))),
123
+ allow_query_auth=bool(
124
+ overrides.get(
125
+ "allow_query_auth",
126
+ os.getenv("MCP_ALLOW_QUERY_AUTH", "false").lower() == "true",
127
+ )
128
+ ),
129
+ enable_public_oauth=bool(
130
+ overrides.get(
131
+ "enable_public_oauth",
132
+ os.getenv("MCP_ENABLE_PUBLIC_OAUTH", "false").lower() == "true",
133
+ )
134
+ ),
135
+ db_path=str(
136
+ overrides.get("db_path", os.getenv("MCP_DB_PATH", str(runtime_dir / "oauth_store.db")))
137
+ ),
138
+ )
@@ -0,0 +1,282 @@
1
+ """Generic MCP HTTP Server & ASGI Transport Layer.
2
+
3
+ Provides reusable ASGI application assembly, CORS setup, authentication middleware,
4
+ and standard RFC 9728/8414/7591 OAuth discovery routes for any FastMCP server.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import socket
9
+ import sys
10
+ from typing import Any, Callable
11
+
12
+ # Protect against Windows GBK UnicodeEncodeError
13
+ if sys.platform == "win32":
14
+ try:
15
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
16
+ sys.stderr.reconfigure(encoding="utf-8", errors="replace")
17
+ except Exception:
18
+ pass
19
+
20
+ from starlette.applications import Starlette
21
+ from starlette.middleware.base import BaseHTTPMiddleware
22
+ from starlette.middleware.cors import CORSMiddleware
23
+ from starlette.requests import Request
24
+ from starlette.responses import JSONResponse, Response
25
+
26
+ try:
27
+ from mcp.server.fastmcp import FastMCP
28
+ except (ImportError, ModuleNotFoundError):
29
+ from mcp.server.mcpserver import MCPServer as FastMCP
30
+
31
+ from .env import DeploymentEnvConfig
32
+ from .oauth import (
33
+ LocalOAuthServer,
34
+ get_server_public_url,
35
+ oauth_authorization_server_metadata_endpoint,
36
+ oauth_authorize_endpoint,
37
+ oauth_jwks_endpoint,
38
+ oauth_protected_resource_endpoint,
39
+ oauth_register_endpoint,
40
+ oauth_token_endpoint,
41
+ )
42
+
43
+
44
+ def get_local_ip() -> str:
45
+ """Attempt to detect the primary local IPv4 address for network access."""
46
+ try:
47
+ with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
48
+ s.connect(("8.8.8.8", 80))
49
+ return s.getsockname()[0]
50
+ except Exception:
51
+ return "127.0.0.1"
52
+
53
+
54
+ class NoCacheMiddleware(BaseHTTPMiddleware):
55
+ """Enforce strict no-cache/no-store headers on all MCP, health, and discovery endpoints.
56
+
57
+ Prevents Cloudflare Edge, CDNs, and intermediate proxies from heuristically caching
58
+ tool schemas, capability catalogs, or dynamic RPC responses across service restarts.
59
+ """
60
+
61
+ async def dispatch(self, request: Request, call_next: Any) -> Response:
62
+ response = await call_next(request)
63
+ response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate, max-age=0"
64
+ response.headers["Pragma"] = "no-cache"
65
+ response.headers["Expires"] = "0"
66
+ return response
67
+
68
+
69
+ class HTTPAuthenticationMiddleware(BaseHTTPMiddleware):
70
+ """Simple API Key / Bearer Token & OAuth 2.1 authentication middleware for HTTP MCP."""
71
+
72
+ def __init__(
73
+ self,
74
+ app: Any,
75
+ oauth_server: LocalOAuthServer,
76
+ require_auth: bool = False,
77
+ allow_query_auth: bool = False,
78
+ enable_public_oauth: bool = True,
79
+ ) -> None:
80
+ super().__init__(app)
81
+ self.oauth_server = oauth_server
82
+ self.require_auth = require_auth
83
+ self.allow_query_auth = allow_query_auth
84
+ self.enable_public_oauth = enable_public_oauth
85
+
86
+ async def dispatch(self, request: Request, call_next: Any) -> Response:
87
+ # 1. Allow CORS pre-flight
88
+ if request.method == "OPTIONS":
89
+ return await call_next(request)
90
+
91
+ # 2. Public health check, OAuth and metadata endpoints require no auth
92
+ path = request.url.path
93
+ if (
94
+ path in ("/", "/health", "/docs")
95
+ or path.startswith("/.well-known/")
96
+ or path.startswith("/oauth/")
97
+ or path.endswith("/.well-known/oauth-protected-resource")
98
+ ):
99
+ return await call_next(request)
100
+
101
+ # 3. If authentication is not enforced, proceed
102
+ if not self.require_auth:
103
+ return await call_next(request)
104
+
105
+ # 4. Extract token from Authorization Header (or URL query only if explicitly allowed)
106
+ token = ""
107
+ auth_header = request.headers.get("Authorization", "")
108
+ if auth_header.startswith("Bearer "):
109
+ token = auth_header[7:].strip()
110
+ elif self.allow_query_auth:
111
+ if "token" in request.query_params:
112
+ token = request.query_params.get("token", "").strip()
113
+ elif "key" in request.query_params:
114
+ token = request.query_params.get("key", "").strip()
115
+
116
+ public_url = get_server_public_url(request)
117
+
118
+ if not token:
119
+ if self.enable_public_oauth:
120
+ challenge = f'Bearer resource_metadata="{public_url}/.well-known/oauth-protected-resource"'
121
+ msg = "Authentication required. Please authenticate via OAuth or provide Bearer token in Authorization header."
122
+ else:
123
+ challenge = 'Bearer error="invalid_token"'
124
+ msg = "Direct public access to this backend is disabled. Please connect through your configured MCP portal or provide a valid Bearer token."
125
+ return JSONResponse(
126
+ {
127
+ "error": "unauthorized",
128
+ "message": msg,
129
+ },
130
+ status_code=401,
131
+ headers={"WWW-Authenticate": challenge, "Access-Control-Allow-Origin": "*"},
132
+ )
133
+
134
+ if not self.oauth_server.is_token_valid(token):
135
+ if self.enable_public_oauth:
136
+ challenge = f'Bearer error="invalid_token", resource_metadata="{public_url}/.well-known/oauth-protected-resource"'
137
+ else:
138
+ challenge = 'Bearer error="invalid_token"'
139
+ return JSONResponse(
140
+ {"error": "forbidden", "message": "Invalid API Key or OAuth Token."},
141
+ status_code=403,
142
+ headers={"WWW-Authenticate": challenge, "Access-Control-Allow-Origin": "*"},
143
+ )
144
+
145
+ return await call_next(request)
146
+
147
+
148
+ def mount_mcp_oauth_routes(
149
+ server: FastMCP,
150
+ oauth_server: LocalOAuthServer,
151
+ service_name: str = "mcp_service",
152
+ root_info_builder: Callable[[Request], dict[str, Any]] | None = None,
153
+ enable_public_oauth: bool = True,
154
+ ) -> None:
155
+ """Mount standard OAuth 2.1 metadata, authorization, token, and health routes on FastMCP."""
156
+ # 1. Custom informative root endpoint
157
+ @server.custom_route("/", methods=["GET", "OPTIONS"])
158
+ async def root_info(request: Request) -> Response:
159
+ if root_info_builder:
160
+ payload = root_info_builder(request)
161
+ else:
162
+ payload = {
163
+ "status": "online",
164
+ "service": service_name,
165
+ "endpoints": {
166
+ "health": "/health",
167
+ "oauth_protected_resource": "/.well-known/oauth-protected-resource",
168
+ "oauth_authorization_server": "/.well-known/oauth-authorization-server",
169
+ "oauth_register": "/oauth/register",
170
+ "oauth_authorize": "/oauth/authorize",
171
+ "oauth_token": "/oauth/token",
172
+ },
173
+ }
174
+ return JSONResponse(payload, headers={"Access-Control-Allow-Origin": "*"})
175
+
176
+ # 2. Health check endpoint
177
+ @server.custom_route("/health", methods=["GET", "OPTIONS"])
178
+ async def health_check(request: Request) -> Response:
179
+ return JSONResponse(
180
+ {"status": "healthy", "service": service_name},
181
+ headers={"Access-Control-Allow-Origin": "*"},
182
+ )
183
+
184
+ # 3. RFC 9728 Protected Resource Metadata (ChatGPT 探测入口)
185
+ @server.custom_route("/.well-known/oauth-protected-resource", methods=["GET", "OPTIONS"])
186
+ @server.custom_route("/.well-known/oauth-protected-resource/mcp", methods=["GET", "OPTIONS"])
187
+ @server.custom_route("/mcp/.well-known/oauth-protected-resource", methods=["GET", "OPTIONS"])
188
+ async def protected_resource(request: Request) -> Response:
189
+ if not enable_public_oauth:
190
+ return JSONResponse(
191
+ {
192
+ "error": "oauth_disabled",
193
+ "message": "Public OAuth registration is disabled on this backend.",
194
+ },
195
+ status_code=404,
196
+ headers={"Access-Control-Allow-Origin": "*"},
197
+ )
198
+ return await oauth_protected_resource_endpoint(request)
199
+
200
+ # 4. RFC 8414 Authorization Server Metadata (OAuth 2.1 发现入口)
201
+ @server.custom_route("/.well-known/oauth-authorization-server", methods=["GET", "OPTIONS"])
202
+ @server.custom_route("/.well-known/oauth-authorization-server/mcp", methods=["GET", "OPTIONS"])
203
+ @server.custom_route("/.well-known/openid-configuration", methods=["GET", "OPTIONS"])
204
+ @server.custom_route("/.well-known/openid-configuration/mcp", methods=["GET", "OPTIONS"])
205
+ async def authorization_server_metadata(request: Request) -> Response:
206
+ if not enable_public_oauth:
207
+ return JSONResponse(
208
+ {"error": "oauth_disabled", "message": "Public OAuth discovery is disabled on this backend."},
209
+ status_code=404,
210
+ headers={"Access-Control-Allow-Origin": "*"},
211
+ )
212
+ return await oauth_authorization_server_metadata_endpoint(request)
213
+
214
+ # 5. RFC 7591 Dynamic Client Registration
215
+ @server.custom_route("/oauth/register", methods=["POST", "OPTIONS"])
216
+ async def oauth_register(request: Request) -> Response:
217
+ if not enable_public_oauth:
218
+ return JSONResponse(
219
+ {
220
+ "error": "registration_disabled",
221
+ "error_description": "Public OAuth registration on this endpoint is disabled.",
222
+ },
223
+ status_code=403,
224
+ headers={"Access-Control-Allow-Origin": "*"},
225
+ )
226
+ return await oauth_register_endpoint(request, oauth_server)
227
+
228
+ # 6. OAuth 2.1 Authorization Code Consent Page
229
+ @server.custom_route("/oauth/authorize", methods=["GET", "POST", "OPTIONS"])
230
+ async def oauth_authorize(request: Request) -> Response:
231
+ if not enable_public_oauth:
232
+ return JSONResponse(
233
+ {"error": "authorization_disabled", "message": "OAuth consent is disabled on this backend."},
234
+ status_code=403,
235
+ headers={"Access-Control-Allow-Origin": "*"},
236
+ )
237
+ return await oauth_authorize_endpoint(request, oauth_server)
238
+
239
+ # 7. OAuth 2.1 Token Exchange
240
+ @server.custom_route("/oauth/token", methods=["POST", "OPTIONS"])
241
+ async def oauth_token(request: Request) -> Response:
242
+ if not enable_public_oauth:
243
+ return JSONResponse(
244
+ {"error": "token_endpoint_disabled", "message": "OAuth token exchange is disabled on this backend."},
245
+ status_code=403,
246
+ headers={"Access-Control-Allow-Origin": "*"},
247
+ )
248
+ return await oauth_token_endpoint(request, oauth_server)
249
+
250
+ # 8. JWKS Public Keys Endpoint
251
+ @server.custom_route("/oauth/jwks.json", methods=["GET", "OPTIONS"])
252
+ @server.custom_route("/.well-known/jwks.json", methods=["GET", "OPTIONS"])
253
+ async def oauth_jwks(request: Request) -> Response:
254
+ return await oauth_jwks_endpoint(request)
255
+
256
+
257
+ def create_mcp_asgi_app(
258
+ server: FastMCP,
259
+ env_cfg: DeploymentEnvConfig,
260
+ oauth_server: LocalOAuthServer,
261
+ require_auth: bool | None = None,
262
+ ) -> Starlette:
263
+ """Build the final Starlette application with CORS and authentication middlewares."""
264
+ is_auth_required = env_cfg.require_auth if require_auth is None else require_auth
265
+
266
+ app = server.streamable_http_app()
267
+ app.add_middleware(NoCacheMiddleware)
268
+ app.add_middleware(
269
+ HTTPAuthenticationMiddleware,
270
+ oauth_server=oauth_server,
271
+ require_auth=is_auth_required,
272
+ allow_query_auth=env_cfg.allow_query_auth,
273
+ enable_public_oauth=env_cfg.enable_public_oauth,
274
+ )
275
+ app.add_middleware(
276
+ CORSMiddleware,
277
+ allow_origins=["*"],
278
+ allow_methods=["*"],
279
+ allow_headers=["*"],
280
+ expose_headers=["*", "WWW-Authenticate", "Mcp-Session-Id"],
281
+ )
282
+ return app