From 9521c7fe7dab2f282df1cac954eaa973619d242d Mon Sep 17 00:00:00 2001 From: Sanket Shakya Date: Tue, 30 Jun 2026 08:58:24 +0530 Subject: [PATCH] feat(mcp): support dynamic HTTP request headers in MCPClient (#37938) --- api/core/mcp/mcp_client.py | 17 ++++++- .../unit_tests/core/mcp/test_mcp_client.py | 51 +++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/api/core/mcp/mcp_client.py b/api/core/mcp/mcp_client.py index 2b0645b5589..c6d33292dae 100644 --- a/api/core/mcp/mcp_client.py +++ b/api/core/mcp/mcp_client.py @@ -1,10 +1,13 @@ import logging +import re from collections.abc import Callable from contextlib import AbstractContextManager, ExitStack from types import TracebackType from typing import Any from urllib.parse import urlparse +from flask import has_request_context, request + from core.mcp.client.sse_client import sse_client from core.mcp.client.streamable_client import streamablehttp_client from core.mcp.error import MCPConnectionError @@ -23,10 +26,22 @@ class MCPClient: sse_read_timeout: float | None = None, ): self.server_url = server_url - self.headers = headers or {} + self.headers = headers.copy() if headers else {} self.timeout = timeout self.sse_read_timeout = sse_read_timeout + # Substitute placeholders with incoming request headers if in a request context + if has_request_context() and self.headers: + pattern = re.compile(r"\{\{\s*request\.headers?\.(.+?)\s*\}\}", re.IGNORECASE) + for key, value in list(self.headers.items()): + if isinstance(value, str): + + def replace_func(match): + header_name = match.group(1) + return request.headers.get(header_name, "") + + self.headers[key] = pattern.sub(replace_func, value) + # Initialize session and client objects self._session: ClientSession | None = None self._exit_stack = ExitStack() diff --git a/api/tests/unit_tests/core/mcp/test_mcp_client.py b/api/tests/unit_tests/core/mcp/test_mcp_client.py index c245b4a77ef..62baee83ce3 100644 --- a/api/tests/unit_tests/core/mcp/test_mcp_client.py +++ b/api/tests/unit_tests/core/mcp/test_mcp_client.py @@ -43,6 +43,57 @@ class TestMCPClient: assert client.timeout is None assert client.sse_read_timeout is None + def test_init_with_dynamic_request_headers(self): + """Test client initialization with dynamic request headers.""" + from flask import Flask + + app = Flask("test") + with app.test_request_context(headers={"X-Custom-Auth": "my-secret-token", "X-User-Id": "user123"}): + client = MCPClient( + server_url="http://test.example.com", + headers={ + "Authorization": "Bearer {{request.headers.X-Custom-Auth}}", + "X-Request-User": "{{request.header.X-User-Id}}", + "X-Static-Header": "static-val", + }, + ) + + assert client.headers == { + "Authorization": "Bearer my-secret-token", + "X-Request-User": "user123", + "X-Static-Header": "static-val", + } + + def test_init_with_dynamic_request_headers_missing(self): + """Test client initialization with dynamic request headers that are missing.""" + from flask import Flask + + app = Flask("test") + with app.test_request_context(headers={}): + client = MCPClient( + server_url="http://test.example.com", + headers={ + "Authorization": "Bearer {{request.headers.X-Custom-Auth}}", + }, + ) + + assert client.headers == { + "Authorization": "Bearer ", + } + + def test_init_with_dynamic_request_headers_no_context(self): + """Test client initialization with dynamic request headers but no request context.""" + client = MCPClient( + server_url="http://test.example.com", + headers={ + "Authorization": "Bearer {{request.headers.X-Custom-Auth}}", + }, + ) + + assert client.headers == { + "Authorization": "Bearer {{request.headers.X-Custom-Auth}}", + } + @patch("core.mcp.mcp_client.streamablehttp_client") @patch("core.mcp.mcp_client.ClientSession") def test_initialize_with_mcp_url(self, mock_client_session, mock_streamable_client):