Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
103 commits
Select commit Hold shift + click to select a range
d0a8878
MCP authentication parameter implementation
Nov 3, 2025
57eb575
Added minor changes
Nov 3, 2025
c49fef8
precommit
Nov 4, 2025
1143db0
added a fix
Nov 4, 2025
376f0fc
minor fix
Nov 4, 2025
9dbeeac
Removed the MCPAuthorization class relying on bearer token
Nov 4, 2025
d2103eb
precommit
Nov 4, 2025
0487496
precommit
Nov 4, 2025
fec6f20
reverted some formatting changes
Nov 4, 2025
abc717e
reverted some formatting changes
Nov 4, 2025
1db14ca
removed _convert_authorization_to_headers
Nov 4, 2025
59793ac
minor linting change
Nov 4, 2025
a23ee35
reverting some formatting changes
Nov 4, 2025
6bd0d64
reverting some formatting
Nov 4, 2025
c911e9a
minor formatting change
Nov 4, 2025
5c5f6f7
updated the test script
Nov 4, 2025
8632c70
Merge branch 'main' into add-mcp-authentication-param
omaryashraf5 Nov 5, 2025
09ef0b3
Updated the authentication field to take just the token
Nov 5, 2025
b8c2419
precommit
Nov 5, 2025
dcb3dc4
raising an error when the authentication field is present in the auth…
Nov 5, 2025
a605cc2
formatting
Nov 5, 2025
76fdff4
created a single helper function and updated list_mcp_tools and invok…
Nov 5, 2025
7db4ed7
fix: update MCP tool runtime provider to use new function signatures
Nov 5, 2025
411b18a
Merge branch 'main' into add-mcp-authentication-param
omaryashraf5 Nov 5, 2025
18aff1a
rejecting headers that include Authorization in the header and pointi…
Nov 6, 2025
d58da03
fix: update test to use authorization parameter instead of headers
Nov 6, 2025
dbe41d9
Updated a single test case to not include authorization field in the …
Nov 6, 2025
e8cb526
Updated get_headers_from_request
Nov 6, 2025
ac9442e
fix: update test_mcp to use authorization parameter instead of headers
Nov 6, 2025
5ce48d2
precommit
Nov 6, 2025
d08c529
formatting issues
Nov 6, 2025
dd9c7b3
removed a small comment
Nov 6, 2025
267c895
precommit
Nov 6, 2025
1c27c1b
feat: add response sanitization and validation for MCP authorization
Nov 7, 2025
8ce30b7
test: update error message match for authorization validation
Nov 7, 2025
50040f3
refactor: move Authorization validation from API model to handler layer
Nov 7, 2025
2b0423c
refactor: move Authorization validation to correct handler file
Nov 7, 2025
a842c90
security: enforce Authorization rejection in remote MCP provider
Nov 7, 2025
445135b
feat: implement dedicated mcp_authorization field for remote provider
Nov 7, 2025
ccb870c
precommit
Nov 7, 2025
a2098ee
docs: add comprehensive docstring for MCPProviderDataValidator
Nov 7, 2025
c563d8a
formatting
Nov 7, 2025
2295a1a
formatting changes
Nov 7, 2025
9e972cf
docs: clarify security mechanism comments in get_headers_from_request
Nov 7, 2025
1a7ba68
Merge branch 'main' into add-mcp-authentication-param
omaryashraf5 Nov 7, 2025
7358312
fix: update tests to use new mcp_authorization field
Nov 7, 2025
0f0aa6a
fix: correct import path for LlamaStackAsLibraryClient in test
Nov 7, 2025
c353873
precommit run
Nov 7, 2025
6716e12
security: exclude mcp_authorization from serialization and logs
Nov 10, 2025
114ab69
Merge branch 'main' into add-mcp-authentication-param
omaryashraf5 Nov 10, 2025
5c6f713
Merge branch 'main' into add-mcp-authentication-param
omaryashraf5 Nov 10, 2025
30a544f
Merge branch 'main' into add-mcp-authentication-param
omaryashraf5 Nov 11, 2025
945a288
Merge branch 'main' into add-mcp-authentication-param
omaryashraf5 Nov 11, 2025
893e186
Merge branch 'main' into add-mcp-authentication-param
omaryashraf5 Nov 11, 2025
84baa5c
feat: unify MCP authentication across Responses and Tool Runtime APIs
Nov 12, 2025
d0ec3b0
fix: add authorization parameter to all ToolRuntime provider implemen…
Nov 12, 2025
d804e37
chore: trigger CI rebuild with fresh Python cache
Nov 12, 2025
7a823bc
fix: remove syntax errors from test files caused by sed
Nov 12, 2025
607e3cc
Merge branch 'main' into add-mcp-authentication-param
omaryashraf5 Nov 12, 2025
bf28c21
chore: trigger CI - all provider signatures fixed
Nov 12, 2025
778b7de
fix: add authorization parameter to ToolRuntimeRouter and routing table
Nov 12, 2025
025c301
Fix CI: Force reinstall llama-stack from source
Nov 12, 2025
1ea57b0
Fix CI: Clear Python bytecode cache before reinstall
Nov 12, 2025
6aaf4ad
fix(ci): Remove uv.lock before sync to ensure fresh dependency resolu…
Nov 12, 2025
8b6588d
fix(ci): Clear UV cache directory instead of lock file
Nov 12, 2025
6dc2d92
fix(ci): Clear cached .venv directory to ensure fresh install
Nov 12, 2025
0754d59
fix(ci): Add final bytecode cache clear after installations
Nov 12, 2025
844a159
fix(ci): Install ci-tests distribution dependencies to fix test failures
Nov 12, 2025
761a2a0
fix(ci): Use 'uv run' to execute llama command in virtual environment
Nov 12, 2025
166c37b
fix(ci): Prevent Python from caching old code during uv sync
Nov 12, 2025
bae5b14
debug: Add detailed logging for signature mismatch errors
Nov 13, 2025
d156451
fix(ci): Add authorization parameter to api_recorder tool runtime pat…
Nov 13, 2025
4a1fa13
revert(ci): Remove unnecessary CI workarounds from action.yml
Nov 13, 2025
c0295a2
revert(debug): Remove temporary debug logging from resolver
Nov 13, 2025
18f1977
fix(tool-runtime): Remove authorization from list_runtime_tools()
Nov 13, 2025
e6ebbd8
fix(tool-runtime): Remove authorization from list_runtime_tools in al…
Nov 13, 2025
66ca51a
feat(tool-runtime): Add authorization parameter to list_runtime_tools
Nov 13, 2025
1a6cb70
precommit
Nov 13, 2025
fa2b361
Merge branch 'main' into add-mcp-authentication-param
ashwinb Nov 13, 2025
8783255
feat(tool-runtime): Add authorization parameter with backward compati…
Nov 13, 2025
c1b6320
Updated the test cases to support the headers for now
Nov 13, 2025
9c484d1
Updated some unit tests
Nov 13, 2025
4b6bfba
Added comments and updated model_context_protocol.py
Nov 13, 2025
d913756
updated test_tools_with_schemas
Nov 13, 2025
e6c6c36
Merge remote-tracking branch 'upstream/main' into add-mcp-authenticat…
Nov 13, 2025
68b8f74
updated a comment in mcp.py
Nov 13, 2025
b090ed2
Merge branch 'main' into add-mcp-authentication-param
omaryashraf5 Nov 13, 2025
949756e
Merge branch 'main' into add-mcp-authentication-param
omaryashraf5 Nov 13, 2025
a9bcc0a
Merge branch 'main' into add-mcp-authentication-param
omaryashraf5 Nov 13, 2025
c2bf725
Merge remote-tracking branch 'upstream/main' into add-mcp-authenticat…
Nov 13, 2025
b5395fa
fix: Update import path after API reorganization
Nov 13, 2025
42d5547
test: Mark test_mcp_tools_in_inference as xfail due to deprecated reg…
Nov 14, 2025
fa8d3f9
test: Remove xfail marker from test_mcp_tools_in_inference
Nov 14, 2025
eddd29a
test: Skip MCP test when SDK lacks register_tool_group method
Nov 14, 2025
50cae44
fix: Update MCP test to use register() instead of register_tool_group()
Nov 14, 2025
8d30c40
test: Add timeout to test_conversation_error_handling to prevent CI hang
Nov 14, 2025
0391aaa
test: Remove skip marker from MCP authentication tests
Nov 14, 2025
a8c8cd8
test: Use responses_client and remove library client skips
Nov 14, 2025
f60d726
test: Fix error handling test to accept BadRequestError
Nov 14, 2025
e13014b
test: Add skip marker for MCP auth tests in replay mode
Nov 14, 2025
3d02349
test: Keep skip marker for MCP auth tests (recordings needed)
Nov 14, 2025
0b575f7
Add MCP authorization parameter support with test recordings
Nov 14, 2025
eb4b6fa
precommit
Nov 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions client-sdks/stainless/openapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2054,6 +2054,13 @@ paths:
required: false
schema:
$ref: '#/components/schemas/URL'
- name: authorization
in: query
description: >-
(Optional) OAuth access token for authenticating with the MCP server.
required: false
schema:
type: string
deprecated: false
/v1/toolgroups:
get:
Expand Down Expand Up @@ -7123,6 +7130,10 @@ components:
- type: object
description: >-
(Optional) HTTP headers to include when connecting to the server
authorization:
type: string
description: >-
(Optional) OAuth access token for authenticating with the MCP server
require_approval:
oneOf:
- type: string
Expand Down Expand Up @@ -9307,6 +9318,10 @@ components:
- type: object
description: >-
A dictionary of arguments to pass to the tool.
authorization:
type: string
description: >-
(Optional) OAuth access token for authenticating with the MCP server.
additionalProperties: false
required:
- tool_name
Expand Down
15 changes: 15 additions & 0 deletions docs/static/llama-stack-spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1878,6 +1878,13 @@ paths:
required: false
schema:
$ref: '#/components/schemas/URL'
- name: authorization
in: query
description: >-
(Optional) OAuth access token for authenticating with the MCP server.
required: false
schema:
type: string
deprecated: false
/v1/toolgroups:
get:
Expand Down Expand Up @@ -6182,6 +6189,10 @@ components:
- type: object
description: >-
(Optional) HTTP headers to include when connecting to the server
authorization:
type: string
description: >-
(Optional) OAuth access token for authenticating with the MCP server
require_approval:
oneOf:
- type: string
Expand Down Expand Up @@ -8366,6 +8377,10 @@ components:
- type: object
description: >-
A dictionary of arguments to pass to the tool.
authorization:
type: string
description: >-
(Optional) OAuth access token for authenticating with the MCP server.
additionalProperties: false
required:
- tool_name
Expand Down
15 changes: 15 additions & 0 deletions docs/static/stainless-llama-stack-spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2054,6 +2054,13 @@ paths:
required: false
schema:
$ref: '#/components/schemas/URL'
- name: authorization
in: query
description: >-
(Optional) OAuth access token for authenticating with the MCP server.
required: false
schema:
type: string
deprecated: false
/v1/toolgroups:
get:
Expand Down Expand Up @@ -7123,6 +7130,10 @@ components:
- type: object
description: >-
(Optional) HTTP headers to include when connecting to the server
authorization:
type: string
description: >-
(Optional) OAuth access token for authenticating with the MCP server
require_approval:
oneOf:
- type: string
Expand Down Expand Up @@ -9307,6 +9318,10 @@ components:
- type: object
description: >-
A dictionary of arguments to pass to the tool.
authorization:
type: string
description: >-
(Optional) OAuth access token for authenticating with the MCP server.
additionalProperties: false
required:
- tool_name
Expand Down
8 changes: 4 additions & 4 deletions src/llama_stack/core/routers/tool_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,16 @@ async def shutdown(self) -> None:
logger.debug("ToolRuntimeRouter.shutdown")
pass

async def invoke_tool(self, tool_name: str, kwargs: dict[str, Any]) -> Any:
async def invoke_tool(self, tool_name: str, kwargs: dict[str, Any], authorization: str | None = None) -> Any:
logger.debug(f"ToolRuntimeRouter.invoke_tool: {tool_name}")
provider = await self.routing_table.get_provider_impl(tool_name)
return await provider.invoke_tool(
tool_name=tool_name,
kwargs=kwargs,
authorization=authorization,
)

async def list_runtime_tools(
self, tool_group_id: str | None = None, mcp_endpoint: URL | None = None
self, tool_group_id: str | None = None, mcp_endpoint: URL | None = None, authorization: str | None = None
) -> ListToolDefsResponse:
logger.debug(f"ToolRuntimeRouter.list_runtime_tools: {tool_group_id}")
return await self.routing_table.list_tools(tool_group_id)
return await self.routing_table.list_tools(tool_group_id, authorization=authorization)
12 changes: 8 additions & 4 deletions src/llama_stack/core/routing_tables/toolgroups.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ async def get_provider_impl(self, routing_key: str, provider_id: str | None = No
routing_key = self.tool_to_toolgroup[routing_key]
return await super().get_provider_impl(routing_key, provider_id)

async def list_tools(self, toolgroup_id: str | None = None) -> ListToolDefsResponse:
async def list_tools(
self, toolgroup_id: str | None = None, authorization: str | None = None
) -> ListToolDefsResponse:
if toolgroup_id:
if group_id := parse_toolgroup_from_toolgroup_name_pair(toolgroup_id):
toolgroup_id = group_id
Expand All @@ -61,7 +63,7 @@ async def list_tools(self, toolgroup_id: str | None = None) -> ListToolDefsRespo
for toolgroup in toolgroups:
if toolgroup.identifier not in self.toolgroups_to_tools:
try:
await self._index_tools(toolgroup)
await self._index_tools(toolgroup, authorization=authorization)
except AuthenticationRequiredError:
# Send authentication errors back to the client so it knows
# that it needs to supply credentials for remote MCP servers.
Expand All @@ -76,9 +78,11 @@ async def list_tools(self, toolgroup_id: str | None = None) -> ListToolDefsRespo

return ListToolDefsResponse(data=all_tools)

async def _index_tools(self, toolgroup: ToolGroup):
async def _index_tools(self, toolgroup: ToolGroup, authorization: str | None = None):
provider_impl = await super().get_provider_impl(toolgroup.identifier, toolgroup.provider_id)
tooldefs_response = await provider_impl.list_runtime_tools(toolgroup.identifier, toolgroup.mcp_endpoint)
tooldefs_response = await provider_impl.list_runtime_tools(
toolgroup.identifier, toolgroup.mcp_endpoint, authorization=authorization
)

tooldefs = tooldefs_response.data
for t in tooldefs:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,19 @@ async def create_openai_response(
stream = bool(stream)
text = OpenAIResponseText(format=OpenAIResponseTextFormat(type="text")) if text is None else text

# Validate MCP tools: ensure Authorization header is not passed via headers dict
if tools:
from llama_stack_api.openai_responses import OpenAIResponseInputToolMCP

for tool in tools:
if isinstance(tool, OpenAIResponseInputToolMCP) and tool.headers:
for key in tool.headers.keys():
if key.lower() == "authorization":
raise ValueError(
"Authorization header cannot be passed via 'headers'. "
"Please use the 'authorization' parameter instead."
)

guardrail_ids = extract_guardrail_ids(guardrails) if guardrails else []

if conversation is not None:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1091,10 +1091,12 @@ async def _process_mcp_tool(
"server_url": mcp_tool.server_url,
"mcp_list_tools_id": list_id,
}
# List MCP tools with authorization from tool config
async with tracing.span("list_mcp_tools", attributes):
tool_defs = await list_mcp_tools(
endpoint=mcp_tool.server_url,
headers=mcp_tool.headers or {},
headers=mcp_tool.headers,
authorization=mcp_tool.authorization,
)

# Create the MCP list tools message
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -296,12 +296,14 @@ async def _execute_tool(
"server_url": mcp_tool.server_url,
"tool_name": function_name,
}
# Invoke MCP tool with authorization from tool config
async with tracing.span("invoke_mcp_tool", attributes):
result = await invoke_mcp_tool(
endpoint=mcp_tool.server_url,
headers=mcp_tool.headers or {},
tool_name=function_name,
kwargs=tool_kwargs,
headers=mcp_tool.headers,
authorization=mcp_tool.authorization,
)
elif function_name == "knowledge_search":
response_file_search_tool = (
Expand Down
9 changes: 7 additions & 2 deletions src/llama_stack/providers/inline/tool_runtime/rag/memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,10 @@ async def query(
)

async def list_runtime_tools(
self, tool_group_id: str | None = None, mcp_endpoint: URL | None = None
self,
tool_group_id: str | None = None,
mcp_endpoint: URL | None = None,
authorization: str | None = None,
) -> ListToolDefsResponse:
# Parameters are not listed since these methods are not yet invoked automatically
# by the LLM. The method is only implemented so things like /tools can list without
Expand Down Expand Up @@ -304,7 +307,9 @@ async def list_runtime_tools(
]
)

async def invoke_tool(self, tool_name: str, kwargs: dict[str, Any]) -> ToolInvocationResult:
async def invoke_tool(
self, tool_name: str, kwargs: dict[str, Any], authorization: str | None = None
) -> ToolInvocationResult:
vector_store_ids = kwargs.get("vector_store_ids", [])
query_config = kwargs.get("query_config")
if query_config:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,10 @@ def _get_api_key(self) -> str:
return provider_data.bing_search_api_key

async def list_runtime_tools(
self, tool_group_id: str | None = None, mcp_endpoint: URL | None = None
self,
tool_group_id: str | None = None,
mcp_endpoint: URL | None = None,
authorization: str | None = None,
) -> ListToolDefsResponse:
return ListToolDefsResponse(
data=[
Expand All @@ -70,7 +73,9 @@ async def list_runtime_tools(
]
)

async def invoke_tool(self, tool_name: str, kwargs: dict[str, Any]) -> ToolInvocationResult:
async def invoke_tool(
self, tool_name: str, kwargs: dict[str, Any], authorization: str | None = None
) -> ToolInvocationResult:
api_key = self._get_api_key()
headers = {
"Ocp-Apim-Subscription-Key": api_key,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@ def _get_api_key(self) -> str:
return provider_data.brave_search_api_key

async def list_runtime_tools(
self, tool_group_id: str | None = None, mcp_endpoint: URL | None = None
self,
tool_group_id: str | None = None,
mcp_endpoint: URL | None = None,
authorization: str | None = None,
) -> ListToolDefsResponse:
return ListToolDefsResponse(
data=[
Expand All @@ -70,7 +73,9 @@ async def list_runtime_tools(
]
)

async def invoke_tool(self, tool_name: str, kwargs: dict[str, Any]) -> ToolInvocationResult:
async def invoke_tool(
self, tool_name: str, kwargs: dict[str, Any], authorization: str | None = None
) -> ToolInvocationResult:
api_key = self._get_api_key()
url = "https://api.search.brave.com/res/v1/web/search"
headers = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,14 @@


class MCPProviderDataValidator(BaseModel):
# mcp_endpoint => dict of headers to send
mcp_headers: dict[str, dict[str, str]] | None = None
"""
Validator for MCP provider-specific data passed via request headers.

Phase 1: Support old header-based authentication for backward compatibility.
In Phase 2, this will be deprecated in favor of the authorization parameter.
"""

mcp_headers: dict[str, dict[str, str]] | None = None # Map of URI -> headers dict


class MCPProviderConfig(BaseModel):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,35 +39,87 @@ async def unregister_toolgroup(self, toolgroup_id: str) -> None:
return

async def list_runtime_tools(
self, tool_group_id: str | None = None, mcp_endpoint: URL | None = None
self,
tool_group_id: str | None = None,
mcp_endpoint: URL | None = None,
authorization: str | None = None,
) -> ListToolDefsResponse:
# this endpoint should be retrieved by getting the tool group right?
if mcp_endpoint is None:
raise ValueError("mcp_endpoint is required")
headers = await self.get_headers_from_request(mcp_endpoint.uri)
return await list_mcp_tools(mcp_endpoint.uri, headers)

async def invoke_tool(self, tool_name: str, kwargs: dict[str, Any]) -> ToolInvocationResult:
# Phase 1: Support both old header-based auth AND new authorization parameter
# Get headers and auth from provider data (old approach)
provider_headers, provider_auth = await self.get_headers_from_request(mcp_endpoint.uri)

# New authorization parameter takes precedence over provider data
final_authorization = authorization or provider_auth

return await list_mcp_tools(
endpoint=mcp_endpoint.uri, headers=provider_headers, authorization=final_authorization
)

async def invoke_tool(
self, tool_name: str, kwargs: dict[str, Any], authorization: str | None = None
) -> ToolInvocationResult:
tool = await self.tool_store.get_tool(tool_name)
if tool.metadata is None or tool.metadata.get("endpoint") is None:
raise ValueError(f"Tool {tool_name} does not have metadata")
endpoint = tool.metadata.get("endpoint")
if urlparse(endpoint).scheme not in ("http", "https"):
raise ValueError(f"Endpoint {endpoint} is not a valid HTTP(S) URL")

headers = await self.get_headers_from_request(endpoint)
return await invoke_mcp_tool(endpoint, headers, tool_name, kwargs)
# Phase 1: Support both old header-based auth AND new authorization parameter
# Get headers and auth from provider data (old approach)
provider_headers, provider_auth = await self.get_headers_from_request(endpoint)

# New authorization parameter takes precedence over provider data
final_authorization = authorization or provider_auth

return await invoke_mcp_tool(
endpoint=endpoint,
tool_name=tool_name,
kwargs=kwargs,
headers=provider_headers,
authorization=final_authorization,
)

async def get_headers_from_request(self, mcp_endpoint_uri: str) -> tuple[dict[str, str], str | None]:
"""
Extract headers and authorization from request provider data (Phase 1 backward compatibility).

Phase 1: Temporarily allows Authorization to be passed via mcp_headers for backward compatibility.
Phase 2: Will enforce that Authorization should use the dedicated authorization parameter instead.

Returns:
Tuple of (headers_dict, authorization_token)
- headers_dict: All headers except Authorization
- authorization_token: Token from Authorization header (with "Bearer " prefix removed), or None
"""

async def get_headers_from_request(self, mcp_endpoint_uri: str) -> dict[str, str]:
def canonicalize_uri(uri: str) -> str:
return f"{urlparse(uri).netloc or ''}/{urlparse(uri).path or ''}"

headers = {}
authorization = None

provider_data = self.get_request_provider_data()
if provider_data and provider_data.mcp_headers:
if provider_data and hasattr(provider_data, "mcp_headers") and provider_data.mcp_headers:
for uri, values in provider_data.mcp_headers.items():
if canonicalize_uri(uri) != canonicalize_uri(mcp_endpoint_uri):
continue
headers.update(values)
return headers

# Phase 1: Extract Authorization from mcp_headers for backward compatibility
# (Phase 2 will reject this and require the dedicated authorization parameter)
for key in values.keys():
if key.lower() == "authorization":
# Extract authorization token and strip "Bearer " prefix if present
auth_value = values[key]
if auth_value.startswith("Bearer "):
authorization = auth_value[7:] # Remove "Bearer " prefix
else:
authorization = auth_value
else:
headers[key] = values[key]

return headers, authorization
Loading
Loading