Skip to content

OLS-3185: mcp servers wiring#82

Draft
tremes wants to merge 2 commits into
openshift:mainfrom
tremes:mcp-wiring
Draft

OLS-3185: mcp servers wiring#82
tremes wants to merge 2 commits into
openshift:mainfrom
tremes:mcp-wiring

Conversation

@tremes

@tremes tremes commented Jun 23, 2026

Copy link
Copy Markdown

No description provided.

tremes and others added 2 commits June 22, 2026 14:44
Add external MCP server integration via streamable HTTP transport.
The operator configures endpoints through LIGHTSPEED_MCP_SERVERS env
var (JSON array of {name, url, headers?} objects).

Configuration:
- McpServerConfig dataclass and resolve_mcp_servers() in config.py
- mcp_servers field on ResolvedSDK and ProviderQueryOptions
- Flows from app startup through routing to provider query

Provider wiring:
- Claude: mcp_servers dict on ClaudeAgentOptions with mcp__*
  allowed_tools patterns
- Gemini: McpToolset with StreamableHTTPConnectionParams in tools
- OpenAI: MCPServerStreamableHttp with connect/cleanup lifecycle

Health probes:
- R3 check_mcp_endpoints() probes each MCP server URL
- Readiness route returns 503 when any MCP endpoint unreachable

Signed-off-by: Tomáš Remeš <tremes@redhat.com>
Assisted-by: Claude Code:claude-opus-4-6
…n source

The operator injects LIGHTSPEED_MCP_SERVERS headers as an array of
{name, source/value} objects, but resolve_mcp_servers() only accepted
a dict. This caused a ValueError at startup, crash-looping the pod.

Accept both formats:
- dict: {"header-name": "static-value"} (existing)
- list: [{"name": "h", "source": "ServiceAccountToken"}] (operator)

ServiceAccountToken source reads from the mounted SA token file.

Co-Authored-By: Claude <noreply@anthropic.com>
@openshift-ci-robot

openshift-ci-robot commented Jun 23, 2026

Copy link
Copy Markdown

@tremes: This pull request references OLS-3185 which is a valid jira issue.

Warning: The referenced jira issue has an invalid target version for the target branch this PR targets: expected the feature request to target the "5.0.0" version, but no target version was set.

Details

In response to this:

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository.

@openshift-ci-robot openshift-ci-robot added the jira/valid-reference Indicates that this PR references a valid Jira ticket of any type. label Jun 23, 2026
@openshift-ci openshift-ci Bot added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Jun 23, 2026
@openshift-ci

openshift-ci Bot commented Jun 23, 2026

Copy link
Copy Markdown

Skipping CI for Draft Pull Request.
If you want CI signal for your change, please convert it to an actual PR.
You can still manually trigger a test run with /test all

@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown
📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Added support for Model Context Protocol (MCP) server configuration via a new environment variable, enabling external MCP servers to be integrated with Claude, Gemini, and OpenAI providers.
    • Added health checks for MCP server reachability to verify connectivity during system startup.

Walkthrough

Adds end-to-end MCP (Model Context Protocol) server support via a new LIGHTSPEED_MCP_SERVERS env var. A McpServerConfig dataclass and resolve_mcp_servers() parser are introduced in config.py, the config flows through ProviderQueryOptions and routes into all three provider adapters (Claude, Gemini, OpenAI) with provider-native wiring, readiness probes are extended with per-MCP reachability checks, and specs/tests are updated throughout.

Changes

MCP Server Support

