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:
Cursor Agent 2026-05-19 09:12:03 +00:00
parent 19b68721c0
commit bd9a6e4872
No known key found for this signature in database
2 changed files with 70 additions and 5 deletions

View file

@ -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:

View file

@ -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">