Skip to content

LCORE-1368: Enable MCP Headers in /tools#1216

Open
jrobertboos wants to merge 1 commit intolightspeed-core:mainfrom
jrobertboos:lcore-1368
Open

LCORE-1368: Enable MCP Headers in /tools#1216
jrobertboos wants to merge 1 commit intolightspeed-core:mainfrom
jrobertboos:lcore-1368

Conversation

@jrobertboos
Copy link
Contributor

@jrobertboos jrobertboos commented Feb 24, 2026

Description

Adds the ability to pass MCP-HEADERS to the MCP servers accessed by the /tools endpoint.

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 #
  • Closes LCORE-1368

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

Release Notes

  • Bug Fixes
    • Tools endpoint now properly propagates authorization headers on a per-toolgroup basis, ensuring correct request handling across multiple tool operations.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 24, 2026

Walkthrough

The changes add MCP headers support to the tools endpoint by injecting McpHeaders via dependency, extracting per-toolgroup headers from the MCP context, and propagating them to client.tools.list calls along with separated authorization handling. Tests are updated to match the new endpoint signature and verify header/query argument passing.

Changes

Cohort / File(s) Summary
MCP Headers Integration
src/app/endpoints/tools.py
Added McpHeaders dependency injection. Within per-toolgroup processing loop, extract per-toolgroup headers from mcp_headers context, pop Authorization header, and pass remaining headers and authorization to client.tools.list via extra_headers and extra_query parameters.
Test Updates
tests/unit/app/endpoints/test_tools.py
Updated endpoint handler wrapper calls to pass empty extra dict ({}). Updated mock assertions to expect extra_headers and extra_query keyword arguments containing authorization in client.tools.list invocations.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

enhancement

Suggested reviewers

  • tisnik
  • umago
  • manstis
🚥 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 and concisely describes the main change: enabling MCP headers in the /tools endpoint, which matches the core objective of passing MCP-HEADERS to MCP servers accessed by this endpoint.
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 docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 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.

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.

🧹 Nitpick comments (3)
tests/unit/app/endpoints/test_tools.py (1)

192-225: No test covers the actual feature: MCP headers (including Authorization) passed to toolgroup calls

All tests pass {} as mcp_headers, so the new code path at lines 91–97 of tools.py — extracting per-toolgroup headers, separating Authorization, and forwarding both — is never exercised. At minimum, a test like the following is needed:

`@pytest.mark.asyncio`
async def test_tools_endpoint_with_mcp_headers(
    mocker: MockerFixture,
    mock_configuration: Configuration,
    mock_tools_response: list[MockType],
) -> None:
    """Test that per-toolgroup MCP headers and Authorization are propagated correctly."""
    ...
    mcp_headers = {
        "filesystem-tools": {
            "Authorization": "Bearer secret-token",
            "X-Custom-Header": "custom-value",
        }
    }

    response = await tools.tools_endpoint_handler.__wrapped__(
        mock_request, mock_auth, mcp_headers
    )

    mock_client.tools.list.assert_any_call(
        toolgroup_id="filesystem-tools",
        extra_headers={"X-Custom-Header": "custom-value"},   # Authorization stripped
        extra_query={"authorization": "Bearer secret-token"},
    )

This is the primary new behaviour introduced by the PR and needs direct coverage.

Would you like me to generate the full test, including a case for a toolgroup absent from mcp_headers (empty headers) and one verifying that only the matching toolgroup receives its headers?

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

In `@tests/unit/app/endpoints/test_tools.py` around lines 192 - 225, Add a unit
test that exercises the MCP header propagation path in
tools.tools_endpoint_handler: call
tools.tools_endpoint_handler.__wrapped__(mock_request, mock_auth, mcp_headers)
where mcp_headers includes an entry for "filesystem-tools" with Authorization
and another custom header; assert mock_client.tools.list was called for
filesystem-tools with extra_headers containing only the custom header
(Authorization removed) and extra_query containing authorization set to the
token, and also include a case where a toolgroup not present in mcp_headers
yields empty extra_headers/extra_query; reference tools.tools_endpoint_handler
and the header-extraction code paths around the new logic in tools.py to locate
where to invoke and assert behavior.
src/app/endpoints/tools.py (2)

91-92: headers.pop() mutates the dict stored inside mcp_headers — guideline violation

mcp_headers.get(toolgroup.identifier, {}) returns a direct reference to the value dict in the mcp_headers mapping (not a copy). Calling .pop("Authorization", None) on it mutates the mcp_headers data structure in place. Any post-loop access to mcp_headers (e.g., debug logging, future retry logic) would see a partially stripped state.

♻️ Proposed fix — copy before mutating
-            headers = mcp_headers.get(toolgroup.identifier, {})
-            authorization = headers.pop("Authorization", None)
+            all_headers = mcp_headers.get(toolgroup.identifier, {})
+            authorization = all_headers.get("Authorization")
+            headers = {k: v for k, v in all_headers.items() if k != "Authorization"}

