Skip to content

Comments

LCORE-1247: E2E Tests for MCP OAuth#1177

Open
jrobertboos wants to merge 16 commits intolightspeed-core:mainfrom
jrobertboos:lcore-1247
Open

LCORE-1247: E2E Tests for MCP OAuth#1177
jrobertboos wants to merge 16 commits intolightspeed-core:mainfrom
jrobertboos:lcore-1247

Conversation

@jrobertboos
Copy link
Contributor

@jrobertboos jrobertboos commented Feb 18, 2026

Description

Created e2e tests for MCP OAuth. The tests that were created are only meant to verify that upon attempted access of a secured MCP server a 401 Unauthorized is returned that contains the www-authenticate header.

These were tested for the following endpoints:

  • /query
  • /streaming_query
  • /tools

Additionally a bug was fixed for the /tools endpoint which would suffer internal failure when in library mode due to a non llama-stack-client defined error being raised. See /src/app/endpoints/tools.py for the bug fix.

Type of change

  • Refactor
  • New feature
  • Bug fix
  • CVE fix
  • Optimization
  • Documentation Update
  • Configuration Update
  • Bump-up service version
  • Bump-up dependent library
  • Bump-up library or tool used for development (does not change the final image)
  • CI configuration change
  • Konflux configuration change
  • Unit tests improvement
  • Integration tests improvement
  • End to end tests improvement
  • Benchmarks improvement

Tools used to create PR

Identify any AI code assistants used in this PR (for transparency and review context)

  • Assisted-by: Cursor
  • Generated by: N/A

Related Tickets & Documents

  • Related Issue LCORE-1247
  • Closes

Checklist before requesting a review

  • I have performed a self-review of my code.
  • PR has passed all pre-merge test jobs.
  • If it is a core feature, I have added thorough tests.

Testing

  • Please provide detailed steps to perform tests related to this code change.
  • How were the fix/results from this change verified? Please provide relevant screenshots or results.

Summary by CodeRabbit

  • New Features

    • MCP integration with OAuth and a local mock MCP service (with health checks) for e2e testing.
    • MCP-specific deployment/configuration options for both library and server modes.
  • Bug Fixes

    • Broadened authentication error handling so auth failures follow the OAuth probe / unauthorized response flow.
  • Tests

    • Added comprehensive e2e MCP tests, MCP-tagged test lifecycle handling, header assertion/setting steps, and updated test listing.

…port

- Removed the old `mcp-mock-server` service and replaced it with `mock-mcp` in both `docker-compose` files.
- Updated health check and dependencies to reflect the new service name.
- Modified E2E test configurations to use the new `mock-mcp` service URL.
- Added a minimal mock MCP server implementation with OAuth support for testing.
- Updated feature tests to check for OAuth authentication requirements and response headers.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 18, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds MCP (Model Context Protocol) OAuth test support: a mock OAuth-capable MCP server, docker-compose wiring, MCP-specific e2e configs and feature tests, test harness hooks to swap configs for MCP-tagged scenarios, and broadened tools endpoint authentication error handling.

Changes

Cohort / File(s) Summary
Docker Compose
docker-compose-library.yaml, docker-compose.yaml
Add mock-mcp service (build, container_name, port 3001, network, healthcheck) and make lightspeed-stack depend on mock-mcp with service_healthy.
Endpoint auth handling
src/app/endpoints/tools.py
Import AuthenticationRequiredError and expand exception handling to catch (AuthenticationError, AuthenticationRequiredError) and follow existing OAuth probing / 401 flow.
MCP configs
tests/e2e/configuration/library-mode/lightspeed-stack-mcp.yaml, tests/e2e/configuration/server-mode/lightspeed-stack-mcp.yaml
Add MCP-enabled Lightspeed configs (library/server modes) referencing http://mock-mcp:3001 and set Authorization header for the provider.
Mock MCP server
tests/e2e/mock_mcp_server/Dockerfile, tests/e2e/mock_mcp_server/server.py
Add Dockerfile and a minimal HTTP mock implementing /health, OAuth Bearer challenge (WWW-Authenticate), and JSON-RPC-like POST handling for initialize and tools/list.
E2E test harness & steps
tests/e2e/features/environment.py, tests/e2e/features/steps/common_http.py, tests/e2e/test_list.txt
Add MCP feature-tag setup/teardown (swap MCP configs, restart container, restore), add steps to set/check headers, and register features/mcp.feature in test list.
E2E features
tests/e2e/features/mcp.feature, tests/e2e/features/info.feature, tests/e2e/features/query.feature, tests/e2e/features/streaming_query.feature
Add mcp.feature with scenarios asserting 401 + WWW-Authenticate for tools/query/streaming_query; minor formatting adjustments in other feature files.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client
    rect rgba(200,200,255,0.5)
    participant LS as Lightspeed Stack
    end
    rect rgba(200,255,200,0.5)
    participant MCP as Mock-MCP (oauth)
    end

    Client->>LS: Request (e.g., POST /v1/query) without credentials
    LS->>MCP: Probe MCP (HTTP) to determine auth requirement
    MCP-->>LS: 401 + WWW-Authenticate: Bearer realm="mock-mcp", error="invalid_token"
    LS-->>Client: 401 + JSON error (detail.cause = "MCP server at http://mock-mcp:3001 requires OAuth") + WWW-Authenticate
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

ok-to-test

Suggested reviewers

  • tisnik
  • radofuchs
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding end-to-end tests for MCP OAuth, which aligns with the primary focus of the PR.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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 and usage tips.

…port

- Removed the old `mcp-mock-server` service and replaced it with `mock-mcp` in both `docker-compose` files.
- Updated health check and dependencies to reflect the new service name.
- Modified E2E test configurations to use the new `mock-mcp` service URL.
- Added a minimal mock MCP server implementation with OAuth support for testing.
- Updated feature tests to check for OAuth authentication requirements and response headers.
- Modified feature files to change the wording for checking response headers from "contains the following" to "contains the following header".
- Updated the corresponding step definition in common_http.py to reflect the new header check format.
- Introduced new feature tests for MCP authentication scenarios, including checks for tools, query, and streaming_query endpoints.
- Added MCP configuration files for both library and server modes.
- Updated the environment setup to switch configurations based on feature tags.
- Removed outdated authentication checks from existing feature files to streamline tests.
@jrobertboos jrobertboos marked this pull request as ready for review February 23, 2026 17:54
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (4)
tests/e2e/features/steps/common_http.py (1)

191-197: Prefer direct membership test over .keys() lookup.

context.response.headers is a requests.structures.CaseInsensitiveDict. The in operator on it already performs a case-insensitive lookup via __contains__, so calling .keys() adds an extra KeysView indirection unnecessarily.

♻️ Suggested simplification
-    assert (
-        header_name in context.response.headers.keys()
-    ), f"The response headers '{context.response.headers}' doesn't contain header '{header_name}'"
+    assert (
+        header_name in context.response.headers
+    ), f"The response headers '{context.response.headers}' doesn't contain header '{header_name}'"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/e2e/features/steps/common_http.py` around lines 191 - 197, The
membership test in check_response_headers_contains unnecessarily calls .keys()
on context.response.headers; change the assertion to use direct membership
(header_name in context.response.headers) since context.response.headers is a
CaseInsensitiveDict and its __contains__ already does case-insensitive lookup —
update the assert that currently checks header_name in
context.response.headers.keys() to use header_name in context.response.headers
and keep the rest of the error message unchanged.
tests/e2e/mock_mcp_server/Dockerfile (1)

1-5: Root user is optional for test containers; pip install is not needed.

server.py uses only Python standard library (json, http.server, typing), so the missing pip install step is not a concern. The Dockerfile will function correctly as-is.

The root user issue (Trivy DS-0002) remains a valid security best practice, but for E2E test infrastructure running in CI, adding a non-root user is optional rather than critical.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/e2e/mock_mcp_server/Dockerfile` around lines 1 - 5, The Dockerfile
currently runs as root (CMD ["python", "server.py"]) which triggers DS-0002; to
address this, create a non-root user and switch to it before CMD: add steps to
create a group/user (e.g., "app" or "nonroot"), chown /app to that user, and set
USER to that account so the container runs python server.py as non-root; ensure
ownership change references the WORKDIR (/app) and the CMD entry (python
server.py) remains unchanged.
tests/e2e/configuration/library-mode/lightspeed-stack-mcp.yaml (1)

24-25: Consider documenting why Authorization: "oauth" is intentionally non-standard.

The value "oauth" is not a valid Bearer token (it lacks the Bearer prefix), which is what causes the mock MCP server to return 401 — the exact behavior the E2E tests are exercising. Without a comment, this looks like a misconfiguration. The same applies to the server-mode config.

✏️ Suggested clarification
 mcp_servers:
   - name: "mcp-oauth"
     provider_id: "model-context-protocol"
     url: "http://mock-mcp:3001"
     authorization_headers:
