-
Notifications
You must be signed in to change notification settings - Fork 11
OLS-3185: mcp servers wiring #82
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -8,12 +8,15 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from __future__ import annotations | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import dataclasses | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import json | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import logging | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import os | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from typing import Any | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger = logging.getLogger(__name__) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| LLM_CREDENTIALS_PATH = "/var/run/secrets/llm-credentials" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _SA_TOKEN_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/token" # noqa: S105 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def _llm_credentials_path() -> str: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -22,6 +25,86 @@ def _llm_credentials_path() -> str: | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return override or LLM_CREDENTIALS_PATH | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @dataclasses.dataclass(frozen=True) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class McpServerConfig: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """A single MCP server endpoint configured by the operator.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name: str | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| url: str | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| headers: dict[str, str] = dataclasses.field(default_factory=dict) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def _resolve_header_source(source: str) -> str: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Resolve a dynamic header source to its value.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if source == "ServiceAccountToken": | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| with open(_SA_TOKEN_PATH) as f: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return f.read().strip() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| except FileNotFoundError as err: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise ValueError( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| f"Header source 'ServiceAccountToken' requires {_SA_TOKEN_PATH}" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) from err | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise ValueError(f"Unknown header source: {source!r}") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def _resolve_headers(raw: Any, index: int) -> dict[str, str]: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Accept headers as a dict or as the operator's list-of-objects format. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Dict format (static): {"header-name": "value"} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| List format (dynamic): [{"name": "header-name", "source": "ServiceAccountToken"}] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if isinstance(raw, dict): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return raw | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+56
to
+57
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win Validate header names and values consistently. The dict path returns Proposed validation if isinstance(raw, dict):
- return raw
+ resolved: dict[str, str] = {}
+ for hdr_name, hdr_value in raw.items():
+ if not hdr_name or not isinstance(hdr_name, str):
+ raise ValueError(
+ f"LIGHTSPEED_MCP_SERVERS[{index}].headers has invalid header name"
+ )
+ if not isinstance(hdr_value, str):
+ raise ValueError(
+ f"LIGHTSPEED_MCP_SERVERS[{index}].headers[{hdr_name!r}] must be a string"
+ )
+ resolved[hdr_name] = hdr_value
+ return resolved
...
if "value" in item:
- resolved[hdr_name] = str(item["value"])
+ value = item["value"]
+ if not isinstance(value, str):
+ raise ValueError(
+ f"LIGHTSPEED_MCP_SERVERS[{index}].headers[{j}].value must be a string"
+ )
+ resolved[hdr_name] = valueAs per path instructions, “Validate at trust boundaries with allow-lists, not deny-lists.” Also applies to: 70-73 🤖 Prompt for AI AgentsSource: Path instructions |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if isinstance(raw, list): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| resolved: dict[str, str] = {} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for j, item in enumerate(raw): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if not isinstance(item, dict): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise ValueError( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| f"LIGHTSPEED_MCP_SERVERS[{index}].headers[{j}] must be an object" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| hdr_name = item.get("name") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if not hdr_name or not isinstance(hdr_name, str): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise ValueError( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| f"LIGHTSPEED_MCP_SERVERS[{index}].headers[{j}] missing 'name'" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if "value" in item: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| resolved[hdr_name] = str(item["value"]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| elif "source" in item: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| resolved[hdr_name] = _resolve_header_source(item["source"]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise ValueError( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| f"LIGHTSPEED_MCP_SERVERS[{index}].headers[{j}] needs 'value' or 'source'" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return resolved | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise ValueError(f"LIGHTSPEED_MCP_SERVERS[{index}] 'headers' must be an object or array") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def resolve_mcp_servers() -> tuple[McpServerConfig, ...]: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Parse LIGHTSPEED_MCP_SERVERS env var into validated configs.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raw = os.environ.get("LIGHTSPEED_MCP_SERVERS", "").strip() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if not raw: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return () | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| entries: list[Any] = json.loads(raw) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if not isinstance(entries, list): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise ValueError("LIGHTSPEED_MCP_SERVERS must be a JSON array") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| servers: list[McpServerConfig] = [] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for i, entry in enumerate(entries): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if not isinstance(entry, dict): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise ValueError(f"LIGHTSPEED_MCP_SERVERS[{i}] must be an object") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name = entry.get("name") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| url = entry.get("url") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if not name or not isinstance(name, str): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise ValueError(f"LIGHTSPEED_MCP_SERVERS[{i}] missing required 'name' string") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if not url or not isinstance(url, str): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise ValueError(f"LIGHTSPEED_MCP_SERVERS[{i}] missing required 'url' string") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| headers = _resolve_headers(entry.get("headers", {}), i) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| servers.append(McpServerConfig(name=name, url=url, headers=headers)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+92
to
+103
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win Reject duplicate MCP server names. Duplicate Proposed duplicate check servers: list[McpServerConfig] = []
+ seen_names: set[str] = set()
for i, entry in enumerate(entries):
if not isinstance(entry, dict):
raise ValueError(f"LIGHTSPEED_MCP_SERVERS[{i}] must be an object")
name = entry.get("name")
url = entry.get("url")
@@
if not url or not isinstance(url, str):
raise ValueError(f"LIGHTSPEED_MCP_SERVERS[{i}] missing required 'url' string")
+ if name in seen_names:
+ raise ValueError(f"LIGHTSPEED_MCP_SERVERS[{i}] duplicate server name {name!r}")
+ seen_names.add(name)
headers = _resolve_headers(entry.get("headers", {}), i)
servers.append(McpServerConfig(name=name, url=url, headers=headers))📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return tuple(servers) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _DEFAULT_VERTEX_REGION = "us-east5" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _DEFAULT_BEDROCK_REGION = "us-east-1" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -33,6 +116,7 @@ class ResolvedSDK: | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name: str # "claude", "gemini", "openai" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expected_envs: tuple[str, ...] # credential env vars expected from envFrom | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| probe_url: str # R2 reachability probe base URL | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mcp_servers: tuple[McpServerConfig, ...] = () | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def _setenv(key: str, value: str) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -194,5 +278,15 @@ def resolve_sdk() -> ResolvedSDK: | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "Supported: anthropic, vertex, openai, azure, bedrock" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.info("Resolved LIGHTSPEED_PROVIDER=%s → SDK=%s", provider, sdk.name) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mcp_servers = resolve_mcp_servers() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if mcp_servers: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sdk = dataclasses.replace(sdk, mcp_servers=mcp_servers) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.info( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "Resolved LIGHTSPEED_PROVIDER=%s → SDK=%s (MCP servers: %s)", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| provider, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sdk.name, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ", ".join(s.name for s in mcp_servers), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.info("Resolved LIGHTSPEED_PROVIDER=%s → SDK=%s", provider, sdk.name) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return sdk | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Fix MCP
headerscontract to include list-format entries.The spec currently documents
headersas object-only, but the implementation and tests support both object and list formats ([{ "name": "...", "value" | "source": ... }]). This mismatch can break operator-side integration expectations.Suggested spec wording update
Also applies to: 70-70, 87-87
🤖 Prompt for AI Agents