Layer / File(s) Summary
McpServerConfig dataclass, parsing, and ResolvedSDK integration
src/lightspeed_agentic/config.py
McpServerConfig dataclass defined; resolve_mcp_servers() parses LIGHTSPEED_MCP_SERVERS JSON with header-source resolution (including SA token reads); ResolvedSDK.mcp_servers field added; resolve_sdk() calls resolve_mcp_servers() and injects results.
ProviderQueryOptions.mcp_servers and route propagation
src/lightspeed_agentic/types.py, src/lightspeed_agentic/routes/__init__.py, src/lightspeed_agentic/routes/query.py, src/lightspeed_agentic/app.py
mcp_servers field added to ProviderQueryOptions; build_router and register_query_routes accept and forward mcp_servers; /run endpoint injects it into ProviderQueryOptions passed to provider.query.
Claude, Gemini, and OpenAI MCP adapter wiring
src/lightspeed_agentic/providers/claude.py, src/lightspeed_agentic/providers/gemini.py, src/lightspeed_agentic/providers/openai.py
Claude builds an mcp_servers dict and appends wildcard entries to allowed_tools; Gemini appends McpToolset via StreamableHTTPConnectionParams; OpenAI constructs MCPServerStreamableHttp instances, connects before the run, and cleans up in a finally block.
MCP reachability readiness checks
src/lightspeed_agentic/health.py
check_mcp_endpoints() helper probes each MCP server URL via HTTP GET (3s timeout) and keys results as mcp_{name}; run_readiness_checks merges these results when sdk.mcp_servers is non-empty.
Tests, spec, and docs
tests/test_config.py, tests/test_ready.py, .ai/spec/what/*, AGENTS.md
resolve_mcp_servers() unit tests cover valid/invalid JSON, header formats, and SA token mocking; health tests cover check_mcp_endpoints, run_readiness_checks, and /ready 503 on MCP failure; specs and AGENTS.md updated for LIGHTSPEED_MCP_SERVERS, R3 probe, and provider MCP details.
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive No pull request description was provided. While a description is absent, the PR objectives and commit messages clearly document the MCP servers implementation, making this a borderline case that warrants explicit flagging. Add a pull request description explaining the MCP server wiring feature, its integration across providers, and any breaking changes or deployment considerations.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'OLS-3185: mcp servers wiring' directly and specifically references the JIRA issue and describes the main feature being added—MCP server wiring—which aligns with the changeset's comprehensive implementation across configuration, health checks, and all three provider adapters.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@openshift-ci

openshift-ci Bot commented Jun 23, 2026

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign onmete for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@tremes

tremes commented Jun 23, 2026

Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
tests/test_config.py (1)

292-319: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for missing ServiceAccountToken file.

This suite validates unknown header sources, but not the explicit missing-token-file error path for source: "ServiceAccountToken". Add one test that patches _SA_TOKEN_PATH to a non-existent file and asserts the raised ValueError.

Proposed test addition
+def test_mcp_servers_headers_service_account_token_missing_file(
+    monkeypatch: pytest.MonkeyPatch,
+) -> None:
+    _clean_env(monkeypatch)
+    import lightspeed_agentic.config as cfg
+
+    monkeypatch.setattr(cfg, "_SA_TOKEN_PATH", "/tmp/does-not-exist")
+    monkeypatch.setenv(
+        "LIGHTSPEED_MCP_SERVERS",
+        json.dumps(
+            [
+                {
+                    "name": "k8s",
+                    "url": "http://mcp.local/mcp",
+                    "headers": [
+                        {"name": "kubernetes-authorization", "source": "ServiceAccountToken"}
+                    ],
+                }
+            ]
+        ),
+    )
+    with pytest.raises(ValueError, match="requires"):
+        resolve_mcp_servers()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_config.py` around lines 292 - 319, Add a new test function to
cover the error case when the ServiceAccountToken file does not exist. Create a
test similar to test_mcp_servers_headers_list_with_source that sets up an MCP
server configuration with a ServiceAccountToken header source, but patches
_SA_TOKEN_PATH to point to a non-existent file path. Use pytest.raises to assert
that calling resolve_mcp_servers() raises a ValueError when the token file
cannot be found, ensuring the error handling for missing token files is properly
tested.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.ai/spec/what/configuration.md:
- Line 20: The `headers` field in the LIGHTSPEED_MCP_SERVERS configuration
documentation only describes the object format but the implementation supports
both object and list formats. Update the documentation for the `headers`
parameter at line 20 to include both supported formats: the object format
(existing documentation) and the list format with entries containing "name" and
either "value" or "source" fields. Also apply the same fix to the additional
occurrences of this documentation at lines 70 and 87 to ensure consistency
across the specification.

In `@src/lightspeed_agentic/config.py`:
- Around line 56-57: The dict path (returning raw) and the list-form path
(coercing with str()) validate headers inconsistently, allowing non-string
header names or values to pass through despite the type hint headers: dict[str,
str]. Add validation in both paths to ensure all header names and values are
strings before returning, rejecting non-string values at startup. Apply the same
validation logic to both the dict return statement around line 56-57 and the
list-form value coercion around line 70-73 to maintain consistency.
- Around line 92-103: Add validation to detect and reject duplicate MCP server
names during parsing. Initialize a set to track seen server names before the
loop that processes entries, then before appending each McpServerConfig to the
servers list, check if the current name already exists in the seen names set. If
a duplicate is found, raise a ValueError with a message indicating that
duplicate server names are not allowed. Add the name to the seen names set after
all validations pass but before appending the server configuration.

In `@src/lightspeed_agentic/providers/openai.py`:
- Around line 241-244: The MCP server connection loop (iterating over
mcp_server_instances and calling srv.connect()) is positioned before the
try/finally block, which means if a connection fails partway through, the
cleanup code in the finally block will never execute and leave open sessions
uncleaned. Move the entire for loop that connects to mcp_server_instances to be
inside the try block, after the try statement but before any other code, so that
all connection attempts are protected by the cleanup guard in the corresponding
finally block. This same pattern issue also appears at lines 289-291, so apply
the same fix there as well.

---

Nitpick comments:
In `@tests/test_config.py`:
- Around line 292-319: Add a new test function to cover the error case when the
ServiceAccountToken file does not exist. Create a test similar to
test_mcp_servers_headers_list_with_source that sets up an MCP server
configuration with a ServiceAccountToken header source, but patches
_SA_TOKEN_PATH to point to a non-existent file path. Use pytest.raises to assert
that calling resolve_mcp_servers() raises a ValueError when the token file
cannot be found, ensuring the error handling for missing token files is properly
tested.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 07a98632-5383-434e-b483-e7ad7d161880

📥 Commits

Reviewing files that changed from the base of the PR and between 34a0950 and 84e3fa9.

📒 Files selected for processing (15)
  • .ai/spec/what/configuration.md
  • .ai/spec/what/health-probes.md
  • .ai/spec/what/provider-contract.md
  • AGENTS.md
  • src/lightspeed_agentic/app.py
  • src/lightspeed_agentic/config.py
  • src/lightspeed_agentic/health.py
  • src/lightspeed_agentic/providers/claude.py
  • src/lightspeed_agentic/providers/gemini.py
  • src/lightspeed_agentic/providers/openai.py
  • src/lightspeed_agentic/routes/__init__.py
  • src/lightspeed_agentic/routes/query.py
  • src/lightspeed_agentic/types.py
  • tests/test_config.py
  • tests/test_ready.py
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • openshift/lightspeed-agentic-operator (manual)

| `LIGHTSPEED_PROVIDER_PROJECT` | When provider=`vertex` | Cloud project ID |
| `LIGHTSPEED_PROVIDER_REGION` | When provider=`vertex` or `bedrock` | Cloud region |
| `LIGHTSPEED_PROVIDER_API_VERSION` | When provider=`azure` | API version |
| `LIGHTSPEED_MCP_SERVERS` | No | JSON array of MCP server configs: `[{"name":"…","url":"…","headers":{}}]` |

Copy link
Copy Markdown

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 headers contract to include list-format entries.

The spec currently documents headers as 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
-| `LIGHTSPEED_MCP_SERVERS` | No | JSON array of MCP server configs: `[{"name":"…","url":"…","headers":{}}]` |
+| `LIGHTSPEED_MCP_SERVERS` | No | JSON array of MCP server configs: `[{"name":"…","url":"…","headers":{}}]` or `[{"name":"…","url":"…","headers":[{"name":"h","value":"v"}]}]` |

-19. **MCP server configuration.** ... Each entry MUST have `name` (string) and `url` (string); `headers` (object) is optional.
+19. **MCP server configuration.** ... Each entry MUST have `name` (string) and `url` (string); `headers` is optional and may be an object (`{"h":"v"}`) or an array of objects (`[{"name":"h","value":"v"}]` or `[{"name":"h","source":"ServiceAccountToken"}]`).

-| `LIGHTSPEED_MCP_SERVERS` | Optional JSON array of MCP server endpoint configs (see rule 19). |
+| `LIGHTSPEED_MCP_SERVERS` | Optional JSON array of MCP endpoint configs; `headers` supports object and list-entry formats (see rule 19). |

Also applies to: 70-70, 87-87

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.ai/spec/what/configuration.md at line 20, The `headers` field in the
LIGHTSPEED_MCP_SERVERS configuration documentation only describes the object
format but the implementation supports both object and list formats. Update the
documentation for the `headers` parameter at line 20 to include both supported
formats: the object format (existing documentation) and the list format with
entries containing "name" and either "value" or "source" fields. Also apply the
same fix to the additional occurrences of this documentation at lines 70 and 87
to ensure consistency across the specification.

Comment on lines +56 to +57
if isinstance(raw, dict):
return raw

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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 raw unchanged, while list-form value is coerced with str(). Non-string header names/values can reach provider HTTP clients despite headers: dict[str, str]; reject them at startup in both formats.

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] = value

As per path instructions, “Validate at trust boundaries with allow-lists, not deny-lists.”

Also applies to: 70-73

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lightspeed_agentic/config.py` around lines 56 - 57, The dict path
(returning raw) and the list-form path (coercing with str()) validate headers
inconsistently, allowing non-string header names or values to pass through
despite the type hint headers: dict[str, str]. Add validation in both paths to
ensure all header names and values are strings before returning, rejecting
non-string values at startup. Apply the same validation logic to both the dict
return statement around line 56-57 and the list-form value coercion around line
70-73 to maintain consistency.

Source: Path instructions

Comment on lines +92 to +103
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))

Copy link
Copy Markdown

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

Reject duplicate MCP server names.

Duplicate name values are accepted here, but downstream consumers key by name: Claude’s dict keeps only one server and health results overwrite mcp_{name}. Fail fast during parsing.

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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))
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 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")
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))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lightspeed_agentic/config.py` around lines 92 - 103, Add validation to
detect and reject duplicate MCP server names during parsing. Initialize a set to
track seen server names before the loop that processes entries, then before
appending each McpServerConfig to the servers list, check if the current name
already exists in the seen names set. If a duplicate is found, raise a
ValueError with a message indicating that duplicate server names are not
allowed. Add the name to the seen names set after all validations pass but
before appending the server configuration.

Comment on lines +241 to +244
for srv in mcp_server_instances:
await srv.connect()

try:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Put MCP connects inside the cleanup guard.

If one server connects and a later srv.connect() raises, execution never enters the try/finally, so already-open MCP sessions are not cleaned up.

Proposed lifecycle guard
-        for srv in mcp_server_instances:
-            await srv.connect()
-
+        connected_mcp_servers: list[Any] = []
         try:
+            for srv in mcp_server_instances:
+                await srv.connect()
+                connected_mcp_servers.append(srv)
+
             run_config = RunConfig(
                 sandbox=SandboxRunConfig(
                     client=UnixLocalSandboxClient(),
@@
             )
         finally:
-            for srv in mcp_server_instances:
+            for srv in connected_mcp_servers:
                 await srv.cleanup()

Also applies to: 289-291

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lightspeed_agentic/providers/openai.py` around lines 241 - 244, The MCP
server connection loop (iterating over mcp_server_instances and calling
srv.connect()) is positioned before the try/finally block, which means if a
connection fails partway through, the cleanup code in the finally block will
never execute and leave open sessions uncleaned. Move the entire for loop that
connects to mcp_server_instances to be inside the try block, after the try
statement but before any other code, so that all connection attempts are
protected by the cleanup guard in the corresponding finally block. This same
pattern issue also appears at lines 289-291, so apply the same fix there as
well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. jira/valid-reference Indicates that this PR references a valid Jira ticket of any type.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants