diff --git a/README.md b/README.md index 701b7be..9daf850 100644 --- a/README.md +++ b/README.md @@ -14,24 +14,20 @@ A curated collection of production-ready Model Context Protocol (MCP) servers pu ## 📦 Available servers -| Actor | Description | Actor badge | Original author | -|----------------------------------------------------------|-------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------| -| **[Brave Search MCP Server](./brave-search-mcp-server/)** | Web search capabilities powered by Brave Search | [![Brave Search MCP Server](https://apify.com/actor-badge?actor=mcp-servers/brave-search-mcp-server)](https://apify.com/mcp-servers/brave-search-mcp-server) | [MCP Community](https://github.com/modelcontextprotocol/servers-archived/tree/main/src/brave-search) | -| **[BrowserBase MCP Server](./browserbase-mcp-server/)** | Cloud web automation capabilities | [![BrowserBase MCP Server](https://apify.com/actor-badge?actor=mcp-servers/browserbase-mcp-server)](https://apify.com/mcp-servers/browserbase-mcp-server) | [BrowserBase](https://github.com/browserbase/mcp-server-browserbase) | -| **[Docfork MCP Server](./docfork-mcp-server/)** | FreshFresh docs for your AI code editor | [![Docfork MCP Server](https://apify.com/actor-badge?actor=mcp-servers/brave-search-mcp-server)](https://apify.com/mcp-servers/brave-search-mcp-server) | [Docfork.AI](https://x.com/docfork_ai) | -| **[Perplexity Sonar MCP Server](./perplexity-mcp-server/)** | AI-powered search and information retrieval | [![Perplexity Soner MCP Server](https://apify.com/actor-badge?actor=mcp-servers/perplexity-sonar-mcp-server)](https://apify.com/mcp-servers/perplexity-sonar-mcp-server) | [Perplexity AI](https://github.com/ppl-ai/modelcontextprotocol) | -| **[Firecrawl MCP Server](./firecrawl-mcp-server/)** | Web crawling and content extraction | [![Firecrawl MCP Server](https://apify.com/actor-badge?actor=mcp-servers/firecrawl-mcp-server)](https://apify.com/actor/mcp-servers/firecrawl-mcp-server) | [Mendable AI](https://github.com/mendableai/firecrawl-mcp-server) | -| **[Slide Speak MCP Server](./slide-speak-mcp-server/)** | Presentation and speech synthesis | [![Slide Speak MCP Server](https://apify.com/actor-badge?actor=mcp-servers/slidespeak-mcp-server)](https://apify.com/mcp-servers/slidespeak-mcp-server) | [SlideSpeak](https://github.com/slidespeak/slidespeak-mcp) | -| **[Lara Translate MCP Server](./lara-translate-mcp-server/)** | Multi-language translation services | [![Lara Translate MCP Server](https://apify.com/actor-badge?actor=mcp-servers/lara-translate-mcp-server)](https://apify.com/mcp-servers/lara-translate-mcp-server) | [Translated](https://github.com/translated/lara-mcp) | -| **[Calculator MCP Server](./calculator-MCP-server/)** | Mathematical computations and calculations | [![Calculator MCP Server](https://apify.com/actor-badge?actor=mcp-servers/calculator-mcp-server)](https://apify.com/mcp-servers/calculator-mcp-server) | [Githejie](https://github.com/githejie/mcp-server-calculator) | -| **[Kiwi MCP Server](./kiwi-mcp-server/)** | Flight search and booking using Kiwi.com | [![Kiwi MCP Server](https://apify.com/actor-badge?actor=mcp-servers/kiwi-mcp-server)](https://apify.com/mcp-servers/kiwi-mcp-server) | [Kiwi.com](https://github.com/alpic-ai/kiwi-mcp-server-public) | -| **[National Parks MCP Server](./national-parks/)** | Geographic and park information | [![National Parks MCP Server](https://apify.com/actor-badge?actor=mcp-servers/national-parks-mcp-server)](https://apify.com/mcp-servers/national-parks-mcp-server) | [KyrieTangSheng](https://github.com/KyrieTangSheng/mcp-server-nationalparks) | -| **[Context7 MCP Server](./context7-mcp-server/)** | Up-to-date code docs for any programming library | [![Context7 MCP Server](https://apify.com/actor-badge?actor=mcp-servers/context7-mcp-server)](https://apify.com/mcp-servers/context7-mcp-server) | [Upstash](https://github.com/upstash/context7) | -| **[DeepL MCP Server](./deepl-mcp-server/)** | Translation capabilities using the DeepL API | [![DeepL MCP Server](https://apify.com/actor-badge?actor=mcp-servers/deepl-mcp-server)](https://apify.com/mcp-servers/deepl-mcp-server) | [DeepLcom](https://github.com/DeepLcom/deepl-mcp-server) | -| **[Pure.md MCP Server](./pure-md-mcp-server/)** | Web content extraction and search capabilities powered by Pure.md | [![Pure.md MCP Server](https://apify.com/actor-badge?actor=mcp-servers/pure-md-mcp-server)](https://apify.com/mcp-servers/pure-md-mcp-server) | [Pure.md](https://github.com/puremd/puremd-mcp) | -| **[Financial Datasets MCP Server](./financial-datasets-mcp-server/)** | Stock market data, financial statements, and cryptocurrency information | [![Financial Datasets MCP Server](https://apify.com/actor-badge?actor=mcp-servers/financial-datasets-mcp-server)](https://apify.com/mcp-servers/financial-datasets-mcp-server) | [Financial Datasets](https://github.com/financial-datasets/mcp-server) | -| **[Microsoft Docs MCP Server](./microsoft-docs-mcp-server/)** | Access Microsoft's official documentation with semantic search | [![Microsoft Docs MCP Server](https://apify.com/actor-badge?actor=mcp-servers/microsoft-docs-mcp-server)](https://apify.com/mcp-servers/microsoft-docs-mcp-server) | [Microsoft](https://github.com/MicrosoftDocs/mcp) | -| **[TomTom MCP Server](./tomtom-mcp-server/)** | Geospatial services including search, routing, traffic and static maps | [![TomTom MCP Server](https://apify.com/actor-badge?actor=mcp-servers/tomtom-mcp-server)](https://apify.com/mcp-servers/tomtom-mcp-server) | [TomTom International](https://github.com/tomtom-international/tomtom-mcp) | +| Actor | Description | Actor badge | Original author | +|----------------------------------------------------------|-------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------| +| **[Brave Search MCP Server](./brave-search-mcp-server/)** | Web search capabilities powered by Brave Search | [![Brave Search MCP Server](https://apify.com/actor-badge?actor=agentify/brave-search-mcp-server)](https://apify.com/agentify/brave-search-mcp-server) | [MCP Community](https://github.com/modelcontextprotocol/servers-archived/tree/main/src/brave-search) | +| **[BrowserBase MCP Server](./browserbase-mcp-server/)** | Cloud web automation capabilities | [![BrowserBase MCP Server](https://apify.com/actor-badge?actor=agentify/browserbase-mcp-server)](https://apify.com/agentify/browserbase-mcp-server) | [BrowserBase](https://github.com/browserbase/mcp-server-browserbase) | +| **[Docfork MCP Server](./docfork-mcp-server/)** | FreshFresh docs for your AI code editor | [![Docfork MCP Server](https://apify.com/actor-badge?actor=agentify/docfork-mcp-server)](https://apify.com/agentify/docfork-mcp-server) | [Docfork.AI](https://x.com/docfork_ai) | +| **[Perplexity Sonar MCP Server](./perplexity-mcp-server/)** | AI-powered search and information retrieval | [![Perplexity Soner MCP Server](https://apify.com/actor-badge?actor=agentify/perplexity-sonar-mcp-server)](https://apify.com/agentify/perplexity-sonar-mcp-server) | [Perplexity AI](https://github.com/ppl-ai/modelcontextprotocol) | +| **[Firecrawl MCP Server](./firecrawl-mcp-server/)** | Web crawling and content extraction | [![Firecrawl MCP Server](https://apify.com/actor-badge?actor=agentify/firecrawl-mcp-server)](https://apify.com/agentify/firecrawl-mcp-server) | [Mendable AI](https://github.com/mendableai/firecrawl-mcp-server) | +| **[Slide Speak MCP Server](./slide-speak-mcp-server/)** | Presentation and speech synthesis | [![Slide Speak MCP Server](https://apify.com/actor-badge?actor=agentify/slidespeak-mcp-server)](https://apify.com/agentify/slidespeak-mcp-server) | +| **[Lara Translate MCP Server](./lara-translate-mcp-server/)** | Multi-language translation services | [![Lara Translate MCP Server](https://apify.com/actor-badge?actor=agentify/lara-translate-mcp-server)](https://apify.com/agentify/lara-translate-mcp-server) | [Translated](https://github.com/translated/lara-mcp) | +| **[Calculator MCP Server](./calculator-MCP-server/)** | Mathematical computations and calculations | [![Calculator MCP Server](https://apify.com/actor-badge?actor=agentify/calculator-mcp-server)](https://apify.com/agentify/calculator-mcp-server) | [Githejie](https://github.com/githejie/mcp-server-calculator) | +| **[Kiwi MCP Server](./kiwi-mcp-server/)** | Flight search and booking using Kiwi.com | [![Kiwi MCP Server](https://apify.com/actor-badge?actor=agentify/kiwi-mcp-server)](https://apify.com/agentify/kiwi-mcp-server) | [Kiwi.com](https://github.com/alpic-ai/kiwi-mcp-server-public) | +| **[National Parks MCP Server](./national-parks/)** | Geographic and park information | [![National Parks MCP Server](https://apify.com/actor-badge?actor=agentify/national-parks-mcp-server)](https://apify.com/agentify/national-parks-mcp-server) | [KyrieTangSheng](https://github.com/KyrieTangSheng/mcp-server-nationalparks) | +| **[Context7 MCP Server](./context7-mcp-server/)** | Up-to-date code docs for any programming library | [![Context7 MCP Server](https://apify.com/actor-badge?actor=agentify/context7-mcp-server)](https://apify.com/agentify/context7-mcp-server) | [Upstash](https://github.com/upstash/context7) | +| **[DeepL MCP Server](./deepl-mcp-server/)** | Translation capabilities using the DeepL API | [![DeepL MCP Server](https://apify.com/actor-badge?actor=agentify/deepl-mcp-server)](https://apify.com/agentify/deepl-mcp-server) | [DeepLcom](https://github.com/DeepLcom/deepl-mcp-server) | ## 🙏 Credits These are wrappers—original code by: Brave Software, Perplexity AI, Mendable AI, SlideSpeak, Translated, Githejie, Kiwi.com, KyrieTangSheng, Upstash, Financial Datasets, Microsoft, and others. diff --git a/calculator-MCP-server/src/server.py b/calculator-MCP-server/src/server.py index ecf79f3..6746c25 100644 --- a/calculator-MCP-server/src/server.py +++ b/calculator-MCP-server/src/server.py @@ -160,7 +160,7 @@ def _log_request(request: Request) -> None: extra={ 'method': request.method, 'path': str(request.url.path), - 'mcp_session_id': request.headers.get('mcp-session-id'), + 'mcp_session_id': ProxyServer._get_session_id_from_headers(request.headers), }, ) @@ -229,6 +229,24 @@ def _cleanup_session_timer(self, session_id: str) -> None: timer.cancel() del self._session_timers[session_id] + @staticmethod + def _get_session_id_from_headers(headers: Any) -> str | None: + """Extract session ID from headers, trying both hyphen and underscore variants. + + HTTP headers are case-insensitive per spec, and Starlette's Request.headers + handles this automatically. We only need to check for hyphen vs underscore variants. + + Args: + headers: Either a Starlette Request.headers object or a dict + + Returns: + Session ID string if found, None otherwise + """ + for key in ('mcp-session-id', 'mcp_session_id'): + if value := headers.get(key): + return value + return None + @staticmethod def _validate_config(client_type: ServerType, config: ServerParameters) -> ServerParameters | None: """Validate and return the appropriate server parameters.""" @@ -252,7 +270,7 @@ def _create_capturing_send( async def capturing_send(message: dict[str, Any]) -> None: if message.get('type') == 'http.response.start': headers = {k.decode('latin-1').lower(): v.decode('latin-1') for k, v in message.get('headers', [])} - if sid := headers.get('mcp-session-id'): + if sid := ProxyServer._get_session_id_from_headers(headers): session_id_from_resp['sid'] = sid await send(message) @@ -334,7 +352,7 @@ async def handle_streamable_http(scope: Scope, receive: Receive, send: Send) -> if scope['method'] == 'DELETE': await session_manager.handle_request(scope, receive, send) - if req_sid := request.headers.get('mcp-session-id'): + if req_sid := self._get_session_id_from_headers(request.headers): self._cleanup_session_last_activity(req_sid) self._cleanup_session_timer(req_sid) return @@ -342,7 +360,7 @@ async def handle_streamable_http(scope: Scope, receive: Receive, send: Send) -> session_id_from_resp: dict[str, str | None] = {'sid': None} capturing_send = self._create_capturing_send(send, session_id_from_resp) - if req_sid := request.headers.get('mcp-session-id'): + if req_sid := self._get_session_id_from_headers(request.headers): self._touch_session(req_sid, session_manager) await session_manager.handle_request(scope, receive, capturing_send) # type: ignore[arg-type] diff --git a/context7-mcp-server/src/server.py b/context7-mcp-server/src/server.py index a247ae9..78a0921 100644 --- a/context7-mcp-server/src/server.py +++ b/context7-mcp-server/src/server.py @@ -165,7 +165,7 @@ def _log_request(request: Request) -> None: extra={ 'method': request.method, 'path': str(request.url.path), - 'mcp_session_id': request.headers.get('mcp-session-id'), + 'mcp_session_id': ProxyServer._get_session_id_from_headers(request.headers), }, ) @@ -234,6 +234,14 @@ def _cleanup_session_timer(self, session_id: str) -> None: timer.cancel() del self._session_timers[session_id] + @staticmethod + def _get_session_id_from_headers(headers: Any) -> str | None: + """Extract session ID from headers supporting dash and underscore variants.""" + for key in ('mcp-session-id', 'mcp_session_id'): + if value := headers.get(key): + return value + return None + @staticmethod def _validate_config(client_type: ServerType, config: ServerParameters) -> ServerParameters | None: """Validate and return the appropriate server parameters.""" @@ -257,7 +265,7 @@ def _create_capturing_send( async def capturing_send(message: dict[str, Any]) -> None: if message.get('type') == 'http.response.start': headers = {k.decode('latin-1').lower(): v.decode('latin-1') for k, v in message.get('headers', [])} - if sid := headers.get('mcp-session-id'): + if sid := ProxyServer._get_session_id_from_headers(headers): session_id_from_resp['sid'] = sid await send(message) @@ -341,7 +349,7 @@ async def handle_streamable_http(scope: Scope, receive: Receive, send: Send) -> if scope['method'] == 'DELETE': await session_manager.handle_request(scope, receive, send) - if req_sid := request.headers.get('mcp-session-id'): + if req_sid := self._get_session_id_from_headers(request.headers): self._cleanup_session_last_activity(req_sid) self._cleanup_session_timer(req_sid) return @@ -352,7 +360,7 @@ async def handle_streamable_http(scope: Scope, receive: Receive, send: Send) -> capturing_send = self._create_capturing_send(send, session_id_from_resp) # Log and touch existing session if present on request - if req_sid := request.headers.get('mcp-session-id'): + if req_sid := self._get_session_id_from_headers(request.headers): self._touch_session(req_sid, session_manager) await session_manager.handle_request(scope, receive, capturing_send) # type: ignore[arg-type] diff --git a/dictionary-mcp-server/src/server.py b/dictionary-mcp-server/src/server.py index a247ae9..78a0921 100644 --- a/dictionary-mcp-server/src/server.py +++ b/dictionary-mcp-server/src/server.py @@ -165,7 +165,7 @@ def _log_request(request: Request) -> None: extra={ 'method': request.method, 'path': str(request.url.path), - 'mcp_session_id': request.headers.get('mcp-session-id'), + 'mcp_session_id': ProxyServer._get_session_id_from_headers(request.headers), }, ) @@ -234,6 +234,14 @@ def _cleanup_session_timer(self, session_id: str) -> None: timer.cancel() del self._session_timers[session_id] + @staticmethod + def _get_session_id_from_headers(headers: Any) -> str | None: + """Extract session ID from headers supporting dash and underscore variants.""" + for key in ('mcp-session-id', 'mcp_session_id'): + if value := headers.get(key): + return value + return None + @staticmethod def _validate_config(client_type: ServerType, config: ServerParameters) -> ServerParameters | None: """Validate and return the appropriate server parameters.""" @@ -257,7 +265,7 @@ def _create_capturing_send( async def capturing_send(message: dict[str, Any]) -> None: if message.get('type') == 'http.response.start': headers = {k.decode('latin-1').lower(): v.decode('latin-1') for k, v in message.get('headers', [])} - if sid := headers.get('mcp-session-id'): + if sid := ProxyServer._get_session_id_from_headers(headers): session_id_from_resp['sid'] = sid await send(message) @@ -341,7 +349,7 @@ async def handle_streamable_http(scope: Scope, receive: Receive, send: Send) -> if scope['method'] == 'DELETE': await session_manager.handle_request(scope, receive, send) - if req_sid := request.headers.get('mcp-session-id'): + if req_sid := self._get_session_id_from_headers(request.headers): self._cleanup_session_last_activity(req_sid) self._cleanup_session_timer(req_sid) return @@ -352,7 +360,7 @@ async def handle_streamable_http(scope: Scope, receive: Receive, send: Send) -> capturing_send = self._create_capturing_send(send, session_id_from_resp) # Log and touch existing session if present on request - if req_sid := request.headers.get('mcp-session-id'): + if req_sid := self._get_session_id_from_headers(request.headers): self._touch_session(req_sid, session_manager) await session_manager.handle_request(scope, receive, capturing_send) # type: ignore[arg-type] diff --git a/docfork-mcp-server/src/server.py b/docfork-mcp-server/src/server.py index a247ae9..78a0921 100644 --- a/docfork-mcp-server/src/server.py +++ b/docfork-mcp-server/src/server.py @@ -165,7 +165,7 @@ def _log_request(request: Request) -> None: extra={ 'method': request.method, 'path': str(request.url.path), - 'mcp_session_id': request.headers.get('mcp-session-id'), + 'mcp_session_id': ProxyServer._get_session_id_from_headers(request.headers), }, ) @@ -234,6 +234,14 @@ def _cleanup_session_timer(self, session_id: str) -> None: timer.cancel() del self._session_timers[session_id] + @staticmethod + def _get_session_id_from_headers(headers: Any) -> str | None: + """Extract session ID from headers supporting dash and underscore variants.""" + for key in ('mcp-session-id', 'mcp_session_id'): + if value := headers.get(key): + return value + return None + @staticmethod def _validate_config(client_type: ServerType, config: ServerParameters) -> ServerParameters | None: """Validate and return the appropriate server parameters.""" @@ -257,7 +265,7 @@ def _create_capturing_send( async def capturing_send(message: dict[str, Any]) -> None: if message.get('type') == 'http.response.start': headers = {k.decode('latin-1').lower(): v.decode('latin-1') for k, v in message.get('headers', [])} - if sid := headers.get('mcp-session-id'): + if sid := ProxyServer._get_session_id_from_headers(headers): session_id_from_resp['sid'] = sid await send(message) @@ -341,7 +349,7 @@ async def handle_streamable_http(scope: Scope, receive: Receive, send: Send) -> if scope['method'] == 'DELETE': await session_manager.handle_request(scope, receive, send) - if req_sid := request.headers.get('mcp-session-id'): + if req_sid := self._get_session_id_from_headers(request.headers): self._cleanup_session_last_activity(req_sid) self._cleanup_session_timer(req_sid) return @@ -352,7 +360,7 @@ async def handle_streamable_http(scope: Scope, receive: Receive, send: Send) -> capturing_send = self._create_capturing_send(send, session_id_from_resp) # Log and touch existing session if present on request - if req_sid := request.headers.get('mcp-session-id'): + if req_sid := self._get_session_id_from_headers(request.headers): self._touch_session(req_sid, session_manager) await session_manager.handle_request(scope, receive, capturing_send) # type: ignore[arg-type] diff --git a/financial-datasets-mcp-server/src/server.py b/financial-datasets-mcp-server/src/server.py index df9fd9a..bcd3555 100644 --- a/financial-datasets-mcp-server/src/server.py +++ b/financial-datasets-mcp-server/src/server.py @@ -165,7 +165,7 @@ def _log_request(request: Request) -> None: extra={ 'method': request.method, 'path': str(request.url.path), - 'mcp_session_id': request.headers.get('mcp-session-id'), + 'mcp_session_id': ProxyServer._get_session_id_from_headers(request.headers), }, ) @@ -234,6 +234,14 @@ def _cleanup_session_timer(self, session_id: str) -> None: timer.cancel() del self._session_timers[session_id] + @staticmethod + def _get_session_id_from_headers(headers: Any) -> str | None: + """Extract session ID from headers supporting dash and underscore variants.""" + for key in ('mcp-session-id', 'mcp_session_id'): + if value := headers.get(key): + return value + return None + @staticmethod def _validate_config(client_type: ServerType, config: ServerParameters) -> ServerParameters | None: """Validate and return the appropriate server parameters.""" @@ -257,7 +265,7 @@ def _create_capturing_send( async def capturing_send(message: dict[str, Any]) -> None: if message.get('type') == 'http.response.start': headers = {k.decode('latin-1').lower(): v.decode('latin-1') for k, v in message.get('headers', [])} - if sid := headers.get('mcp-session-id'): + if sid := ProxyServer._get_session_id_from_headers(headers): session_id_from_resp['sid'] = sid await send(message) @@ -339,7 +347,7 @@ async def handle_streamable_http(scope: Scope, receive: Receive, send: Send) -> if scope['method'] == 'DELETE': await session_manager.handle_request(scope, receive, send) - if req_sid := request.headers.get('mcp-session-id'): + if req_sid := self._get_session_id_from_headers(request.headers): self._cleanup_session_last_activity(req_sid) self._cleanup_session_timer(req_sid) return @@ -347,7 +355,7 @@ async def handle_streamable_http(scope: Scope, receive: Receive, send: Send) -> session_id_from_resp: dict[str, str | None] = {'sid': None} capturing_send = self._create_capturing_send(send, session_id_from_resp) - if req_sid := request.headers.get('mcp-session-id'): + if req_sid := self._get_session_id_from_headers(request.headers): self._touch_session(req_sid, session_manager) await session_manager.handle_request(scope, receive, capturing_send) # type: ignore[arg-type] diff --git a/kiwi-mcp-server/src/server.py b/kiwi-mcp-server/src/server.py index 6c68104..4265211 100644 --- a/kiwi-mcp-server/src/server.py +++ b/kiwi-mcp-server/src/server.py @@ -179,7 +179,7 @@ def _log_request(request: Request) -> None: extra={ 'method': request.method, 'path': str(request.url.path), - 'mcp_session_id': request.headers.get('mcp-session-id'), + 'mcp_session_id': ProxyServer._get_session_id_from_headers(request.headers), }, ) @@ -248,6 +248,14 @@ def _cleanup_session_timer(self, session_id: str) -> None: timer.cancel() del self._session_timers[session_id] + @staticmethod + def _get_session_id_from_headers(headers: Any) -> str | None: + """Extract session ID from headers supporting dash and underscore variants.""" + for key in ('mcp-session-id', 'mcp_session_id'): + if value := headers.get(key): + return value + return None + def _touch_session(self, session_id: str, session_manager: StreamableHTTPSessionManager) -> None: """Record activity for a session and (re)schedule inactivity termination.""" self._session_last_activity[session_id] = time.time() @@ -322,7 +330,7 @@ def _create_capturing_send( async def capturing_send(message: dict[str, Any]) -> None: if message.get('type') == 'http.response.start': headers = {k.decode('latin-1').lower(): v.decode('latin-1') for k, v in message.get('headers', [])} - if sid := headers.get('mcp-session-id'): + if sid := ProxyServer._get_session_id_from_headers(headers): session_id_from_resp['sid'] = sid await send(message) @@ -406,7 +414,7 @@ async def handle_streamable_http(scope: Scope, receive: Receive, send: Send) -> if scope['method'] == 'DELETE': await session_manager.handle_request(scope, receive, send) - if req_sid := request.headers.get('mcp-session-id'): + if req_sid := self._get_session_id_from_headers(request.headers): self._cleanup_session_last_activity(req_sid) self._cleanup_session_timer(req_sid) return @@ -417,7 +425,7 @@ async def handle_streamable_http(scope: Scope, receive: Receive, send: Send) -> capturing_send = self._create_capturing_send(send, session_id_from_resp) # Log and touch existing session if present on request - if req_sid := request.headers.get('mcp-session-id'): + if req_sid := self._get_session_id_from_headers(request.headers): self._touch_session(req_sid, session_manager) await session_manager.handle_request(scope, receive, capturing_send) # type: ignore[arg-type] diff --git a/mindmap-mcp-server/src/server.py b/mindmap-mcp-server/src/server.py index 07e84cd..053f79f 100644 --- a/mindmap-mcp-server/src/server.py +++ b/mindmap-mcp-server/src/server.py @@ -165,7 +165,7 @@ def _log_request(request: Request) -> None: extra={ 'method': request.method, 'path': str(request.url.path), - 'mcp_session_id': request.headers.get('mcp-session-id'), + 'mcp_session_id': ProxyServer._get_session_id_from_headers(request.headers), }, ) @@ -234,6 +234,14 @@ def _cleanup_session_timer(self, session_id: str) -> None: timer.cancel() del self._session_timers[session_id] + @staticmethod + def _get_session_id_from_headers(headers: Any) -> str | None: + """Extract session ID from headers supporting dash and underscore variants.""" + for key in ('mcp-session-id', 'mcp_session_id'): + if value := headers.get(key): + return value + return None + @staticmethod def _validate_config(client_type: ServerType, config: ServerParameters) -> ServerParameters | None: """Validate and return the appropriate server parameters.""" @@ -257,7 +265,7 @@ def _create_capturing_send( async def capturing_send(message: dict[str, Any]) -> None: if message.get('type') == 'http.response.start': headers = {k.decode('latin-1').lower(): v.decode('latin-1') for k, v in message.get('headers', [])} - if sid := headers.get('mcp-session-id'): + if sid := ProxyServer._get_session_id_from_headers(headers): session_id_from_resp['sid'] = sid await send(message) @@ -341,7 +349,7 @@ async def handle_streamable_http(scope: Scope, receive: Receive, send: Send) -> if scope['method'] == 'DELETE': await session_manager.handle_request(scope, receive, send) - if req_sid := request.headers.get('mcp-session-id'): + if req_sid := self._get_session_id_from_headers(request.headers): self._cleanup_session_last_activity(req_sid) self._cleanup_session_timer(req_sid) return @@ -352,7 +360,7 @@ async def handle_streamable_http(scope: Scope, receive: Receive, send: Send) -> capturing_send = self._create_capturing_send(send, session_id_from_resp) # Log and touch existing session if present on request - if req_sid := request.headers.get('mcp-session-id'): + if req_sid := self._get_session_id_from_headers(request.headers): self._touch_session(req_sid, session_manager) await session_manager.handle_request(scope, receive, capturing_send) # type: ignore[arg-type] diff --git a/open-strategy-partners-mcp-server/src/server.py b/open-strategy-partners-mcp-server/src/server.py index 2243b75..3ca864c 100644 --- a/open-strategy-partners-mcp-server/src/server.py +++ b/open-strategy-partners-mcp-server/src/server.py @@ -165,7 +165,7 @@ def _log_request(request: Request) -> None: extra={ 'method': request.method, 'path': str(request.url.path), - 'mcp_session_id': request.headers.get('mcp-session-id'), + 'mcp_session_id': ProxyServer._get_session_id_from_headers(request.headers), }, ) @@ -234,6 +234,14 @@ def _cleanup_session_timer(self, session_id: str) -> None: timer.cancel() del self._session_timers[session_id] + @staticmethod + def _get_session_id_from_headers(headers: Any) -> str | None: + """Extract session ID from headers supporting dash and underscore variants.""" + for key in ('mcp-session-id', 'mcp_session_id'): + if value := headers.get(key): + return value + return None + @staticmethod def _validate_config(client_type: ServerType, config: ServerParameters) -> ServerParameters | None: """Validate and return the appropriate server parameters.""" @@ -257,7 +265,7 @@ def _create_capturing_send( async def capturing_send(message: dict[str, Any]) -> None: if message.get('type') == 'http.response.start': headers = {k.decode('latin-1').lower(): v.decode('latin-1') for k, v in message.get('headers', [])} - if sid := headers.get('mcp-session-id'): + if sid := ProxyServer._get_session_id_from_headers(headers): session_id_from_resp['sid'] = sid await send(message) @@ -344,7 +352,7 @@ async def handle_streamable_http(scope: Scope, receive: Receive, send: Send) -> if scope['method'] == 'DELETE': await session_manager.handle_request(scope, receive, send) - if req_sid := request.headers.get('mcp-session-id'): + if req_sid := self._get_session_id_from_headers(request.headers): self._cleanup_session_last_activity(req_sid) self._cleanup_session_timer(req_sid) return @@ -355,7 +363,7 @@ async def handle_streamable_http(scope: Scope, receive: Receive, send: Send) -> capturing_send = self._create_capturing_send(send, session_id_from_resp) # Log and touch existing session if present on request - if req_sid := request.headers.get('mcp-session-id'): + if req_sid := self._get_session_id_from_headers(request.headers): self._touch_session(req_sid, session_manager) await session_manager.handle_request(scope, receive, capturing_send) # type: ignore[arg-type] diff --git a/pubmed-mcp-server/src/server.py b/pubmed-mcp-server/src/server.py index 6e3cf09..7779611 100644 --- a/pubmed-mcp-server/src/server.py +++ b/pubmed-mcp-server/src/server.py @@ -244,10 +244,18 @@ def _log_request(request: Request) -> None: extra={ 'method': request.method, 'path': str(request.url.path), - 'mcp_session_id': request.headers.get('mcp-session-id'), + 'mcp_session_id': ProxyServer._get_session_id_from_headers(request.headers), }, ) + @staticmethod + def _get_session_id_from_headers(headers: Any) -> str | None: + """Extract session ID from headers supporting dash and underscore variants.""" + for key in ('mcp-session-id', 'mcp_session_id'): + if value := headers.get(key): + return value + return None + @staticmethod def _create_capturing_send( send: Send, session_id_from_resp: dict[str, str | None] @@ -257,7 +265,7 @@ def _create_capturing_send( async def capturing_send(message: dict[str, Any]) -> None: if message.get('type') == 'http.response.start': headers = {k.decode('latin-1').lower(): v.decode('latin-1') for k, v in message.get('headers', [])} - if sid := headers.get('mcp-session-id'): + if sid := ProxyServer._get_session_id_from_headers(headers): session_id_from_resp['sid'] = sid await send(message) @@ -344,7 +352,7 @@ async def handle_streamable_http(scope: Scope, receive: Receive, send: Send) -> if scope['method'] == 'DELETE': await session_manager.handle_request(scope, receive, send) - if req_sid := request.headers.get('mcp-session-id'): + if req_sid := self._get_session_id_from_headers(request.headers): self._cleanup_session_last_activity(req_sid) self._cleanup_session_timer(req_sid) return @@ -355,7 +363,7 @@ async def handle_streamable_http(scope: Scope, receive: Receive, send: Send) -> capturing_send = self._create_capturing_send(send, session_id_from_resp) # Log and touch existing session if present on request - if req_sid := request.headers.get('mcp-session-id'): + if req_sid := self._get_session_id_from_headers(request.headers): self._touch_session(req_sid, session_manager) await session_manager.handle_request(scope, receive, capturing_send) # type: ignore[arg-type] diff --git a/pypi-mcp-server/src/server.py b/pypi-mcp-server/src/server.py index df9fd9a..bcd3555 100644 --- a/pypi-mcp-server/src/server.py +++ b/pypi-mcp-server/src/server.py @@ -165,7 +165,7 @@ def _log_request(request: Request) -> None: extra={ 'method': request.method, 'path': str(request.url.path), - 'mcp_session_id': request.headers.get('mcp-session-id'), + 'mcp_session_id': ProxyServer._get_session_id_from_headers(request.headers), }, ) @@ -234,6 +234,14 @@ def _cleanup_session_timer(self, session_id: str) -> None: timer.cancel() del self._session_timers[session_id] + @staticmethod + def _get_session_id_from_headers(headers: Any) -> str | None: + """Extract session ID from headers supporting dash and underscore variants.""" + for key in ('mcp-session-id', 'mcp_session_id'): + if value := headers.get(key): + return value + return None + @staticmethod def _validate_config(client_type: ServerType, config: ServerParameters) -> ServerParameters | None: """Validate and return the appropriate server parameters.""" @@ -257,7 +265,7 @@ def _create_capturing_send( async def capturing_send(message: dict[str, Any]) -> None: if message.get('type') == 'http.response.start': headers = {k.decode('latin-1').lower(): v.decode('latin-1') for k, v in message.get('headers', [])} - if sid := headers.get('mcp-session-id'): + if sid := ProxyServer._get_session_id_from_headers(headers): session_id_from_resp['sid'] = sid await send(message) @@ -339,7 +347,7 @@ async def handle_streamable_http(scope: Scope, receive: Receive, send: Send) -> if scope['method'] == 'DELETE': await session_manager.handle_request(scope, receive, send) - if req_sid := request.headers.get('mcp-session-id'): + if req_sid := self._get_session_id_from_headers(request.headers): self._cleanup_session_last_activity(req_sid) self._cleanup_session_timer(req_sid) return @@ -347,7 +355,7 @@ async def handle_streamable_http(scope: Scope, receive: Receive, send: Send) -> session_id_from_resp: dict[str, str | None] = {'sid': None} capturing_send = self._create_capturing_send(send, session_id_from_resp) - if req_sid := request.headers.get('mcp-session-id'): + if req_sid := self._get_session_id_from_headers(request.headers): self._touch_session(req_sid, session_manager) await session_manager.handle_request(scope, receive, capturing_send) # type: ignore[arg-type] diff --git a/slide-speak-mcp-server/src/server.py b/slide-speak-mcp-server/src/server.py index df9fd9a..bcd3555 100644 --- a/slide-speak-mcp-server/src/server.py +++ b/slide-speak-mcp-server/src/server.py @@ -165,7 +165,7 @@ def _log_request(request: Request) -> None: extra={ 'method': request.method, 'path': str(request.url.path), - 'mcp_session_id': request.headers.get('mcp-session-id'), + 'mcp_session_id': ProxyServer._get_session_id_from_headers(request.headers), }, ) @@ -234,6 +234,14 @@ def _cleanup_session_timer(self, session_id: str) -> None: timer.cancel() del self._session_timers[session_id] + @staticmethod + def _get_session_id_from_headers(headers: Any) -> str | None: + """Extract session ID from headers supporting dash and underscore variants.""" + for key in ('mcp-session-id', 'mcp_session_id'): + if value := headers.get(key): + return value + return None + @staticmethod def _validate_config(client_type: ServerType, config: ServerParameters) -> ServerParameters | None: """Validate and return the appropriate server parameters.""" @@ -257,7 +265,7 @@ def _create_capturing_send( async def capturing_send(message: dict[str, Any]) -> None: if message.get('type') == 'http.response.start': headers = {k.decode('latin-1').lower(): v.decode('latin-1') for k, v in message.get('headers', [])} - if sid := headers.get('mcp-session-id'): + if sid := ProxyServer._get_session_id_from_headers(headers): session_id_from_resp['sid'] = sid await send(message) @@ -339,7 +347,7 @@ async def handle_streamable_http(scope: Scope, receive: Receive, send: Send) -> if scope['method'] == 'DELETE': await session_manager.handle_request(scope, receive, send) - if req_sid := request.headers.get('mcp-session-id'): + if req_sid := self._get_session_id_from_headers(request.headers): self._cleanup_session_last_activity(req_sid) self._cleanup_session_timer(req_sid) return @@ -347,7 +355,7 @@ async def handle_streamable_http(scope: Scope, receive: Receive, send: Send) -> session_id_from_resp: dict[str, str | None] = {'sid': None} capturing_send = self._create_capturing_send(send, session_id_from_resp) - if req_sid := request.headers.get('mcp-session-id'): + if req_sid := self._get_session_id_from_headers(request.headers): self._touch_session(req_sid, session_manager) await session_manager.handle_request(scope, receive, capturing_send) # type: ignore[arg-type] diff --git a/time-mcp-server/src/server.py b/time-mcp-server/src/server.py index 2243b75..3ca864c 100644 --- a/time-mcp-server/src/server.py +++ b/time-mcp-server/src/server.py @@ -165,7 +165,7 @@ def _log_request(request: Request) -> None: extra={ 'method': request.method, 'path': str(request.url.path), - 'mcp_session_id': request.headers.get('mcp-session-id'), + 'mcp_session_id': ProxyServer._get_session_id_from_headers(request.headers), }, ) @@ -234,6 +234,14 @@ def _cleanup_session_timer(self, session_id: str) -> None: timer.cancel() del self._session_timers[session_id] + @staticmethod + def _get_session_id_from_headers(headers: Any) -> str | None: + """Extract session ID from headers supporting dash and underscore variants.""" + for key in ('mcp-session-id', 'mcp_session_id'): + if value := headers.get(key): + return value + return None + @staticmethod def _validate_config(client_type: ServerType, config: ServerParameters) -> ServerParameters | None: """Validate and return the appropriate server parameters.""" @@ -257,7 +265,7 @@ def _create_capturing_send( async def capturing_send(message: dict[str, Any]) -> None: if message.get('type') == 'http.response.start': headers = {k.decode('latin-1').lower(): v.decode('latin-1') for k, v in message.get('headers', [])} - if sid := headers.get('mcp-session-id'): + if sid := ProxyServer._get_session_id_from_headers(headers): session_id_from_resp['sid'] = sid await send(message) @@ -344,7 +352,7 @@ async def handle_streamable_http(scope: Scope, receive: Receive, send: Send) -> if scope['method'] == 'DELETE': await session_manager.handle_request(scope, receive, send) - if req_sid := request.headers.get('mcp-session-id'): + if req_sid := self._get_session_id_from_headers(request.headers): self._cleanup_session_last_activity(req_sid) self._cleanup_session_timer(req_sid) return @@ -355,7 +363,7 @@ async def handle_streamable_http(scope: Scope, receive: Receive, send: Send) -> capturing_send = self._create_capturing_send(send, session_id_from_resp) # Log and touch existing session if present on request - if req_sid := request.headers.get('mcp-session-id'): + if req_sid := self._get_session_id_from_headers(request.headers): self._touch_session(req_sid, session_manager) await session_manager.handle_request(scope, receive, capturing_send) # type: ignore[arg-type] diff --git a/weather-mcp-server/src/server.py b/weather-mcp-server/src/server.py index e8d4691..49839b4 100644 --- a/weather-mcp-server/src/server.py +++ b/weather-mcp-server/src/server.py @@ -158,15 +158,15 @@ def __init__( # noqa: PLR0913 self._session_timeout_secs: int = SESSION_TIMEOUT_SECS self._agnost_org_id: str | None = os.environ.get('AGNOST_AI_ORG_ID') - @staticmethod - def _log_request(request: Request) -> None: + @classmethod + def _log_request(cls, request: Request) -> None: """Log incoming MCP transport request for diagnostics.""" logger.info( 'MCP transport request', extra={ 'method': request.method, 'path': str(request.url.path), - 'mcp_session_id': request.headers.get('mcp-session-id'), + 'mcp_session_id': cls._get_session_id_from_headers(request.headers), }, ) @@ -184,6 +184,8 @@ async def _idle_close() -> None: await asyncio.sleep(self._session_timeout_secs) # Verify that no new activity occurred during wait last = self._session_last_activity.get(session_id, 0) + # If the session has been idle for less than 90% of the timeout period, + # skip termination to avoid closing sessions too early after activity. if time.time() - last < self._session_timeout_secs * 0.9: return # Activity happened; skip logger.info(f'Terminating idle MCP session {session_id}') @@ -235,6 +237,26 @@ def _cleanup_session_timer(self, session_id: str) -> None: timer.cancel() del self._session_timers[session_id] + @staticmethod + def _get_session_id_from_headers(headers: Any) -> str | None: + """Extract session ID from headers, trying both hyphen and underscore variants. + + HTTP headers are case-insensitive per spec, and Starlette's Request.headers + handles this automatically. We only need to check for hyphen vs underscore variants. + + Args: + headers: Either a Starlette Request.headers object or a dict + + Returns: + Session ID string if found, None otherwise + """ + # Try both hyphen and underscore variants + # Case is handled automatically by Starlette's case-insensitive headers + for key in ('mcp-session-id', 'mcp_session_id'): + if value := headers.get(key): + return value + return None + @staticmethod def _validate_config(client_type: ServerType, config: ServerParameters) -> ServerParameters | None: """Validate and return the appropriate server parameters.""" @@ -249,16 +271,16 @@ def _validate_config(client_type: ServerType, config: ServerParameters) -> Serve except ValidationError as e: raise ValueError(f'Invalid server configuration: {e}') from e - @staticmethod + @classmethod def _create_capturing_send( - send: Send, session_id_from_resp: dict[str, str | None] + cls, send: Send, session_id_from_resp: dict[str, str | None] ) -> Callable[[dict[str, Any]], Awaitable[None]]: """Create a send wrapper that captures session ID from response headers.""" async def capturing_send(message: dict[str, Any]) -> None: if message.get('type') == 'http.response.start': headers = {k.decode('latin-1').lower(): v.decode('latin-1') for k, v in message.get('headers', [])} - if sid := headers.get('mcp-session-id'): + if sid := cls._get_session_id_from_headers(headers): session_id_from_resp['sid'] = sid await send(message) @@ -345,7 +367,7 @@ async def handle_streamable_http(scope: Scope, receive: Receive, send: Send) -> if scope['method'] == 'DELETE': await session_manager.handle_request(scope, receive, send) - if req_sid := request.headers.get('mcp-session-id'): + if req_sid := self._get_session_id_from_headers(request.headers): self._cleanup_session_last_activity(req_sid) self._cleanup_session_timer(req_sid) return @@ -356,7 +378,7 @@ async def handle_streamable_http(scope: Scope, receive: Receive, send: Send) -> capturing_send = self._create_capturing_send(send, session_id_from_resp) # Log and touch existing session if present on request - if req_sid := request.headers.get('mcp-session-id'): + if req_sid := self._get_session_id_from_headers(request.headers): self._touch_session(req_sid, session_manager) await session_manager.handle_request(scope, receive, capturing_send) # type: ignore[arg-type] diff --git a/wikipedia-mcp-server/src/server.py b/wikipedia-mcp-server/src/server.py index a247ae9..50215dc 100644 --- a/wikipedia-mcp-server/src/server.py +++ b/wikipedia-mcp-server/src/server.py @@ -165,7 +165,7 @@ def _log_request(request: Request) -> None: extra={ 'method': request.method, 'path': str(request.url.path), - 'mcp_session_id': request.headers.get('mcp-session-id'), + 'mcp_session_id': ProxyServer._get_session_id_from_headers(request.headers), }, ) @@ -234,6 +234,26 @@ def _cleanup_session_timer(self, session_id: str) -> None: timer.cancel() del self._session_timers[session_id] + @staticmethod + def _get_session_id_from_headers(headers: Any) -> str | None: + """Extract session ID from headers, trying both hyphen and underscore variants. + + HTTP headers are case-insensitive per spec, and Starlette's Request.headers + handles this automatically. We only need to check for hyphen vs underscore variants. + + Args: + headers: Either a Starlette Request.headers object or a dict + + Returns: + Session ID string if found, None otherwise + """ + # Try both hyphen and underscore variants + # Case is handled automatically by Starlette's case-insensitive headers + for key in ('mcp-session-id', 'mcp_session_id'): + if value := headers.get(key): + return value + return None + @staticmethod def _validate_config(client_type: ServerType, config: ServerParameters) -> ServerParameters | None: """Validate and return the appropriate server parameters.""" @@ -257,7 +277,7 @@ def _create_capturing_send( async def capturing_send(message: dict[str, Any]) -> None: if message.get('type') == 'http.response.start': headers = {k.decode('latin-1').lower(): v.decode('latin-1') for k, v in message.get('headers', [])} - if sid := headers.get('mcp-session-id'): + if sid := ProxyServer._get_session_id_from_headers(headers): session_id_from_resp['sid'] = sid await send(message) @@ -341,7 +361,7 @@ async def handle_streamable_http(scope: Scope, receive: Receive, send: Send) -> if scope['method'] == 'DELETE': await session_manager.handle_request(scope, receive, send) - if req_sid := request.headers.get('mcp-session-id'): + if req_sid := self._get_session_id_from_headers(request.headers): self._cleanup_session_last_activity(req_sid) self._cleanup_session_timer(req_sid) return @@ -352,7 +372,7 @@ async def handle_streamable_http(scope: Scope, receive: Receive, send: Send) -> capturing_send = self._create_capturing_send(send, session_id_from_resp) # Log and touch existing session if present on request - if req_sid := request.headers.get('mcp-session-id'): + if req_sid := self._get_session_id_from_headers(request.headers): self._touch_session(req_sid, session_manager) await session_manager.handle_request(scope, receive, capturing_send) # type: ignore[arg-type]