litellm - ✅(Solved) Fix [Bug]: MCP server settings page crashes with TypeError when cost_per_query in config.yaml uses scientific notation without a decimal point [1 pull requests, 1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
BerriAI/litellm#27097Fetched 2026-05-04 04:59:04
View on GitHub
Comments
0
Participants
1
Timeline
4
Reactions
0
Participants
Timeline (top)
cross-referenced ×2labeled ×1referenced ×1

Error Message

Uncaught TypeError: e.default_cost_per_query.toFixed is not a function

Fix Action

Fixed

PR fix notes

PR #27098: fix(mcp): coerce YAML 1.1 string costs to floats; defend UI .toFixed (#27097)

Description (problem / solution / changelog)

Closes #27097.

MCPServerCostInfo is a TypedDict with no runtime validation. When a user writes a perfectly reasonable YAML value like default_cost_per_query: 7e-05, PyYAML (YAML 1.1) correctly parses it as "7e-05" — a string. The string round-trips through _prepare_mcp_server_data → safe_dumps → mcp_info JSONB column unchanged, and on the way back the UI crashes:

Uncaught TypeError: e.default_cost_per_query.toFixed is not a function

The settings page becomes unopenable, and a user has no signal that their YAML is the problem.

What this PR does

Defense in depth — fix at the config-load boundary, plus a UI safety net for any data already in users' DBs:

  1. Backend coercion at ingest (mcp_server_manager.py:load_servers_from_config)

    • New helpers _coerce_optional_float and _coerce_mcp_cost_info_in_place.
    • On config load, every default_cost_per_query and every value in tool_name_to_cost_per_query is coerced to float. Strings like "7e-05", "1e-5", "7E-05" all become real floats.
    • Genuinely non-numeric values are dropped (not crashed on) with a clear warning naming the server and explaining the YAML 1.1 caveat: "write 7.0e-5 instead of 7e-5".
  2. UI safety net (types.tsx, mcp_server_cost_display.tsx, mcp_server_cost_config.tsx)

    • New toFiniteNumber(unknown): number | null helper in types.tsx.
    • Both the display and config components pipe values through toFiniteNumber before .toFixed(4) and before feeding <InputNumber value={...} />, so the settings page renders even if the server returns a stringified value (pre-existing bad rows in production DBs).

What this PR does not change

  • I did not convert MCPServerCostInfo from TypedDict to a Pydantic BaseModel as suggested in the issue. That would change MCPServerCostInfo() semantics and .get(...) access patterns used by MCPCostCalculator and other callers, expanding scope considerably. The boundary-coercion fix here is sufficient: bad data is normalized at ingest and any survivor is normalized again in the UI before formatting.

Tests

tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server_manager.py::TestCoerceMcpCostInfo adds:

  • _coerce_optional_float passthrough for ints/floats/None.
  • Scientific-notation string coercion ("7e-05", "1e-5", "7E-05").
  • Rejection of bool, lists, dicts, garbage strings, empty string.
  • The exact YAML 1.1 repro from the issue: safe_load("...: 7e-05") → string → after coerce → float.
  • tool_name_to_cost_per_query partial-coerce + drop-garbage behavior with caplog assertion.
  • No-op behavior when mcp_server_cost_info is missing or non-dict.

Manual verification

import yaml
from litellm.proxy._experimental.mcp_server.mcp_server_manager import (
    _coerce_mcp_cost_info_in_place,
)

mcp_info = yaml.safe_load("mcp_server_cost_info:\n  default_cost_per_query: 7e-05\n")
print(type(mcp_info["mcp_server_cost_info"]["default_cost_per_query"]))  # <class 'str'>
_coerce_mcp_cost_info_in_place(mcp_info, "google_maps")
print(type(mcp_info["mcp_server_cost_info"]["default_cost_per_query"]))  # <class 'float'>
print(mcp_info["mcp_server_cost_info"]["default_cost_per_query"])         # 7e-05

🤖 Generated with Claude Code

AI-assisted, human reviewed.

Changed files

  • litellm/proxy/_experimental/mcp_server/mcp_server_manager.py (modified, +140/-4)
  • tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server_manager.py (modified, +92/-0)
  • ui/litellm-dashboard/src/components/mcp_tools/mcp_server_cost_config.tsx (modified, +25/-24)
  • ui/litellm-dashboard/src/components/mcp_tools/mcp_server_cost_display.tsx (modified, +30/-33)
  • ui/litellm-dashboard/src/components/mcp_tools/types.tsx (modified, +14/-0)

Code Example

Uncaught TypeError: e.default_cost_per_query.toFixed is not a function

---

mcp_servers:
  google_maps:
    url: 'https://mapstools.googleapis.com/mcp'
    transport: 'http'
    mcp_info:
      mcp_server_cost_info:
        default_cost_per_query: 7e-05   # parsed as STRING by PyYAML (YAML 1.1)

---

>>> import yaml
>>> yaml.safe_load("x: 7e-05")    # {'x': '7e-05'}   <-- str, crashes UI
>>> yaml.safe_load("x: 7.0e-05")  # {'x': 7e-05}     <-- float, works
>>> yaml.safe_load("x: 1e-5")     # {'x': '1e-5'}    <-- str, crashes UI
>>> yaml.safe_load("x: 0.00007")  # {'x': 7e-05}     <-- float, works

---

Uncaught TypeError: e.default_cost_per_query.toFixed is not a function
    at mcp_server_cost_display.tsx:38
RAW_BUFFERClick to expand / collapse

What happened?

Opening an MCP server's settings page in the LiteLLM UI throws an uncaught TypeError and the page fails to render:

Uncaught TypeError: e.default_cost_per_query.toFixed is not a function

The trigger is a reasonable-looking config.yaml value — default_cost_per_query: 7e-05 — that PyYAML (correctly) parses as a string, not a float. LiteLLM never validates or coerces the value on the way in, persists it through the stack as a string, and the UI crashes when it tries to format it.

Minimal repro config.yaml:

mcp_servers:
  google_maps:
    url: 'https://mapstools.googleapis.com/mcp'
    transport: 'http'
    mcp_info:
      mcp_server_cost_info:
        default_cost_per_query: 7e-05   # parsed as STRING by PyYAML (YAML 1.1)

Independent confirmation that the YAML value is a string, not a float:

>>> import yaml
>>> yaml.safe_load("x: 7e-05")    # {'x': '7e-05'}   <-- str, crashes UI
>>> yaml.safe_load("x: 7.0e-05")  # {'x': 7e-05}     <-- float, works
>>> yaml.safe_load("x: 1e-5")     # {'x': '1e-5'}    <-- str, crashes UI
>>> yaml.safe_load("x: 0.00007")  # {'x': 7e-05}     <-- float, works

Why PyYAML is not the bug

PyYAML is a YAML 1.1 parser, and YAML 1.1's float schema explicitly requires a decimal point in the mantissa — so 7e-5, 7e-05, 1e-5, 7E-05 all correctly resolve to strings under that spec. YAML 1.2 relaxed this, but PyYAML has never adopted 1.2 and that is a documented, deliberate upstream position, not a defect. The YAML parser is behaving as specified.

Why this is a LiteLLM bug

LiteLLM has four boundaries at which it could catch a non-numeric value being used where a float is required, and enforces none of them:

  1. Type declaration. MCPServerCostInfo is a TypedDict (litellm/types/mcp.py:121) — zero runtime validation. The annotation default_cost_per_query: Optional[float] is documentation only.
  2. Config loader. MCPServerManager.load_servers_from_config (litellm/proxy/_experimental/mcp_server/mcp_server_manager.py:204) copies mcp_info through to the in-memory server with no shape checking on cost fields.
  3. Persistence. _prepare_mcp_server_data (litellm/proxy/_experimental/mcp_server/db.py:30) calls safe_dumps(data.mcp_info) on whatever it's handed and writes it to the mcp_info JSONB column verbatim. A stringified "7e-05" round-trips through the DB unchanged.
  4. UI render. ui/litellm-dashboard/src/components/mcp_tools/mcp_server_cost_display.tsx (lines 38 and 67) and mcp_server_cost_config.tsx (lines 140 and 149) call .toFixed(4) directly on default_cost_per_query, and line 52 does the same for each value in tool_name_to_cost_per_query. The TypeScript type is number | null, but that is a compile-time promise — at runtime the value is whatever the server returned.

Any single one of those being enforced would have turned this into a clear config error or a non-issue. Since none are, a user who writes 7e-05 in their YAML ends up with an unopenable settings page and no signal about what's wrong.

Expected behavior

Either (a) the config loader rejects a non-numeric default_cost_per_query at startup with a clear message pointing at the YAML line and suggesting the fix, or (b) the value is coerced to float at ingest and the UI can trust it. The settings page must not be rendered unopenable by a round-trip-valid config value.

Suggested fix (defense in depth, backend-first)

  1. Primary — promote MCPServerCostInfo from TypedDict to a Pydantic BaseModel in litellm/types/mcp.py, with default_cost_per_query: Optional[float] and tool_name_to_cost_per_query: Optional[Dict[str, float]]. Pydantic will coerce stringified floats ("7e-05"7e-05) automatically and reject genuinely non-numeric input. This covers both config-file and API-call ingest paths.
  2. Belt-and-suspenders in config load — in load_servers_from_config, validate/coerce mcp_info.mcp_server_cost_info via that model and emit a clear startup error pointing at the offending server name if coercion fails. The YAML scientific-notation footgun deserves an explicit hint (e.g. "YAML 1.1 requires a decimal point in scientific notation; write 7.0e-5 instead of 7e-5").
  3. UI, defensive — in mcp_server_cost_display.tsx and mcp_server_cost_config.tsx, use a toFiniteNumber(v: unknown): number | null helper to coerce before .toFixed(4), applied to default_cost_per_query and each entry in tool_name_to_cost_per_query. This keeps the settings page openable for users with pre-existing bad data in the DB.
  4. Docs — a one-liner in the MCP cost-tracking docs mentioning the YAML 1.1 scientific-notation rule would save a lot of future confusion.

Steps to Reproduce

  1. Put the minimal repro config above in config.yaml and start the proxy.
  2. Open the UI → MCP Servers → click the google_maps server.
  3. Page does not render; browser console shows Uncaught TypeError: e.default_cost_per_query.toFixed is not a function.

Relevant log output

Uncaught TypeError: e.default_cost_per_query.toFixed is not a function
    at mcp_server_cost_display.tsx:38

What part of LiteLLM is this about?

Proxy (config parsing / type validation) and UI Dashboard

What LiteLLM version are you on?

v1.83.14

Also verified present at tip of the following upstream branches (files unchanged since PR #12526 + prettier reformat; mcp_server_cost_display.tsx MD5 d5bd7800500a31e22ea75c785f3cf0c6, mcp_server_cost_config.tsx MD5 e1ecdaf379a9ef6560d6ef6415a3718d on all of them):

  • main @ 934ecdca78
  • litellm_internal_staging @ c011a7e3ba
  • litellm_oss_staging @ 50ef2d51a2
  • litellm_thursday_release_staging @ 6956c76d02
  • litellm_oss_staging_06_06_2026 @ 7a9a9f0c79

extent analysis

TL;DR

The most likely fix is to promote MCPServerCostInfo to a Pydantic BaseModel and validate/coerce default_cost_per_query to a float.

Guidance

  1. Update MCPServerCostInfo to a Pydantic BaseModel: Change the type declaration in litellm/types/mcp.py to use Pydantic's BaseModel for runtime validation and coercion of default_cost_per_query to a float.
  2. Validate/coerce mcp_info.mcp_server_cost_info in load_servers_from_config: Add a check in load_servers_from_config to ensure mcp_info.mcp_server_cost_info conforms to the updated MCPServerCostInfo model, emitting a clear error if coercion fails.
  3. Use a defensive coercion helper in the UI: Implement a toFiniteNumber helper in mcp_server_cost_display.tsx and mcp_server_cost_config.tsx to safely coerce default_cost_per_query and tool_name_to_cost_per_query values before calling .toFixed(4).
  4. Document the YAML 1.1 scientific-notation rule: Add a note to the MCP cost-tracking documentation about the requirement for a decimal point in scientific notation in YAML 1.1.

Example

from pydantic import BaseModel

class MCPServerCostInfo(BaseModel):
    default_cost_per_query: Optional[float]
    tool_name_to_cost_per_query: Optional[Dict[str, float]]

Notes

This solution focuses on updating the backend to enforce type validation and coercion. The UI changes are defensive measures to handle potential existing bad data.

Recommendation

Apply the suggested fix by promoting MCPServerCostInfo to a Pydantic BaseModel and implementing the other steps outlined, as this addresses the root cause of the issue

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

FAQ

Expected behavior

Either (a) the config loader rejects a non-numeric default_cost_per_query at startup with a clear message pointing at the YAML line and suggesting the fix, or (b) the value is coerced to float at ingest and the UI can trust it. The settings page must not be rendered unopenable by a round-trip-valid config value.

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

litellm - ✅(Solved) Fix [Bug]: MCP server settings page crashes with TypeError when cost_per_query in config.yaml uses scientific notation without a decimal point [1 pull requests, 1 participants]