fix(mcp): roll up MCP tool spend to user counters and usage UI - #31576
Conversation
Direct REST MCP tool calls now fire success logging so spend_logs and user/team rollups include configured mcp_server_cost_info charges. Co-authored-by: Cursor <cursoragent@cursor.com>
Greptile SummaryThis PR wires MCP tool-call spend logging end-to-end across three entry paths (direct REST, Playground, streaming chat) and adds per-session MCP cost aggregates to the spend-logs UI. The changes are self-contained to the MCP subsystem and its cost callback.
Confidence Score: 5/5Safe to merge. All changes are isolated to MCP spend-logging paths; failures are wrapped in try/except or in The core logic — firing success logging on the REST path, enriching metadata before cost rollup, draining the inner stream, and aggregating MCP spend in the UI — is correct and well-tested. Error handling is consistently defensive: logging failures warn and continue, the drain swallows non- No files require special attention.
|
| Filename | Overview |
|---|---|
| litellm/proxy/_experimental/mcp_server/rest_endpoints.py | Adds _safe_fire_mcp_success_logging helper and fires it after both the virtual-tool and standard execute paths; CancelledError is correctly re-raised while other errors warn-and-continue. |
| litellm/proxy/_experimental/mcp_server/server.py | Extracts _fire_mcp_success_logging into a standalone helper; call_mcp_tool now calls it via the helper instead of inlining; list_mcp_tools serialises tools with model_dump(mode="json") before logging and accepts request_tags. |
| litellm/proxy/hooks/proxy_track_cost_callback.py | Adds conditional _enrich_failure_metadata_with_key_info call in the success callback when user_api_key_user_id is absent, enabling user/team rollup for MCP REST calls; adds _write_spend_metadata_to_kwargs to propagate enriched fields back to kwargs. |
| litellm/proxy/spend_tracking/spend_management_endpoints.py | New query_raw in _build_ui_spend_logs_response aggregates MCP sub-call counts and spend per session; scoped by api_key collected from the authorized page rows; errors are swallowed at debug level. |
| litellm/responses/mcp/chat_completions_handler.py | Adds _drain_inner_stream to exhaust the CustomStreamWrapper after the final chunk so spend handlers fire; drain exceptions are caught and logged, preserving the final chunk; request_tags are threaded through the streaming iterator. |
| litellm/responses/mcp/litellm_proxy_mcp_handler.py | Adds _get_parent_request_tags static method reusing StandardLoggingPayloadSetup._get_request_tags; upgrades _execute_tool_calls to use LiteLLMProxyRequestSetup.add_user_api_key_auth_to_request_metadata for fuller metadata; threads request_tags through all call sites. |
| litellm/responses/mcp/mcp_streaming_iterator.py | Minimal change: propagates request_tags to _execute_tool_calls in MCPEnhancedStreamingIterator. |
| litellm/responses/main.py | Threads request_tags from _get_parent_request_tags to all three MCP tool-processing call sites in aresponses_api_with_mcp. |
Reviews (8): Last reviewed commit: "fix: propagate MCP logging cancellation" | Re-trigger Greptile
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
…mport order - Only call _enrich_failure_metadata_with_key_info when user_api_key_user_id is absent, avoiding a cache/DB lookup on every normal LLM request. - Move LiteLLMProxyRequestSetup import to correct alphabetical position (I001). Co-authored-by: Cursor <cursoragent@cursor.com>
PR overviewAll previously flagged issues have been addressed. No open security concerns remain on this pull request. Security reviewNo open security issues remain on this pull request. Fixed/addressed: 1 · PR risk: 0/10 |
…t disclosure Add api_key = ANY($2) to the MCP session aggregate query so it is bounded by the same ownership already applied to the main page query. Co-authored-by: Cursor <cursoragent@cursor.com>
Replace the 8 new UP006 violations introduced by the mcp-tags changes: - Optional[List[str]] → Optional[list[str]] for request_tags params - List[str] return type → list[str] in _get_parent_request_tags - Dict[str, Dict[...]] → dict[str, dict[...]] for mcp_spend_map annotation Co-authored-by: Cursor <cursoragent@cursor.com>
|
Generated by Claude Code |
…w MCP spend enrichment except to PrismaError
|
Generated by Claude Code |
|
Generated by Claude Code |
|
bugbot run Generated by Claude Code |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 4 potential issues.
Autofix Details
Bugbot Autofix prepared fixes for all 4 issues found in the latest run.
- ✅ Fixed: REST logging errors fail requests
- Wrapped REST MCP success logging in a non-throwing helper so completed tool calls still return if logging fails.
- ✅ Fixed: Stream drain skipped on exhaustion
- Added inner stream draining on the StopAsyncIteration path after processing the final collected MCP chat chunk.
- ✅ Fixed: Parent tags use wrong params
- Changed parent tag extraction to read nested litellm_params and proxy_server_request while preserving the top-level fallback.
- ✅ Fixed: Virtual REST path skips logging
- The virtual REST mcp_tool_call branch now fires the same isolated success logging path before returning the tool result.
You can send follow-ups to the cloud agent here.
|
|
|
bugbot run |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Safe logging swallows cancellation
- Cancellation from MCP success logging is now re-raised before ordinary logging failures are swallowed, with a regression test covering the propagation.
You can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit 2622db2. Configure here.
|
Generated by Claude Code |
fabe5c2
into
litellm_internal_staging
…AI#31576) * fix(mcp): roll up MCP tool spend to user counters and usage UI Direct REST MCP tool calls now fire success logging so spend_logs and user/team rollups include configured mcp_server_cost_info charges. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(mcp): gate key-info enrichment to requests missing user_id; fix import order - Only call _enrich_failure_metadata_with_key_info when user_api_key_user_id is absent, avoiding a cache/DB lookup on every normal LLM request. - Move LiteLLMProxyRequestSetup import to correct alphabetical position (I001). Co-authored-by: Cursor <cursoragent@cursor.com> * fix(mcp): scope MCP spend aggregate by api_key to prevent cross-tenant disclosure Add api_key = ANY($2) to the MCP session aggregate query so it is bounded by the same ownership already applied to the main page query. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix spend logs for call and list mcp tools * Add tags in mcp logging * Fix ruff * fix(lint): replace List/Dict with list/dict in new annotations (UP006) Replace the 8 new UP006 violations introduced by the mcp-tags changes: - Optional[List[str]] → Optional[list[str]] for request_tags params - List[str] return type → list[str] in _get_parent_request_tags - Dict[str, Dict[...]] → dict[str, dict[...]] for mcp_spend_map annotation Co-authored-by: Cursor <cursoragent@cursor.com> * fix(lint): keep call_tool_rest_api within complexity budget and narrow MCP spend enrichment except to PrismaError * fix(mcp): keep final streaming chunk when draining inner stream fails * fix: handle MCP logging edge cases * fix: propagate MCP logging cancellation --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
## What - Add `orjson>=3.11.6,<4.0` as a direct SDK runtime dependency for tool-enabled rollout calls across local and remote backends. - Regenerate `uv.lock`, resolving `orjson==3.11.9`. ## Why Tool-enabled rollout calls can fail on any backend that installs the base SDK dependency set without separately installing `orjson`. This affected both `LocalBackend` and Harbor/Daytona sandbox execution, failing with `litellm.APIConnectionError: OpenAIException - No module named 'orjson'`. LiteLLM imports `LiteLLM_Proxy_MCP_Handler` whenever a completion contains any `tools` ([v1.92.0 source](https://github.com/BerriAI/litellm/blob/v1.92.0/litellm/main.py#L4865-L4876)). [LiteLLM PR #31576](BerriAI/litellm#31576) added a top-level `LiteLLMProxyRequestSetup` import to that handler ([commit](BerriAI/litellm@fabe5c2)), which imports `http_parsing_utils` and therefore `orjson` at module load time. LiteLLM still declares `orjson` only under its `proxy` optional extra ([pyproject.toml](https://github.com/BerriAI/litellm/blob/v1.92.0/pyproject.toml#L42-L54)), so a base LiteLLM installation can reach this runtime path without having `orjson` installed. Declaring `orjson` directly with LiteLLM's existing version bound makes the SDK dependency set complete for tool-enabled rollout calls across local and remote backends. Adding `orjson` to the rollout image also unblocked the failed remote evaluation and artifact collection, confirming that the dependency gap—not artifact persistence—caused that incident. ## How to Test - `uv lock --check` - `uv run --locked --extra dev ruff check .` - `uv run --locked --extra dev ruff format --check .` - `uv run --locked --extra dev pyright osmosis_ai/` - `uv run --locked --extra dev pytest -q` - `uv build --wheel --out-dir /tmp/osmosis-sdk-orjson-dist` - `unzip -p /tmp/osmosis-sdk-orjson-dist/*.whl '*/METADATA' | rg '^Requires-Dist: orjson'` ## Checklist - [x] PR title follows `[module] type: description` format - [x] Appropriate labels added (`bug`, `rollout`, `dependencies`) - [x] `ruff check .` and `ruff format --check .` pass - [x] `pyright osmosis_ai/` passes - [x] `pytest` passes (`1607 passed`) - [x] Public API changes are documented (no public API changes) - [x] No secrets or credentials included

