mirror of
https://github.com/mcp-use/mcp-use.git
synced 2026-09-14 18:06:01 +08:00
30b23271d5
* fix(python): respect configured inspector path * fix(python): treat inspector path as a base prefix * fix(python): format inspector fixes for ruff * fix(ci): run conformance with node 22 * fix(ci): fail conformance runs with empty results * chore(ci): drop inline conformance result validation * docs(python): clarify inspector path behavior
616 lines
24 KiB
Python
616 lines
24 KiB
Python
from __future__ import annotations
|
|
|
|
import inspect
|
|
import logging
|
|
import os
|
|
import time
|
|
import weakref
|
|
from datetime import UTC, datetime
|
|
from typing import TYPE_CHECKING, Any, cast
|
|
|
|
import mcp.server.lowlevel.server as lowlevel
|
|
import mcp.server.session as mcp_session
|
|
from mcp.server.fastmcp import FastMCP
|
|
from mcp.types import (
|
|
AnyFunction,
|
|
CallToolRequest,
|
|
CompleteRequest,
|
|
Completion,
|
|
GetPromptRequest,
|
|
Icon,
|
|
ListPromptsRequest,
|
|
ListResourcesRequest,
|
|
ListToolsRequest,
|
|
ReadResourceRequest,
|
|
ServerResult,
|
|
SetLevelRequest,
|
|
SubscribeRequest,
|
|
UnsubscribeRequest,
|
|
)
|
|
|
|
# Import auth components
|
|
from mcp_use.server.auth import AuthMiddleware, BearerAuthProvider
|
|
from mcp_use.server.context import Context as MCPContext
|
|
from mcp_use.server.logging import MCPLoggingMiddleware
|
|
from mcp_use.server.middleware import (
|
|
Middleware,
|
|
MiddlewareManager,
|
|
ServerMiddlewareContext,
|
|
TelemetryMiddleware,
|
|
)
|
|
from mcp_use.server.middleware.server_session import MiddlewareServerSession
|
|
from mcp_use.server.runner import ServerRunner
|
|
from mcp_use.server.types import TransportType
|
|
from mcp_use.server.utils.inspector import _inspector_index, _inspector_static
|
|
from mcp_use.server.utils.json_schema import simplify_optional_schema
|
|
from mcp_use.server.utils.routes import docs_ui, openmcp_json
|
|
from mcp_use.telemetry.telemetry import Telemetry, telemetry
|
|
from mcp_use.telemetry.utils import track_server_run_from_server
|
|
|
|
# Monkey patching for init request of middleware
|
|
mcp_session.ServerSession = MiddlewareServerSession
|
|
lowlevel.ServerSession = MiddlewareServerSession
|
|
|
|
if TYPE_CHECKING:
|
|
from mcp.server.session import ServerSession
|
|
|
|
from mcp_use.server.router import MCPRouter
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
_telemetry = Telemetry()
|
|
|
|
|
|
def _normalize_inspector_path(path: str) -> str:
|
|
"""Normalize inspector path input into a concrete route ending in `/inspector`."""
|
|
if not path or path == "/":
|
|
return "/inspector"
|
|
|
|
normalized = f"/{path.strip('/')}"
|
|
if normalized.endswith("/inspector"):
|
|
return normalized
|
|
return f"{normalized}/inspector"
|
|
|
|
|
|
class MCPServer(FastMCP):
|
|
"""Main MCP Server class with integrated inspector and development tools."""
|
|
|
|
def __init__(
|
|
self,
|
|
name: str | None = None,
|
|
version: str | None = None,
|
|
instructions: str | None = None,
|
|
icons: list[Icon] | None = None,
|
|
auth: BearerAuthProvider | None = None,
|
|
middleware: list[Middleware] | None = None,
|
|
debug: bool = False,
|
|
mcp_path: str = "/mcp",
|
|
docs_path: str = "/docs",
|
|
inspector_path: str = "/",
|
|
openmcp_path: str = "/openmcp.json",
|
|
show_inspector_logs: bool = False,
|
|
pretty_print_jsonrpc: bool = False,
|
|
mcp_logs_only: bool = False,
|
|
host: str = "0.0.0.0",
|
|
port: int = 8000,
|
|
dns_rebinding_protection: bool = False,
|
|
):
|
|
"""Initialize an MCP server.
|
|
|
|
Args:
|
|
name: Server name for identification
|
|
version: Server version string
|
|
instructions: Instructions for the AI model using this server
|
|
icons: Optional list of icons for the server implementation
|
|
middleware: List of middleware to apply to requests
|
|
debug: Enable debug mode (adds /docs, /inspector, /openmcp.json endpoints)
|
|
mcp_path: Path for MCP endpoint (default: "/mcp")
|
|
docs_path: Path for documentation endpoint (default: "/docs")
|
|
inspector_path: Base path prefix for the inspector UI; final route is `<prefix>/inspector`.
|
|
Examples: `/` -> `/inspector`, `/mcp` -> `/mcp/inspector`.
|
|
Passing `/mcp/inspector` is also accepted for backward compatibility.
|
|
openmcp_path: Path for OpenMCP metadata (default: "/openmcp.json")
|
|
show_inspector_logs: Show inspector-related logs
|
|
pretty_print_jsonrpc: Pretty print JSON-RPC messages in logs
|
|
mcp_logs_only: Only show MCP protocol logs, suppress HTTP access logs (default: False)
|
|
host: Default host for server binding (default: "0.0.0.0"). Can be overridden in run().
|
|
port: Default port for server binding (default: 8000). Can be overridden in run().
|
|
dns_rebinding_protection: Enable DNS rebinding protection by validating Host/Origin
|
|
headers. When True, only requests from localhost origins are accepted.
|
|
Recommended for local development servers. Default: False.
|
|
"""
|
|
self._start_time = time.time()
|
|
self._dns_rebinding_protection = dns_rebinding_protection
|
|
super().__init__(
|
|
name=name or "mcp-use server",
|
|
instructions=instructions,
|
|
icons=icons,
|
|
host=host,
|
|
port=port,
|
|
)
|
|
|
|
# Apply DNS rebinding protection if requested
|
|
if dns_rebinding_protection:
|
|
self._apply_dns_rebinding_protection(host)
|
|
|
|
if version:
|
|
self._mcp_server.version = version
|
|
|
|
# Register default protocol handlers for full MCP spec compliance
|
|
self._register_default_protocol_handlers()
|
|
|
|
# Store auth provider
|
|
self._auth = auth
|
|
|
|
# Set debug level: DEBUG env var takes precedence, then debug parameter
|
|
env_debug_level = self._parse_debug_level()
|
|
if env_debug_level > 0:
|
|
# Environment variable overrides parameter
|
|
self.debug_level = env_debug_level
|
|
else:
|
|
# Use debug parameter (0 or 1)
|
|
self.debug_level = 1 if debug else 0
|
|
|
|
# Set route paths
|
|
self.mcp_path = mcp_path
|
|
self.docs_path = docs_path
|
|
self.inspector_path = _normalize_inspector_path(inspector_path)
|
|
self.openmcp_path = openmcp_path
|
|
self.show_inspector_logs = show_inspector_logs
|
|
self.pretty_print_jsonrpc = pretty_print_jsonrpc
|
|
self.mcp_logs_only = mcp_logs_only
|
|
self._transport_type: TransportType = "streamable-http"
|
|
|
|
self.middleware_manager = MiddlewareManager()
|
|
self.middleware_manager.add_middleware(TelemetryMiddleware())
|
|
|
|
if middleware:
|
|
for middleware_instance in middleware:
|
|
self.middleware_manager.add_middleware(middleware_instance)
|
|
|
|
# Add dev routes only in DEBUG=1 and above
|
|
if self.debug_level >= 1:
|
|
self._add_dev_routes()
|
|
|
|
self.app = self.streamable_http_app()
|
|
|
|
# Inject middleware in the ServerSession
|
|
MiddlewareServerSession._middleware_manager = self.middleware_manager
|
|
MiddlewareServerSession._transport_type = self._transport_type
|
|
|
|
@property
|
|
def debug(self) -> bool:
|
|
"""Whether debug mode is enabled."""
|
|
return self.debug_level >= 1
|
|
|
|
def _parse_debug_level(self) -> int:
|
|
"""Parse DEBUG environment variable to get debug level.
|
|
|
|
Returns:
|
|
0: Production mode (clean logs only)
|
|
1: Debug mode (clean logs + dev routes)
|
|
2: Full debug mode (clean logs + dev routes + JSON-RPC logging)
|
|
"""
|
|
debug_env = os.environ.get("DEBUG", "0")
|
|
try:
|
|
level = int(debug_env)
|
|
return max(0, min(2, level)) # Clamp between 0-2
|
|
except ValueError:
|
|
# Handle string values
|
|
if debug_env.lower() in ("1", "true", "yes"):
|
|
return 1
|
|
elif debug_env.lower() in ("2", "full", "verbose"):
|
|
return 2
|
|
else:
|
|
return 0
|
|
|
|
def _add_dev_routes(self):
|
|
"""Add development routes for debugging and inspection."""
|
|
|
|
# OpenMCP configuration
|
|
async def openmcp_handler(request):
|
|
return await openmcp_json(request, self)
|
|
|
|
self.custom_route(self.openmcp_path, methods=["GET"])(openmcp_handler)
|
|
|
|
# Documentation UI
|
|
self.custom_route(self.docs_path, methods=["GET"])(docs_ui)
|
|
|
|
# Inspector routes - wrap to pass mcp_path
|
|
async def inspector_index_handler(request):
|
|
return await _inspector_index(
|
|
request,
|
|
mcp_path=self.mcp_path,
|
|
inspector_path=self.inspector_path,
|
|
)
|
|
|
|
self.custom_route(self.inspector_path, methods=["GET"])(inspector_index_handler)
|
|
self.custom_route(f"{self.inspector_path}/{{path:path}}", methods=["GET"])(_inspector_static)
|
|
|
|
def add_tool(
|
|
self,
|
|
fn: AnyFunction,
|
|
name: str | None = None,
|
|
title: str | None = None,
|
|
description: str | None = None,
|
|
annotations: Any = None,
|
|
icons: Any = None,
|
|
meta: dict[str, Any] | None = None,
|
|
structured_output: bool | None = None,
|
|
) -> None:
|
|
"""Register a tool, simplifying Pydantic's nullable ``anyOf`` schemas.
|
|
|
|
Pydantic emits ``{"anyOf": [{"type": "T"}, {"type": "null"}]}`` for
|
|
``Optional[T]`` fields. MCP expresses optionality by omitting the
|
|
property from ``required``, so the ``anyOf``/null wrapper is redundant
|
|
and breaks description rendering in several MCP clients.
|
|
|
|
This override lets the upstream ``FastMCP`` build the tool as usual,
|
|
then rewrites its ``parameters`` schema into the simpler form.
|
|
"""
|
|
super().add_tool(
|
|
fn,
|
|
name=name,
|
|
title=title,
|
|
description=description,
|
|
annotations=annotations,
|
|
icons=icons,
|
|
meta=meta,
|
|
structured_output=structured_output,
|
|
)
|
|
# The tool was registered under its resolved name — look it up and
|
|
# simplify the schema that Pydantic generated.
|
|
tool_name = name or fn.__name__
|
|
tool = self._tool_manager.get_tool(tool_name)
|
|
if tool is not None:
|
|
tool.parameters = simplify_optional_schema(tool.parameters)
|
|
|
|
@telemetry("server_router_used")
|
|
def include_router(self, router: MCPRouter, prefix: str = "", enabled: bool = True) -> None:
|
|
"""
|
|
Include a router's tools, resources, and prompts into this server.
|
|
|
|
Similar to FastAPI's include_router, this allows you to organize your
|
|
MCP server into multiple files/modules.
|
|
|
|
Args:
|
|
router: The MCPRouter instance to include
|
|
prefix: Optional prefix to add to all tool names (e.g., "math" -> "math_add")
|
|
enabled: Whether to enable this router (default True). Set to False to skip registration.
|
|
|
|
Example:
|
|
```python
|
|
from mcp_use.server import MCPServer, MCPRouter
|
|
|
|
# In routes/math.py
|
|
router = MCPRouter()
|
|
|
|
@router.tool()
|
|
def add(a: int, b: int) -> int:
|
|
return a + b
|
|
|
|
# In main.py
|
|
server = MCPServer(name="my-server")
|
|
server.include_router(router, prefix="math") # Tool becomes "math_add"
|
|
server.include_router(other_router, enabled=False) # Skip this router
|
|
```
|
|
"""
|
|
if not enabled:
|
|
return
|
|
# Register all tools from the router
|
|
for tool in router.tools:
|
|
tool_name = tool.name or getattr(tool.fn, "__name__", "unknown")
|
|
if prefix:
|
|
tool_name = f"{prefix}_{tool_name}"
|
|
|
|
self.add_tool(
|
|
tool.fn,
|
|
name=tool_name,
|
|
title=tool.title,
|
|
description=tool.description,
|
|
annotations=tool.annotations,
|
|
structured_output=tool.structured_output,
|
|
)
|
|
|
|
# Register all resources from the router
|
|
for resource in router.resources:
|
|
resource_name = resource.name or getattr(resource.fn, "__name__", "unknown")
|
|
if prefix:
|
|
resource_name = f"{prefix}_{resource_name}"
|
|
self.resource(
|
|
uri=resource.uri,
|
|
name=resource.name,
|
|
description=resource.description,
|
|
mime_type=resource.mime_type,
|
|
)(resource.fn)
|
|
|
|
# Register all prompts from the router
|
|
for prompt in router.prompts:
|
|
prompt_name = prompt.name or getattr(prompt.fn, "__name__", "unknown")
|
|
if prefix:
|
|
prompt_name = f"{prefix}_{prompt_name}"
|
|
|
|
self.prompt(
|
|
name=prompt_name,
|
|
description=prompt.description,
|
|
)(prompt.fn)
|
|
|
|
def _apply_dns_rebinding_protection(self, host: str) -> None:
|
|
"""Configure transport security to reject non-localhost Host/Origin headers."""
|
|
from mcp.server.transport_security import TransportSecuritySettings
|
|
|
|
localhost_host = host if host in ("127.0.0.1", "localhost", "::1") else "127.0.0.1"
|
|
self.settings.transport_security = TransportSecuritySettings(
|
|
enable_dns_rebinding_protection=True,
|
|
allowed_hosts=[f"{localhost_host}:*", "localhost:*", "127.0.0.1:*", "[::1]:*"],
|
|
allowed_origins=[
|
|
f"http://{localhost_host}:*",
|
|
"http://localhost:*",
|
|
"http://127.0.0.1:*",
|
|
"http://[::1]:*",
|
|
],
|
|
)
|
|
# Reset session manager so it picks up new security settings
|
|
self._session_manager = None
|
|
|
|
def _register_default_protocol_handlers(self) -> None:
|
|
"""Register default handlers for MCP protocol methods not covered by FastMCP.
|
|
|
|
Registers logging/setLevel, resources/subscribe, resources/unsubscribe,
|
|
and completion/complete so the server advertises these capabilities and
|
|
responds to requests correctly.
|
|
"""
|
|
|
|
# logging/setLevel — store the level so Context.log() can filter
|
|
self._client_log_level: str = "debug" # Default: send all levels
|
|
|
|
@self._mcp_server.set_logging_level()
|
|
async def _handle_set_logging_level(level: Any) -> None:
|
|
self._client_log_level = str(level)
|
|
|
|
# resources/subscribe + unsubscribe — track per-URI subscriptions
|
|
# Uses WeakSet so that subscriptions are automatically cleaned up when
|
|
# a client disconnects and its session object is garbage-collected,
|
|
# preventing the memory leak described in GitHub issue #1092.
|
|
self._resource_subscriptions: dict[str, weakref.WeakSet] = {} # uri -> WeakSet of sessions
|
|
|
|
@self._mcp_server.subscribe_resource()
|
|
async def _handle_subscribe(uri: Any) -> None:
|
|
session = self._current_session()
|
|
if session:
|
|
self._resource_subscriptions.setdefault(str(uri), weakref.WeakSet()).add(session)
|
|
|
|
@self._mcp_server.unsubscribe_resource()
|
|
async def _handle_unsubscribe(uri: Any) -> None:
|
|
session = self._current_session()
|
|
if session:
|
|
subscribers = self._resource_subscriptions.get(str(uri))
|
|
if subscribers:
|
|
subscribers.discard(session)
|
|
if not subscribers:
|
|
del self._resource_subscriptions[str(uri)]
|
|
|
|
# Patch capabilities to advertise subscribe support
|
|
original_get_capabilities = self._mcp_server.get_capabilities
|
|
|
|
def _patched_get_capabilities(notification_options: Any, experimental_capabilities: Any) -> Any:
|
|
caps = original_get_capabilities(notification_options, experimental_capabilities)
|
|
if caps.resources is not None:
|
|
caps.resources.subscribe = True
|
|
return caps
|
|
|
|
self._mcp_server.get_capabilities = _patched_get_capabilities # type: ignore[assignment]
|
|
|
|
# completion/complete — default empty completions (override via mcp.completion())
|
|
@self._mcp_server.completion()
|
|
async def _handle_completion(_ref: Any, _argument: Any, _context: Any = None) -> Completion:
|
|
return Completion(values=[], total=0, hasMore=False)
|
|
|
|
async def notify_resource_updated(self, uri: str) -> None:
|
|
"""Notify all subscribed sessions that a resource has been updated.
|
|
|
|
Broadcasts a ``notifications/resources/updated`` message to every session
|
|
that called ``resources/subscribe`` for this URI.
|
|
|
|
Can be called from within a tool handler or from any async context.
|
|
|
|
Args:
|
|
uri: The URI of the resource that was updated.
|
|
"""
|
|
subscribers = self._resource_subscriptions.get(uri)
|
|
if not subscribers:
|
|
# Clean up empty entry left behind after all sessions were GC'd
|
|
self._resource_subscriptions.pop(uri, None)
|
|
return
|
|
|
|
# Iterate a snapshot; the WeakSet may shrink during iteration
|
|
# if sessions are garbage-collected concurrently.
|
|
for session in list(subscribers):
|
|
try:
|
|
await session.send_resource_updated(uri=uri)
|
|
except Exception:
|
|
pass # Session may have disconnected
|
|
|
|
def streamable_http_app(self):
|
|
"""Override to add our custom middleware."""
|
|
from starlette.middleware.cors import CORSMiddleware
|
|
|
|
app = super().streamable_http_app()
|
|
|
|
# Add MCP logging middleware (cast to satisfy type checker)
|
|
app.add_middleware(
|
|
cast(type, MCPLoggingMiddleware),
|
|
debug_level=self.debug_level,
|
|
mcp_path=self.mcp_path,
|
|
pretty_print_jsonrpc=self.pretty_print_jsonrpc,
|
|
)
|
|
|
|
# Add auth middleware if provider is configured
|
|
if self._auth:
|
|
app.add_middleware(
|
|
AuthMiddleware,
|
|
auth_provider=self._auth,
|
|
)
|
|
logger.debug("AuthMiddleware added to application")
|
|
|
|
# Add CORS middleware when auth is enabled (handles OPTIONS preflight)
|
|
# Note: allow_credentials=False because allow_origins=["*"] - browsers reject the combination
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=["*"],
|
|
allow_credentials=False,
|
|
allow_methods=["GET", "POST", "OPTIONS"],
|
|
allow_headers=["*"],
|
|
expose_headers=["WWW-Authenticate"],
|
|
)
|
|
|
|
return app
|
|
|
|
def _wrap_handlers_with_middleware(self) -> None:
|
|
handlers = self._mcp_server.request_handlers
|
|
|
|
if self.debug_level >= 1:
|
|
logger.debug(f"Wrapping handlers. Available handlers: {list(handlers.keys())}")
|
|
|
|
def wrap_request(request_cls: type, method: str) -> None:
|
|
if request_cls not in handlers:
|
|
return
|
|
|
|
original = handlers[request_cls]
|
|
|
|
async def wrapped(request: Any) -> ServerResult:
|
|
# Get session ID from HTTP headers if available
|
|
session_id = self._get_session_id_from_request()
|
|
|
|
context = ServerMiddlewareContext(
|
|
message=request.params,
|
|
method=method,
|
|
timestamp=datetime.now(UTC),
|
|
transport=self._transport_type,
|
|
session_id=session_id,
|
|
)
|
|
|
|
async def call_original(_: ServerMiddlewareContext[Any]) -> Any:
|
|
return await original(request)
|
|
|
|
return await self.middleware_manager.process_request(context, call_original)
|
|
|
|
handlers[request_cls] = wrapped
|
|
|
|
wrap_request(CallToolRequest, "tools/call")
|
|
wrap_request(ReadResourceRequest, "resources/read")
|
|
wrap_request(GetPromptRequest, "prompts/get")
|
|
wrap_request(ListToolsRequest, "tools/list")
|
|
wrap_request(ListResourcesRequest, "resources/list")
|
|
wrap_request(ListPromptsRequest, "prompts/list")
|
|
wrap_request(SetLevelRequest, "logging/setLevel")
|
|
wrap_request(SubscribeRequest, "resources/subscribe")
|
|
wrap_request(UnsubscribeRequest, "resources/unsubscribe")
|
|
wrap_request(CompleteRequest, "completion/complete")
|
|
|
|
def run( # type: ignore[override]
|
|
self,
|
|
transport: TransportType = "streamable-http",
|
|
host: str | None = None,
|
|
port: int | None = None,
|
|
reload: bool = False,
|
|
debug: bool = False,
|
|
) -> None:
|
|
"""Run the MCP server.
|
|
|
|
Args:
|
|
transport: Transport protocol to use ("stdio", "streamable-http" or "sse")
|
|
host: Host to bind to. If provided, overrides __init__ value and reconfigures
|
|
DNS rebinding protection accordingly.
|
|
port: Port to bind to. If not provided, uses the value from __init__.
|
|
reload: Whether to enable auto-reload
|
|
debug: Whether to enable debug mode. Overrides the server's debug setting,
|
|
adds /docs and /openmcp.json endpoints if not already added.
|
|
"""
|
|
# Use settings from __init__, run() values override
|
|
final_host = host if host is not None else self.settings.host
|
|
final_port = port if port is not None else self.settings.port
|
|
|
|
# If host changed, update settings and rebuild app
|
|
if final_host != self.settings.host:
|
|
self.settings.host = final_host
|
|
self.settings.port = final_port
|
|
if self._dns_rebinding_protection:
|
|
self._apply_dns_rebinding_protection(final_host)
|
|
# Rebuild app with updated settings
|
|
self._session_manager = None
|
|
self.app = self.streamable_http_app()
|
|
|
|
# Override debug_level if debug=True is passed to run()
|
|
if debug and self.debug_level < 1:
|
|
self.debug_level = 1
|
|
self._add_dev_routes()
|
|
# Rebuild the Starlette app so the new routes are included
|
|
self._session_manager = None
|
|
self.app = self.streamable_http_app()
|
|
|
|
self._transport_type = transport
|
|
track_server_run_from_server(self, transport, final_host, final_port, _telemetry)
|
|
|
|
self._wrap_handlers_with_middleware()
|
|
|
|
runner = ServerRunner(self)
|
|
runner.run(transport=transport, host=final_host, port=final_port, reload=reload)
|
|
|
|
def get_context(self) -> MCPContext: # type: ignore[override]
|
|
"""Use the extended MCP-Use context that adds convenience helpers."""
|
|
return MCPContext(request_context=self._get_request_context(), fastmcp=self) # type: ignore[override]
|
|
|
|
def _resource_is_template(self, fn: AnyFunction, uri: str) -> bool:
|
|
has_uri_params = "{" in uri and "}" in uri
|
|
if has_uri_params:
|
|
return True
|
|
return bool(inspect.signature(fn).parameters)
|
|
|
|
def _current_session(self) -> ServerSession | None:
|
|
request_context = self._get_request_context()
|
|
if request_context is None:
|
|
return None
|
|
return request_context.session
|
|
|
|
def _get_request_context(self):
|
|
try:
|
|
return self._mcp_server.request_context
|
|
except LookupError:
|
|
return None
|
|
|
|
def _get_session_id(self) -> str | None:
|
|
"""Get session ID from the session object (deprecated - use _get_session_id_from_request)."""
|
|
session = self._current_session()
|
|
if session is None:
|
|
return None
|
|
|
|
try:
|
|
return session.session_id # type: ignore[attr-defined]
|
|
except AttributeError:
|
|
try:
|
|
return session.id # type: ignore[attr-defined]
|
|
except AttributeError:
|
|
return None
|
|
|
|
def _get_session_id_from_request(self) -> str | None:
|
|
"""Get session ID from HTTP request headers (for Streamable HTTP transport).
|
|
|
|
The session ID is managed at the transport layer and sent via the
|
|
mcp-session-id header according to the MCP specification.
|
|
"""
|
|
request_context = self._get_request_context()
|
|
if request_context is None:
|
|
return None
|
|
|
|
# Try to get the HTTP request from context
|
|
request = getattr(request_context, "request", None)
|
|
if request is None:
|
|
return None
|
|
|
|
# Extract mcp-session-id header
|
|
try:
|
|
return request.headers.get("mcp-session-id")
|
|
except (AttributeError, KeyError):
|
|
return None
|