As per coding guidelines: "Avoid in-place parameter modification anti-patterns; return new data structures instead."

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

In `@src/app/endpoints/tools.py` around lines 91 - 92, The code mutates the dict
stored in mcp_headers by calling headers.pop(...); instead, make a shallow copy
of the headers dict returned by mcp_headers.get(toolgroup.identifier, {}) before
modifying it (e.g., use dict(...) or .copy()), then pop "Authorization" from
that copy so mcp_headers remains unchanged; update the logic around mcp_headers,
headers, and authorization in the same scope (referencing mcp_headers and
toolgroup.identifier) to use the copied dict.

47-47: Prefer Annotated style for dependency injection to match the rest of the signature

auth on line 46 uses the modern Annotated[T, Depends(...)] form; mcp_headers uses the legacy default-value style, creating an inconsistency in the same function signature.

♻️ Proposed refactor
-    mcp_headers: McpHeaders = Depends(mcp_headers_dependency),
+    mcp_headers: Annotated[McpHeaders, Depends(mcp_headers_dependency)],
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/endpoints/tools.py` at line 47, The mcp_headers parameter uses the
legacy default-value dependency style instead of the modern Annotated form used
by auth; update the function signature to use Annotated[McpHeaders,
Depends(mcp_headers_dependency)] for mcp_headers (replace the current
mcp_headers: McpHeaders = Depends(mcp_headers_dependency) pattern), ensure
Annotated is imported (from typing or typing_extensions as your project uses),
and keep the rest of the signature (including auth) unchanged so that
mcp_headers, mcp_headers_dependency, Annotated, Depends and McpHeaders are
consistently used.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/app/endpoints/tools.py`:
- Around line 91-92: The code mutates the dict stored in mcp_headers by calling
headers.pop(...); instead, make a shallow copy of the headers dict returned by
mcp_headers.get(toolgroup.identifier, {}) before modifying it (e.g., use
dict(...) or .copy()), then pop "Authorization" from that copy so mcp_headers
remains unchanged; update the logic around mcp_headers, headers, and
authorization in the same scope (referencing mcp_headers and
toolgroup.identifier) to use the copied dict.
- Line 47: The mcp_headers parameter uses the legacy default-value dependency
style instead of the modern Annotated form used by auth; update the function
signature to use Annotated[McpHeaders, Depends(mcp_headers_dependency)] for
mcp_headers (replace the current mcp_headers: McpHeaders =
Depends(mcp_headers_dependency) pattern), ensure Annotated is imported (from
typing or typing_extensions as your project uses), and keep the rest of the
signature (including auth) unchanged so that mcp_headers,
mcp_headers_dependency, Annotated, Depends and McpHeaders are consistently used.

In `@tests/unit/app/endpoints/test_tools.py`:
- Around line 192-225: Add a unit test that exercises the MCP header propagation
path in tools.tools_endpoint_handler: call
tools.tools_endpoint_handler.__wrapped__(mock_request, mock_auth, mcp_headers)
where mcp_headers includes an entry for "filesystem-tools" with Authorization
and another custom header; assert mock_client.tools.list was called for
filesystem-tools with extra_headers containing only the custom header
(Authorization removed) and extra_query containing authorization set to the
token, and also include a case where a toolgroup not present in mcp_headers
yields empty extra_headers/extra_query; reference tools.tools_endpoint_handler
and the header-extraction code paths around the new logic in tools.py to locate
where to invoke and assert behavior.

ℹ️ 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 051c06c and e65ff18.

📒 Files selected for processing (2)
  • src/app/endpoints/tools.py
  • tests/unit/app/endpoints/test_tools.py

tools_response = await client.tools.list(
toolgroup_id=toolgroup.identifier,
extra_headers=headers,
extra_query={"authorization": authorization},
Copy link
Contributor Author

@jrobertboos jrobertboos Feb 24, 2026

Choose a reason for hiding this comment

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

I am getting slightly weird behavior with this implementation ...

It works for authorizing the MCP server however once I have sent a valid authorization token it does not need the token again. Basically if I access /tools with MCP-HEADERS set it will successfully authorize however if I access /tools again without passing MCP-HEADERS it will still successfully authorize and not return a 401. This behavior persists until I restart the llama-stack server.

Im guessing that this is because the tool list is either cached or the authorization header is persisted but I am not sure just yet.

Copy link
Contributor

@tisnik tisnik left a comment

Choose a reason for hiding this comment

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

Does not it break API compatibility. Is the OpenAPI spec affected by this change?

@jrobertboos
Copy link
Contributor Author

I don't think it breaks API compatibility, because the headers are optional... when looking at the query/streaming_query openapi nothing was added regarding the MCP headers. I could be missing something but I don't think so.

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.

2 participants