-      Authorization: "oauth"
+      # Intentionally not a valid Bearer token — causes mock-mcp to return 401
+      # so E2E tests can verify the OAuth error propagation path.
+      Authorization: "oauth"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/e2e/configuration/library-mode/lightspeed-stack-mcp.yaml` around lines
24 - 25, Add a short inline comment next to the authorization_headers
Authorization: "oauth" entry explaining that the value is intentionally
non-standard (missing the "Bearer " prefix) to deliberately trigger a 401 from
the mock MCP server for the E2E tests; update the equivalent server-mode config
similarly so both configs explicitly state this is intentional and not a
misconfiguration (refer to the authorization_headers map and the Authorization
key when making the change).
tests/e2e/configuration/server-mode/lightspeed-stack-mcp.yaml (1)

25-26: Same documentation gap for Authorization: "oauth" as in the library-mode config.

The value "oauth" is intentionally invalid to exercise the 401 path; a clarifying comment (as suggested for the library-mode YAML) would benefit this file equally.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/e2e/configuration/server-mode/lightspeed-stack-mcp.yaml` around lines
25 - 26, Add a clarifying inline comment next to the Authorization header entry
to indicate that Authorization: "oauth" is intentionally invalid and used to
exercise the 401 authentication path; update the authorization_headers ->
Authorization value comment so readers/testers understand this is deliberate
(refer to the Authorization key under authorization_headers in the
lightspeed-stack-mcp.yaml snippet).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/e2e/features/mcp.feature`:
- Line 23: The scenario titles in the feature file contain a duplicated word
"error error"; update both Scenario lines (the one shown and the other at the
same file referenced) to remove the duplicate so they read e.g. "Scenario: Check
if query endpoint reports error when mcp requires authentication" (apply the
same change to the other occurrence).

---

Nitpick comments:
In `@tests/e2e/configuration/library-mode/lightspeed-stack-mcp.yaml`:
- Around line 24-25: Add a short inline comment next to the
authorization_headers Authorization: "oauth" entry explaining that the value is
intentionally non-standard (missing the "Bearer " prefix) to deliberately
trigger a 401 from the mock MCP server for the E2E tests; update the equivalent
server-mode config similarly so both configs explicitly state this is
intentional and not a misconfiguration (refer to the authorization_headers map
and the Authorization key when making the change).

In `@tests/e2e/configuration/server-mode/lightspeed-stack-mcp.yaml`:
- Around line 25-26: Add a clarifying inline comment next to the Authorization
header entry to indicate that Authorization: "oauth" is intentionally invalid
and used to exercise the 401 authentication path; update the
authorization_headers -> Authorization value comment so readers/testers
understand this is deliberate (refer to the Authorization key under
authorization_headers in the lightspeed-stack-mcp.yaml snippet).

In `@tests/e2e/features/steps/common_http.py`:
- Around line 191-197: The membership test in check_response_headers_contains
unnecessarily calls .keys() on context.response.headers; change the assertion to
use direct membership (header_name in context.response.headers) since
context.response.headers is a CaseInsensitiveDict and its __contains__ already
does case-insensitive lookup — update the assert that currently checks
header_name in context.response.headers.keys() to use header_name in
context.response.headers and keep the rest of the error message unchanged.

In `@tests/e2e/mock_mcp_server/Dockerfile`:
- Around line 1-5: The Dockerfile currently runs as root (CMD ["python",
"server.py"]) which triggers DS-0002; to address this, create a non-root user
and switch to it before CMD: add steps to create a group/user (e.g., "app" or
"nonroot"), chown /app to that user, and set USER to that account so the
container runs python server.py as non-root; ensure ownership change references
the WORKDIR (/app) and the CMD entry (python server.py) remains unchanged.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a263db1 and bc00db3.

📒 Files selected for processing (14)
  • docker-compose-library.yaml
  • docker-compose.yaml
  • src/app/endpoints/tools.py
  • tests/e2e/configuration/library-mode/lightspeed-stack-mcp.yaml
  • tests/e2e/configuration/server-mode/lightspeed-stack-mcp.yaml
  • tests/e2e/features/environment.py
  • tests/e2e/features/info.feature
  • tests/e2e/features/mcp.feature
  • tests/e2e/features/query.feature
  • tests/e2e/features/steps/common_http.py
  • tests/e2e/features/streaming_query.feature
  • tests/e2e/mock_mcp_server/Dockerfile
  • tests/e2e/mock_mcp_server/server.py
  • tests/e2e/test_list.txt

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/e2e/mock_mcp_server/server.py`:
- Around line 53-106: The Content-Length header parsing in do_POST currently
does int(self.headers.get("Content-Length", 0)) which can raise ValueError for
non-numeric headers and crash the handler; update do_POST to defensively parse
Content-Length (e.g., inspect headers.get("Content-Length"), try/except
ValueError around int(), or validate with str.isdigit()) and fall back to 0 on
parse failure, then use that safe length value for reading from self.rfile and
subsequent JSON decoding.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0a50741 and 7070af0.

