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:
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user