Summary
/mcp-rest/tools/callso direct REST MCP tool calls writespend_logsand update key/user/team spend counters.Additional MCP spend-log fixes
MCPStreamingIteratorreturned on the final chunk without draining the innerCustomStreamWrapper, so end-of-stream success handlers never ran. Drain the inner stream after processing tool calls solist_mcp_tools,call_mcp_tool, and the parent completion all write toLiteLLM_SpendLogs.list_mcp_toolslogging crash:async_success_handlerreceived rawMCPToolobjects and failed withObject of type Tool is not JSON serializable. Serialize tools withmodel_dump(mode="json")before logging.request_tagson MCP sub-calls:list_mcp_toolsandcall_mcp_toolspend logs had emptyrequest_tagsbecause tag extraction only checkedmetadata.tags. ReuseStandardLoggingPayloadSetup._get_request_tagsso parent tags (including User-Agent-derived tags fromproxy_server_request) propagate to MCP sub-call logs.Test plan
list_mcp_tools, andcall_mcp_toollist_mcp_toolslogs succeed without JSON serialization errorsrequest_tagsNote
Medium Risk
Touches spend logging, cost callbacks, and a raw SQL enrichment query scoped by api_key; failures are mostly non-blocking, but incorrect rollup metadata could mis-attribute MCP spend.
Overview
This PR tightens MCP spend logging and rollups so direct REST calls, gateway sub-calls, and streaming completions all land in
LiteLLM_SpendLogsand user/team counters./mcp-rest/tools/callnow runs shared_fire_mcp_success_loggingafter tool execution (including the virtual tool-search path), wrapped in_safe_fire_mcp_success_loggingso logging failures only warn and never fail the request.call_mcp_tooluses the same helper instead of inlined logging.Cost tracking enriches metadata with user/team/org when only
user_api_keyis present (typical MCP REST), via a conditional lookup in_PROXY_track_cost_callbackand_write_spend_metadata_to_kwargs. MCPcall_mcp_toollogging usesLiteLLMProxyRequestSetup.add_user_api_key_auth_to_request_metadatafor fuller metadata.Parent request tags propagate to
list_mcp_toolsandcall_mcp_toolthrough_get_parent_request_tags(same logic as standard logging, including User-Agent).list_mcp_toolssuccess logging serializes tools withmodel_dump(mode="json")to avoid JSON errors.Streaming MCP chat completions drain the inner
CustomStreamWrapperafter the final chunk so end-of-stream spend handlers run; drain errors are swallowed so the final chunk still reaches the client.The spend logs UI adds per-session
mcp_tool_call_countandmcp_tool_call_spend, scoped byapi_keyto avoid cross-tenant leakage on collidingsession_ids.Reviewed by Cursor Bugbot for commit 2622db2. Bugbot is set up for automated code reviews on this repo. Configure here.