📒 Files selected for processing (1)
  • tests/e2e/mock_mcp_server/server.py

Comment on lines +53 to +106
def do_POST(self) -> None: # pylint: disable=invalid-name
"""Handle POST requests."""
if self._parse_auth() is None:
self._require_oauth()
return

length = int(self.headers.get("Content-Length", 0))
raw = self.rfile.read(length) if length else b"{}"
try:
req = json.loads(raw.decode("utf-8"))
req_id = req.get("id", 1)
method = req.get("method", "")
except (json.JSONDecodeError, UnicodeDecodeError):
req_id = 1
method = ""

if method == "initialize":
self._json_response(
{
"jsonrpc": "2.0",
"id": req_id,
"result": {
"protocolVersion": "2024-11-05",
"capabilities": {"tools": {}},
"serverInfo": {"name": "mock-mcp-e2e", "version": "1.0.0"},
},
}
)
elif method == "tools/list":
self._json_response(
{
"jsonrpc": "2.0",
"id": req_id,
"result": {
"tools": [
{
"name": "mock_tool_e2e",
"description": "Mock tool for E2E",
"inputSchema": {
"type": "object",
"properties": {
"message": {
"type": "string",
"description": "Test message",
}
},
},
}
],
},
}
)
else:
self._json_response({"jsonrpc": "2.0", "id": req_id, "result": {}})
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Guard invalid Content-Length to avoid 500s on malformed headers.
int() can raise ValueError if the header is non-numeric, which would crash the request handler. Consider a safe fallback.

🛠️ Suggested defensive parse
-        length = int(self.headers.get("Content-Length", 0))
+        try:
+            length = int(self.headers.get("Content-Length", 0))
+        except ValueError:
+            length = 0
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/e2e/mock_mcp_server/server.py` around lines 53 - 106, The
Content-Length header parsing in do_POST currently does
int(self.headers.get("Content-Length", 0)) which can raise ValueError for
non-numeric headers and crash the handler; update do_POST to defensively parse
Content-Length (e.g., inspect headers.get("Content-Length"), try/except
ValueError around int(), or validate with str.isdigit()) and fall back to 0 on
parse failure, then use that safe length value for reading from self.rfile and
subsequent JSON decoding.

- Implemented scenarios to verify successful and error responses for the 'tools', 'query', and 'streaming_query' endpoints when valid and invalid MCP auth tokens are provided.
- Enhanced the common HTTP step definitions to support setting headers dynamically.
- Updated the mock MCP server to handle invalid tokens appropriately.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
tests/e2e/mock_mcp_server/server.py (1)

53-66: ⚠️ Potential issue | 🟡 Minor

Guard Content-Length parsing to avoid 500s on malformed headers.
int() will raise ValueError for non-numeric values.

🛠️ Suggested defensive parse
-        length = int(self.headers.get("Content-Length", 0))
+        try:
+            length = int(self.headers.get("Content-Length", 0))
+        except ValueError:
+            length = 0
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/e2e/mock_mcp_server/server.py` around lines 53 - 66, In do_POST,
parsing Content-Length with int(self.headers.get("Content-Length", 0)) can raise
ValueError for non-numeric headers; wrap that conversion in a try/except (catch
ValueError and TypeError) and fallback to 0 (or a safe default) before reading
self.rfile, so length is always an integer; update the logic around the length,
raw, and subsequent json.loads handling to use the guarded length value
(references: do_POST, length, self.headers.get("Content-Length"), raw).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@tests/e2e/mock_mcp_server/server.py`:
- Around line 53-66: In do_POST, parsing Content-Length with
int(self.headers.get("Content-Length", 0)) can raise ValueError for non-numeric
headers; wrap that conversion in a try/except (catch ValueError and TypeError)
and fallback to 0 (or a safe default) before reading self.rfile, so length is
always an integer; update the logic around the length, raw, and subsequent
json.loads handling to use the guarded length value (references: do_POST,
length, self.headers.get("Content-Length"), raw).

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7070af0 and a4050c3.

📒 Files selected for processing (4)
  • tests/e2e/features/mcp.feature
  • tests/e2e/features/steps/common_http.py
  • tests/e2e/features/steps/llm_query_response.py
  • tests/e2e/mock_mcp_server/server.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/e2e/features/steps/common_http.py

context.response = requests.post(url, json=data, timeout=DEFAULT_LLM_TIMEOUT)


@step('I use "{endpoint}" to ask question with headers')
Copy link
Contributor

Choose a reason for hiding this comment

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

why do you need to define new step for this? It only adds confusion since you do not change the body in any way

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants