mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-27 01:22:18 +00:00
fix(mcp): inject user_fields headers on managed OpenAPI dispatch and clear false UI Ready badge
- mcp_server_manager.py: resolve admin-declared per-user field values and forward them as upstream headers through the managed _call_openapi_tool_handler path via the openapi generator's _request_user_field_headers ContextVar. The local-registry dispatch in server.execute_mcp_tool already injects these; the managed fallback was silently dropping them, so a server with spec_path + user_fields would pass _enforce_user_fields but still fail upstream with no credentials. - mcp_server_columns.tsx: distinguish null/undefined (annotation never ran — e.g. empty user_id, no DB connection) from [] (verified complete) when rendering the user-fields credential cell. Previously a server whose missing_user_field_keys was never populated rendered a green 'Ready' badge while enforcement at tool-call time would still return 401, lying to the user. Now show a neutral 'Not verified' badge when the annotation was skipped. Co-authored-by: Yassin Kortam <yassin@berri.ai>
This commit is contained in:
parent
19b68721c0
commit
bd9a6e4872
2 changed files with 70 additions and 5 deletions
|
|
@ -2482,6 +2482,7 @@ class MCPServerManager:
|
|||
server: MCPServer,
|
||||
tool_name: str,
|
||||
arguments: Dict[str, Any],
|
||||
user_field_headers: Optional[Dict[str, str]] = None,
|
||||
) -> CallToolResult:
|
||||
"""
|
||||
Call an OpenAPI tool handler directly.
|
||||
|
|
@ -2493,12 +2494,20 @@ class MCPServerManager:
|
|||
Args:
|
||||
tool_name: The full tool name (with prefix) to call
|
||||
arguments: Tool arguments to pass to the handler
|
||||
user_field_headers: Optional admin-declared per-user header values
|
||||
resolved from ``MCPServer.user_fields`` for the calling user.
|
||||
Forwarded via the openapi generator's request ContextVar so
|
||||
the closure-baked handler picks them up — mirrors the
|
||||
local-registry dispatch path in ``server.execute_mcp_tool``.
|
||||
|
||||
Returns:
|
||||
CallToolResult with the response from the API
|
||||
"""
|
||||
from mcp.types import TextContent
|
||||
|
||||
from litellm.proxy._experimental.mcp_server.openapi_to_mcp_generator import (
|
||||
_request_user_field_headers,
|
||||
)
|
||||
from litellm.proxy._experimental.mcp_server.tool_registry import (
|
||||
global_mcp_tool_registry,
|
||||
)
|
||||
|
|
@ -2514,6 +2523,11 @@ class MCPServerManager:
|
|||
isError=True,
|
||||
)
|
||||
|
||||
_user_field_token = (
|
||||
_request_user_field_headers.set(user_field_headers)
|
||||
if user_field_headers
|
||||
else None
|
||||
)
|
||||
try:
|
||||
# Call the tool handler with the arguments
|
||||
# The handler is an async function that makes the HTTP request
|
||||
|
|
@ -2534,6 +2548,9 @@ class MCPServerManager:
|
|||
content=[TextContent(type="text", text=error_msg)],
|
||||
isError=True,
|
||||
)
|
||||
finally:
|
||||
if _user_field_token is not None:
|
||||
_request_user_field_headers.reset(_user_field_token)
|
||||
|
||||
async def pre_call_tool_check(
|
||||
self,
|
||||
|
|
@ -3006,9 +3023,34 @@ class MCPServerManager:
|
|||
"transport to enable hook header injection.",
|
||||
server_name,
|
||||
)
|
||||
# User-fields: resolve the calling user's stored values and forward
|
||||
# them as upstream headers so admin-declared required fields reach
|
||||
# the OpenAPI handler. Mirrors the local-registry dispatch path in
|
||||
# ``server.execute_mcp_tool`` — without this, _enforce_user_fields
|
||||
# would gate the call on the values being present but the values
|
||||
# would be silently dropped before dispatch.
|
||||
user_field_headers: Optional[Dict[str, str]] = None
|
||||
from litellm.proxy._experimental.mcp_server.user_fields import (
|
||||
resolve_user_field_headers,
|
||||
)
|
||||
|
||||
stored_user_field_values = await self._resolve_user_field_values(
|
||||
mcp_server, user_api_key_auth
|
||||
)
|
||||
if stored_user_field_values:
|
||||
resolved = resolve_user_field_headers(
|
||||
mcp_server, stored_user_field_values
|
||||
)
|
||||
if resolved:
|
||||
user_field_headers = resolved
|
||||
tasks.append(
|
||||
asyncio.create_task(
|
||||
self._call_openapi_tool_handler(mcp_server, name, arguments)
|
||||
self._call_openapi_tool_handler(
|
||||
mcp_server,
|
||||
name,
|
||||
arguments,
|
||||
user_field_headers=user_field_headers,
|
||||
)
|
||||
)
|
||||
)
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -273,10 +273,13 @@ export const mcpServerColumns = (
|
|||
const hasUserFields = declaredUserFields.length > 0;
|
||||
if (hasUserFields) {
|
||||
// missing_user_field_keys is populated by the proxy for the calling
|
||||
// user on the list endpoint. Falling back to an empty array assumes
|
||||
// "nothing missing" so old API responses don't show a false alarm.
|
||||
const missing = server.missing_user_field_keys ?? [];
|
||||
if (missing.length > 0) {
|
||||
// user on the list endpoint. Distinguish `null`/`undefined`
|
||||
// (annotation never ran — e.g. no user_id, no DB) from `[]`
|
||||
// (annotation ran and all required fields are satisfied): rendering
|
||||
// a green "Ready" badge for the unannotated case would lie to the
|
||||
// user, since the actual tool call would still 401 on missing fields.
|
||||
const missing = server.missing_user_field_keys;
|
||||
if (missing && missing.length > 0) {
|
||||
return onUserFieldsConnect ? (
|
||||
<button
|
||||
className="inline-flex items-center gap-1.5 text-xs font-semibold px-2.5 py-1 rounded-md bg-red-50 text-red-700 border border-red-200 hover:bg-red-100 transition-colors"
|
||||
|
|
@ -293,6 +296,26 @@ export const mcpServerColumns = (
|
|||
</span>
|
||||
);
|
||||
}
|
||||
if (missing == null) {
|
||||
return onUserFieldsConnect ? (
|
||||
<button
|
||||
className="inline-flex items-center gap-1.5 text-xs font-medium px-2 py-0.5 rounded-md bg-gray-50 text-gray-600 border border-gray-200 hover:bg-gray-100 transition-colors"
|
||||
onClick={() => onUserFieldsConnect(server)}
|
||||
title="Per-user field status is unavailable. Open to configure."
|
||||
>
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-gray-400" />
|
||||
Not verified
|
||||
</button>
|
||||
) : (
|
||||
<span
|
||||
className="inline-flex items-center gap-1.5 text-xs font-medium px-2 py-0.5 rounded-md bg-gray-50 text-gray-600 border border-gray-200"
|
||||
title="Per-user field status is unavailable."
|
||||
>
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-gray-400" />
|
||||
Not verified
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="inline-flex items-center gap-1 text-xs font-medium px-2 py-0.5 rounded-full bg-green-50 text-green-700 border border-green-200">
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue