Skip to content

Commit 4ffeb00

Browse files
Merge pull request #2634 from blublinsky/mcp-headers
changed mcp headers definitions
2 parents 3b38238 + b371fb8 commit 4ffeb00

File tree

3 files changed

+106
-115
lines changed

3 files changed

+106
-115
lines changed

ols/src/query_helpers/docs_summarizer.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,11 @@ def __init__(
106106
mcp_config_builder = MCPConfigBuilder(
107107
self.user_token, config.mcp_servers.servers
108108
)
109-
self.mcp_servers = mcp_config_builder.dump_client_config()
109+
try:
110+
self.mcp_servers = mcp_config_builder.dump_client_config()
111+
except Exception as e:
112+
logger.error("Failed to resolve MCP server(s): %s", e)
113+
self.mcp_servers = {}
110114
if self.mcp_servers:
111115
logger.info("MCP servers provided: %s", list(self.mcp_servers.keys()))
112116
self._tool_calling_enabled = True

ols/src/tools/mcp_config_builder.py

Lines changed: 28 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,12 @@
66
from typing import Any
77

88
from ols.app.models.config import MCPServerConfig
9+
from ols.utils import checks
910

1011
logger = logging.getLogger(__name__)
1112

12-
13-
# Additional header containing user token for k8s/ocp authentication
14-
# for SSE MCP servers.
15-
K8S_AUTH_HEADER = "kubernetes-authorization"
13+
# Constant, defining usage of kubernetes token
14+
KUBERNETES_PLACEHOLDER = "kubernetes"
1615

1716

1817
class MCPConfigBuilder:
@@ -25,20 +24,6 @@ def __init__(
2524
self.user_token = user_token
2625
self.mcp_server_configs = mcp_server_configs
2726

28-
@staticmethod
29-
def include_auth_header(user_token: str, config: dict[str, Any]) -> dict[str, Any]:
30-
"""Include user token in the config headers."""
31-
# Only add Authorization header if we have a valid token
32-
if user_token and user_token.strip():
33-
if "headers" not in config:
34-
config["headers"] = {}
35-
if K8S_AUTH_HEADER in config["headers"]:
36-
logger.warning(
37-
"Kubernetes auth header is already set, overriding with actual user token."
38-
)
39-
config["headers"][K8S_AUTH_HEADER] = f"Bearer {user_token}"
40-
return config
41-
4227
def include_auth_to_stdio(self, server_envs: dict[str, str]) -> dict[str, str]:
4328
"""Resolve OpenShift stdio env config."""
4429
logger.debug("Updating env configuration of openshift stdio mcp server")
@@ -79,18 +64,20 @@ def dump_client_config(self) -> dict[str, Any]:
7964
server_conf.stdio.env
8065
)
8166
servers_conf[server_conf.name].update(stdio_conf)
67+
continue
8268

8369
if server_conf.sse:
8470
sse_conf = server_conf.sse.model_dump()
85-
self.include_auth_header(self.user_token, sse_conf)
71+
sse_conf["headers"] = self._resolve_tokens_to_value(sse_conf["headers"])
8672
servers_conf[server_conf.name].update(sse_conf)
73+
continue
8774

8875
if server_conf.streamable_http:
89-
servers_conf[server_conf.name].update(
90-
self.include_auth_header(
91-
self.user_token, server_conf.streamable_http.model_dump()
92-
)
76+
http_conf = server_conf.streamable_http.model_dump()
77+
http_conf["headers"] = self._resolve_tokens_to_value(
78+
http_conf["headers"]
9379
)
80+
servers_conf[server_conf.name].update(http_conf)
9481
# Note: Streamable HTTP transport expects timedelta instead of
9582
# int as for the sse - blame langchain-mcp-adapters for
9683
# inconsistency
@@ -100,3 +87,21 @@ def dump_client_config(self) -> dict[str, Any]:
10087
)
10188

10289
return servers_conf
90+
91+
def _resolve_tokens_to_value(self, headers: dict[str, str]) -> dict[str, Any]:
92+
"""Convert header definitions to values."""
93+
updated = {}
94+
for name, value in headers.items():
95+
if value == KUBERNETES_PLACEHOLDER:
96+
updated[name] = f"Bearer {self.user_token}"
97+
else:
98+
try:
99+
# load token value
100+
with open(value, "r", encoding="utf-8") as token_store:
101+
token = token_store.read()
102+
updated[name] = token
103+
except Exception as e:
104+
raise checks.InvalidConfigurationError(
105+
f"token value refers to non existent file '{value}', error {e}"
106+
)
107+
return updated

tests/unit/tools/test_mcp_config_builder.py

Lines changed: 73 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Tests for MCPConfigBuilder."""
22

33
import os
4+
import tempfile
45
from datetime import timedelta
56
from unittest.mock import patch
67

@@ -10,7 +11,7 @@
1011
StdioTransportConfig,
1112
StreamableHttpTransportConfig,
1213
)
13-
from ols.src.tools.mcp_config_builder import K8S_AUTH_HEADER, MCPConfigBuilder
14+
from ols.src.tools.mcp_config_builder import KUBERNETES_PLACEHOLDER, MCPConfigBuilder
1415

1516

1617
def test_mcp_config_builder_dump_client_config():
@@ -145,66 +146,42 @@ def test_missing_kubeconfig_and_kubernetes_service(caplog):
145146
assert mcp_config == expected
146147
assert "Missing necessary KUBECONFIG/KUBERNETES_SERVICE_* envs" in caplog.text
147148

148-
@staticmethod
149-
def test_include_auth_header():
150-
"""Test include_auth_header method."""
151-
user_token = "fake-token" # noqa: S105
152-
config = {}
153-
154-
result = MCPConfigBuilder.include_auth_header(user_token, config)
155-
156-
assert "headers" in result
157-
assert result["headers"][K8S_AUTH_HEADER] == f"Bearer {user_token}"
158-
159-
@staticmethod
160-
def test_include_auth_header_existing_headers():
161-
"""Test include_auth_header with existing headers."""
162-
user_token = "fake-token" # noqa: S105
163-
config = {"headers": {"Content-Type": "application/json"}}
164-
165-
result = MCPConfigBuilder.include_auth_header(user_token, config)
166-
167-
assert result["headers"]["Content-Type"] == "application/json"
168-
assert result["headers"][K8S_AUTH_HEADER] == f"Bearer {user_token}"
169-
170-
@staticmethod
171-
def test_include_auth_header_existing_auth(caplog):
172-
"""Test include_auth_header with existing auth header."""
173-
user_token = "fake-token" # noqa: S105
174-
config = {"headers": {K8S_AUTH_HEADER: "old-token"}}
175-
176-
result = MCPConfigBuilder.include_auth_header(user_token, config)
177-
178-
assert result["headers"][K8S_AUTH_HEADER] == f"Bearer {user_token}"
179-
assert (
180-
"Kubernetes auth header is already set, overriding with actual user token"
181-
in caplog.text
182-
)
183-
184149
@staticmethod
185150
def test_dump_client_config_with_sse():
186151
"""Test dump_client_config with SSE configuration."""
187-
mcp_server_configs = [
188-
MCPServerConfig(
189-
name="sse-server",
190-
transport="sse",
191-
sse=SseTransportConfig(
192-
url="https://example.com/events",
193-
headers={"X-Custom-Header": "value"},
152+
file_descriptor, file_path = tempfile.mkstemp(suffix=".tmp")
153+
try:
154+
with os.fdopen(file_descriptor, "w") as open_file:
155+
open_file.write("value")
156+
mcp_server_configs = [
157+
MCPServerConfig(
158+
name="sse-server",
159+
transport="sse",
160+
sse=SseTransportConfig(
161+
url="https://example.com/events",
162+
headers={
163+
"X-Custom-Header": file_path,
164+
"kubernetes": KUBERNETES_PLACEHOLDER,
165+
},
166+
),
194167
),
195-
),
196-
]
197-
user_token = "fake-token" # noqa: S105
198-
199-
builder = MCPConfigBuilder(user_token, mcp_server_configs)
200-
result = builder.dump_client_config()
168+
]
169+
user_token = "fake-token" # noqa: S105
201170

202-
assert result["sse-server"]["transport"] == "sse"
203-
assert result["sse-server"]["url"] == "https://example.com/events"
204-
assert result["sse-server"]["headers"]["X-Custom-Header"] == "value"
205-
assert (
206-
result["sse-server"]["headers"][K8S_AUTH_HEADER] == f"Bearer {user_token}"
207-
)
171+
builder = MCPConfigBuilder(user_token, mcp_server_configs)
172+
try:
173+
result = builder.dump_client_config()
174+
except Exception as e:
175+
print(f"failed creating config {e}")
176+
assert False
177+
assert result["sse-server"]["transport"] == "sse"
178+
assert result["sse-server"]["url"] == "https://example.com/events"
179+
assert result["sse-server"]["headers"]["X-Custom-Header"] == "value"
180+
assert (
181+
result["sse-server"]["headers"]["kubernetes"] == f"Bearer {user_token}"
182+
)
183+
finally:
184+
os.unlink(file_path)
208185

209186
@staticmethod
210187
def test_dump_client_config_with_mixed_transports():
@@ -237,40 +214,52 @@ def test_dump_client_config_with_mixed_transports():
237214
assert result["openshift"]["transport"] == "stdio"
238215
assert result["sse-server"]["transport"] == "sse"
239216
assert result["openshift"]["env"]["OC_USER_TOKEN"] == user_token
240-
assert (
241-
result["sse-server"]["headers"][K8S_AUTH_HEADER] == f"Bearer {user_token}"
242-
)
243217

244218
@staticmethod
245219
def test_dump_client_config_with_streamable_http():
246220
"""Test dump_client_config with streamable HTTP configuration."""
247-
mcp_server_configs = [
248-
MCPServerConfig(
249-
name="streamable-server",
250-
transport="streamable_http",
251-
streamable_http=StreamableHttpTransportConfig(
252-
url="https://example.com/stream",
253-
headers={"X-Custom-Header": "value"},
254-
timeout=30,
255-
sse_read_timeout=60,
221+
file_descriptor, file_path = tempfile.mkstemp(suffix=".tmp")
222+
try:
223+
with os.fdopen(file_descriptor, "w") as open_file:
224+
open_file.write("value")
225+
mcp_server_configs = [
226+
MCPServerConfig(
227+
name="streamable-server",
228+
transport="streamable_http",
229+
streamable_http=StreamableHttpTransportConfig(
230+
url="https://example.com/stream",
231+
headers={
232+
"X-Custom-Header": file_path,
233+
"kubernetes": KUBERNETES_PLACEHOLDER,
234+
},
235+
timeout=30,
236+
sse_read_timeout=60,
237+
),
256238
),
257-
),
258-
]
259-
user_token = "fake-token" # noqa: S105
260-
261-
builder = MCPConfigBuilder(user_token, mcp_server_configs)
262-
result = builder.dump_client_config()
239+
]
240+
user_token = "fake-token" # noqa: S105
263241

264-
assert result["streamable-server"]["transport"] == "streamable_http"
265-
assert result["streamable-server"]["url"] == "https://example.com/stream"
266-
assert result["streamable-server"]["headers"]["X-Custom-Header"] == "value"
267-
assert (
268-
result["streamable-server"]["headers"][K8S_AUTH_HEADER]
269-
== f"Bearer {user_token}"
270-
)
271-
# Verify that timeout values are converted to timedelta objects
272-
assert result["streamable-server"]["timeout"] == timedelta(seconds=30)
273-
assert result["streamable-server"]["sse_read_timeout"] == timedelta(seconds=60)
242+
builder = MCPConfigBuilder(user_token, mcp_server_configs)
243+
try:
244+
result = builder.dump_client_config()
245+
except Exception as e:
246+
print(f"failed creating config {e}")
247+
assert False
248+
249+
assert result["streamable-server"]["transport"] == "streamable_http"
250+
assert result["streamable-server"]["url"] == "https://example.com/stream"
251+
assert result["streamable-server"]["headers"]["X-Custom-Header"] == "value"
252+
assert (
253+
result["streamable-server"]["headers"]["kubernetes"]
254+
== f"Bearer {user_token}"
255+
)
256+
# Verify that timeout values are converted to timedelta objects
257+
assert result["streamable-server"]["timeout"] == timedelta(seconds=30)
258+
assert result["streamable-server"]["sse_read_timeout"] == timedelta(
259+
seconds=60
260+
)
261+
finally:
262+
os.unlink(file_path)
274263

275264
@staticmethod
276265
def test_dump_client_config_with_all_transports():
@@ -312,10 +301,3 @@ def test_dump_client_config_with_all_transports():
312301
assert result["sse-server"]["transport"] == "sse"
313302
assert result["streamable-server"]["transport"] == "streamable_http"
314303
assert result["openshift"]["env"]["OC_USER_TOKEN"] == user_token
315-
assert (
316-
result["sse-server"]["headers"][K8S_AUTH_HEADER] == f"Bearer {user_token}"
317-
)
318-
assert (
319-
result["streamable-server"]["headers"][K8S_AUTH_HEADER]
320-
== f"Bearer {user_token}"
321-
)

0 commit comments

Comments
 (0)