[GH-ISSUE #20208] feat: MCP clients recreated per request, breaking stateful HTTP servers with session IDs #57791

Closed
opened 2026-05-05 21:37:11 -05:00 by GiteaMirror · 6 comments
Owner

Originally created by @adolfoweloy on GitHub (Dec 27, 2025).
Original GitHub issue: https://github.com/open-webui/open-webui/issues/20208

Check Existing Issues

  • I have searched for all existing open AND closed issues and discussions for similar requests. I have found none that is comparable to my request.

Verify Feature Scope

  • I have read through and understood the scope definition for feature requests in the Issues section. I believe my feature request meets the definition and belongs in the Issues section instead of the Discussions.

Problem Description

Open WebUI creates a new MCP client and calls initialize() for every chat request, then destroys it afterward. This breaks stateful HTTP MCP servers that return a session ID and expect it to be included in subsequent requests via the Mcp-Session-Id header.

Impact: The MCP server returns 400 errors because:

  • A new session is initialized on every request instead of reusing the existing one
  • The session ID is never extracted or stored
  • Subsequent tool calls don't include the Mcp-Session-Id header
  • Server-side state is lost between requests

Root Cause: MCP clients are request-scoped (created/destroyed per request) instead of session-scoped (created once, reused across requests).

Desired Solution you'd like

For stateful HTTP MCP servers, the client lifecycle should work as follows:

  1. Initialize once: connect() and initialize() should be called once when the MCP server configuration is loaded at application startup
  2. Extract session ID: Capture the session ID from the server's response after initialization
  3. Persist session: Store the session ID and include it in the Mcp-Session-Id header for all subsequent requests (tool calls, list operations, etc.)
  4. Reuse connection: Keep the client instance alive for the application lifetime, not destroyed after each request
  5. Per-server instances: Maintain one client instance per MCP server configuration, shared across all chat requests

This approach ensures session state is preserved and the MCP server receives the expected session ID on every request after initialization.

Alternatives Considered

Hybrid: Stateful vs Stateless Server Detection

Approach: Detect if an MCP server is stateful (returns session ID) and only use persistent connections for those servers.

Pros:

  • Backward compatible with current stateless server behavior
  • Optimizes resources based on server type

Cons:

  • Adds complexity to detect server capabilities
  • Requires maintaining two different code paths
  • Session ID detection logic needed

Additional Context

N/A

Originally created by @adolfoweloy on GitHub (Dec 27, 2025). Original GitHub issue: https://github.com/open-webui/open-webui/issues/20208 ### Check Existing Issues - [x] I have searched for all existing **open AND closed** issues and discussions for similar requests. I have found none that is comparable to my request. ### Verify Feature Scope - [x] I have read through and understood the scope definition for feature requests in the Issues section. I believe my feature request meets the definition and belongs in the Issues section instead of the Discussions. ### Problem Description Open WebUI creates a **new MCP client and calls `initialize()` for every chat request**, then destroys it afterward. This breaks stateful HTTP MCP servers that return a session ID and expect it to be included in subsequent requests via the `Mcp-Session-Id` header. **Impact**: The MCP server returns 400 errors because: - A new session is initialized on every request instead of reusing the existing one - The session ID is never extracted or stored - Subsequent tool calls don't include the `Mcp-Session-Id` header - Server-side state is lost between requests **Root Cause**: MCP clients are **request-scoped** (created/destroyed per request) instead of **session-scoped** (created once, reused across requests). ### Desired Solution you'd like For stateful HTTP MCP servers, the client lifecycle should work as follows: 1. **Initialize once**: `connect()` and `initialize()` should be called **once** when the MCP server configuration is loaded at application startup 2. **Extract session ID**: Capture the session ID from the server's response after initialization 3. **Persist session**: Store the session ID and include it in the `Mcp-Session-Id` header for all subsequent requests (tool calls, list operations, etc.) 4. **Reuse connection**: Keep the client instance alive for the application lifetime, not destroyed after each request 5. **Per-server instances**: Maintain one client instance **per MCP server configuration**, shared across all chat requests This approach ensures session state is preserved and the MCP server receives the expected session ID on every request after initialization. ### Alternatives Considered ### Hybrid: Stateful vs Stateless Server Detection **Approach**: Detect if an MCP server is stateful (returns session ID) and only use persistent connections for those servers. **Pros**: - Backward compatible with current stateless server behavior - Optimizes resources based on server type **Cons**: - Adds complexity to detect server capabilities - Requires maintaining two different code paths - Session ID detection logic needed ### Additional Context N/A
Author
Owner

@owui-terminator[bot] commented on GitHub (Dec 27, 2025):

🔍 Similar Issues Found

I found some existing issues that might be related to this one. Please check if any of these are duplicates or contain helpful solutions:

  1. #19313 feat: User specific MCP headers
    by patrykkozuch • Nov 20, 2025

  2. #19794 MCP OAuth 2.1: Not following WWW-Authenticate → Protected Resource → Authorization Server discovery chain
    by jamie-dit • Dec 07, 2025


💡 Tips:

  • If this is a duplicate, please consider closing this issue and adding any additional details to the existing one
  • If you found a solution in any of these issues, please share it here to help others

This comment was generated automatically by a bot. Please react with a 👍 if this comment was helpful, or a 👎 if it was not.

<!-- gh-comment-id:3693964756 --> @owui-terminator[bot] commented on GitHub (Dec 27, 2025): 🔍 **Similar Issues Found** I found some existing issues that might be related to this one. Please check if any of these are duplicates or contain helpful solutions: 1. [#19313](https://github.com/open-webui/open-webui/issues/19313) **feat: User specific MCP headers** *by patrykkozuch • Nov 20, 2025* 2. [#19794](https://github.com/open-webui/open-webui/issues/19794) **MCP OAuth 2.1: Not following WWW-Authenticate → Protected Resource → Authorization Server discovery chain** *by jamie-dit • Dec 07, 2025* --- 💡 **Tips:** - If this is a duplicate, please consider closing this issue and adding any additional details to the existing one - If you found a solution in any of these issues, please share it here to help others *This comment was generated automatically by a bot.* Please react with a 👍 if this comment was helpful, or a 👎 if it was not.
Author
Owner

@rgaricano commented on GitHub (Dec 27, 2025):

For reference,

Solution Approaches

1. Application-Scoped MCP Client Pool

Create a global MCP client manager that maintains persistent connections:

# In backend/open_webui/utils/mcp/manager.py
class MCPClientManager:
    def __init__(self):
        self.clients = {}
        self.session_ids = {}

    async def get_client(self, server_id: str, connection_config: dict):
        if server_id not in self.clients:
            client = MCPClient()
            await client.connect(
                url=connection_config.get("url", ""),
                headers=self._build_headers(connection_config)
            )
            self.clients[server_id] = client
        return self.clients[server_id]

    def _build_headers(self, config: dict) -> dict:
        headers = {}
        if server_id in self.session_ids:
            headers["Mcp-Session-Id"] = self.session_ids[server_id]
        return headers

2. Session ID Extraction and Storage

Modify MCPClient.connect() to extract and store session IDs:

# In backend/open_webui/utils/mcp/client.py
async def connect(self, url: str, headers: Optional[dict] = None):
    # ... existing connection code ...

    # Extract session ID from response headers if present
    if hasattr(self.session, '_response_headers'):
        session_id = self.session._response_headers.get('mcp-session-id')
        if session_id:
            self.session_id = session_id

3. Middleware Integration

Update process_chat_payload() to use the client manager:

# In backend/open_webui/utils/middleware.py
async def process_chat_payload(request, form_data, user, metadata, model):
    # ... existing code ...

    mcp_clients = {}
    mcp_tools_dict = {}

    if tool_ids:
        for tool_id in tool_ids:
            if tool_id.startswith("server:mcp:"):
                server_id = tool_id[len("server:mcp:") :]

                # Use client manager instead of creating new client
                client = await request.app.state.mcp_client_manager.get_client(
                    server_id, mcp_server_connection
                )
                mcp_clients[server_id] = client

                # ... rest of tool setup ...

4. Application Startup Initialization

Initialize the client manager in main.py:

# In backend/open_webui/main.py
@app.on_event("startup")
async def startup_event():
    app.state.mcp_client_manager = MCPClientManager()

Implementation Steps

  1. Create MCPClientManager class to handle client lifecycle and session persistence
  2. Modify MCPClient to extract and store session IDs from server responses
  3. Update middleware to use the client manager instead of creating new clients
  4. Add session ID headers to all subsequent requests to MCP servers
  5. Implement cleanup logic for expired sessions while preserving active ones

This approach maintains session state across requests while preserving the existing tool execution flow

<!-- gh-comment-id:3693969738 --> @rgaricano commented on GitHub (Dec 27, 2025): For reference, ## Solution Approaches ### 1. Application-Scoped MCP Client Pool Create a global MCP client manager that maintains persistent connections: ```python # In backend/open_webui/utils/mcp/manager.py class MCPClientManager: def __init__(self): self.clients = {} self.session_ids = {} async def get_client(self, server_id: str, connection_config: dict): if server_id not in self.clients: client = MCPClient() await client.connect( url=connection_config.get("url", ""), headers=self._build_headers(connection_config) ) self.clients[server_id] = client return self.clients[server_id] def _build_headers(self, config: dict) -> dict: headers = {} if server_id in self.session_ids: headers["Mcp-Session-Id"] = self.session_ids[server_id] return headers ``` ### 2. Session ID Extraction and Storage Modify `MCPClient.connect()` to extract and store session IDs: ```python # In backend/open_webui/utils/mcp/client.py async def connect(self, url: str, headers: Optional[dict] = None): # ... existing connection code ... # Extract session ID from response headers if present if hasattr(self.session, '_response_headers'): session_id = self.session._response_headers.get('mcp-session-id') if session_id: self.session_id = session_id ``` ### 3. Middleware Integration Update `process_chat_payload()` to use the client manager: ```python # In backend/open_webui/utils/middleware.py async def process_chat_payload(request, form_data, user, metadata, model): # ... existing code ... mcp_clients = {} mcp_tools_dict = {} if tool_ids: for tool_id in tool_ids: if tool_id.startswith("server:mcp:"): server_id = tool_id[len("server:mcp:") :] # Use client manager instead of creating new client client = await request.app.state.mcp_client_manager.get_client( server_id, mcp_server_connection ) mcp_clients[server_id] = client # ... rest of tool setup ... ``` ### 4. Application Startup Initialization Initialize the client manager in `main.py`: ```python # In backend/open_webui/main.py @app.on_event("startup") async def startup_event(): app.state.mcp_client_manager = MCPClientManager() ``` ## Implementation Steps 1. **Create MCPClientManager class** to handle client lifecycle and session persistence 2. **Modify MCPClient** to extract and store session IDs from server responses 3. **Update middleware** to use the client manager instead of creating new clients 4. **Add session ID headers** to all subsequent requests to MCP servers 5. **Implement cleanup logic** for expired sessions while preserving active ones This approach maintains session state across requests while preserving the existing tool execution flow
Author
Owner

@adolfoweloy commented on GitHub (Dec 28, 2025):

I am closing this issue. It turns out that the session is actually managed by modelcontextprotocol/python-sdk.

<!-- gh-comment-id:3694478040 --> @adolfoweloy commented on GitHub (Dec 28, 2025): I am closing this issue. It turns out that the session is actually managed by `modelcontextprotocol/python-sdk`.
Author
Owner

@rgaricano commented on GitHub (Dec 28, 2025):

modelcontextprotocol/python-sdk

yes, we can leverage the ctx.session object from the modelcontextprotocol/python-sdk for mcp session management. Actually current MCPClient creates a ClientSession but doesn't utilize the session context, for use it we need to modify the MCPClientManager to preserve the session context, e.g.:

# backend/open_webui/utils/mcp/manager.py
class MCPClientManager:
    def __init__(self):
        self.session_contexts = {}  # Store session contexts per server_id

    async def get_client(self, server_id: str, connection_config: dict):
        if server_id not in self.session_contexts:
            # Create new session context and store it
            client = MCPClient()
            await client.connect(connection_config.get("url", ""))

            # Store the session context for reuse
            self.session_contexts[server_id] = client._session_context
            return client
        else:
            # Reuse existing session context
            client = MCPClient()
            client._session_context = self.session_contexts[server_id]
            client.session = await client._session_context.__aenter__()
            return client
<!-- gh-comment-id:3694639885 --> @rgaricano commented on GitHub (Dec 28, 2025): > modelcontextprotocol/python-sdk yes, we can leverage the `ctx.session` object from the `modelcontextprotocol/python-sdk` for mcp session management. Actually current MCPClient creates a ClientSession but doesn't utilize the session context, for use it we need to modify the MCPClientManager to preserve the session context, e.g.: ``` # backend/open_webui/utils/mcp/manager.py class MCPClientManager: def __init__(self): self.session_contexts = {} # Store session contexts per server_id async def get_client(self, server_id: str, connection_config: dict): if server_id not in self.session_contexts: # Create new session context and store it client = MCPClient() await client.connect(connection_config.get("url", "")) # Store the session context for reuse self.session_contexts[server_id] = client._session_context return client else: # Reuse existing session context client = MCPClient() client._session_context = self.session_contexts[server_id] client.session = await client._session_context.__aenter__() return client ```
Author
Owner

@gmag11 commented on GitHub (Mar 20, 2026):

This is still happening in v0.8.10 and breaks MCP functionality for stateful HTTP MCP servers. A single chat session should reuse the connection with server to keep context valid.

<!-- gh-comment-id:4097292021 --> @gmag11 commented on GitHub (Mar 20, 2026): This is still happening in v0.8.10 and breaks MCP functionality for stateful HTTP MCP servers. A single chat session should reuse the connection with server to keep context valid.
Author
Owner

@allarac commented on GitHub (Apr 10, 2026):

I think this is a breaking bug @adolfoweloy. Could this maybe be reopened?

<!-- gh-comment-id:4221997034 --> @allarac commented on GitHub (Apr 10, 2026): I think this is a breaking bug @adolfoweloy. Could this maybe be reopened?
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/open-webui#57791