fix(core): strip quotes from MCP server header keys and values (#9349)

* fix(core): strip quotes from MCP server header keys and values

Users pasting JSON-formatted env vars into MCP server config end up with
quoted header names like `"CONTEXT7_API_KEY":` which causes
httpx.LocalProtocolError. Sanitize keys (strip surrounding quotes and
trailing colons) and values (strip surrounding quotes) in
resolve_custom_headers, resolve_environment_variables for HTTP configs,
and stdio env dicts.

Datadog: https://us5.datadoghq.com/error-tracking/issue/4a2f4af6-f2d8-11f0-930c-da7ad0900000

🤖 Generated with [Letta Code](https://letta.com)

Co-Authored-By: Letta <noreply@letta.com>

* fix: revert stdio env sanitization to pass-through

The stdio path doesn't need header/env sanitization - that's only
relevant for SSE/streamable HTTP servers with auth headers.

🐾 Generated with [Letta Code](https://letta.com)

Co-Authored-By: Letta <noreply@letta.com>

---------

Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
Kian Jones
2026-02-11 10:52:07 -08:00
committed by Caren Thomas
parent ddcfeb26b1
commit 2568c02b51

View File

@@ -98,6 +98,32 @@ class BaseServerConfig(BaseModel):
return result
@staticmethod
def _sanitize_dict_key(key: str) -> str:
"""Strip surrounding quotes and trailing colons from a dict key."""
key = key.strip()
for quote in ('"', "'"):
if key.startswith(quote) and key.endswith(quote):
key = key[1:-1]
break
key = key.rstrip(":")
return key.strip()
@staticmethod
def _sanitize_dict_value(value: str) -> str:
"""Strip surrounding quotes from a dict value."""
value = value.strip()
for quote in ('"', "'"):
if value.startswith(quote) and value.endswith(quote):
value = value[1:-1]
break
return value
@classmethod
def _sanitize_dict(cls, d: Dict[str, str]) -> Dict[str, str]:
"""Sanitize a string dict by stripping quotes from keys and values."""
return {cls._sanitize_dict_key(k): cls._sanitize_dict_value(v) for k, v in d.items()}
def resolve_custom_headers(
self, custom_headers: Optional[Dict[str, str]], environment_variables: Optional[Dict[str, str]] = None
) -> Optional[Dict[str, str]]:
@@ -114,6 +140,8 @@ class BaseServerConfig(BaseModel):
if custom_headers is None:
return None
custom_headers = self._sanitize_dict(custom_headers)
resolved_headers = {}
for key, value in custom_headers.items():
# Resolve templated variables in each header value
@@ -164,8 +192,12 @@ class HTTPBasedServerConfig(BaseServerConfig):
return None
def resolve_environment_variables(self, environment_variables: Optional[Dict[str, str]] = None) -> None:
if self.auth_token and super().is_templated_tool_variable(self.auth_token):
self.auth_token = super().get_tool_variable(self.auth_token, environment_variables)
if self.auth_header:
self.auth_header = self._sanitize_dict_key(self.auth_header)
if self.auth_token:
self.auth_token = self._sanitize_dict_value(self.auth_token)
if super().is_templated_tool_variable(self.auth_token):
self.auth_token = super().get_tool_variable(self.auth_token, environment_variables)
self.custom_headers = super().resolve_custom_headers(self.custom_headers, environment_variables)