litellm - ✅(Solved) Fix [Bug]: MCP server settings view crashes with "default_cost_per_query.toFixed is not a function" [1 pull requests, 1 comments, 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#27095Fetched 2026-05-04 04:59:06
View on GitHub
Comments
1
Participants
1
Timeline
5
Reactions
0
Participants
Timeline (top)
labeled ×2closed ×1commented ×1cross-referenced ×1

Error Message

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

Root Cause

Why it happens (root cause):

  • MCPServerCostInfo is declared as a TypedDict in litellm/types/mcp.py (no runtime validation), and the values are serialized into the mcp_info JSONB column via safe_dumps and re-read as-is. Whatever type goes in comes out.
  • The antd InputNumber in mcp_server_cost_config.tsx is typed locally as (defaultCost: number | null), but antd's own signature is (value: number | string | null) => void — under certain inputs/precisions it can emit a string, which the UI then persists verbatim.
  • Even if the UI always wrote numbers, values coming from external sources (YAML config import, direct API call, hand-populated DB rows) can land as strings without being rejected anywhere.
  • The render code does not defensively coerce before calling .toFixed, so one malformed value blocks the entire settings page.

Fix Action

Fixed

PR fix notes

PR #27096: fix(ui): coerce MCP cost values defensively before .toFixed (closes #27095)

Description (problem / solution / changelog)

Summary

The MCP server settings view crashed with Uncaught TypeError: e.default_cost_per_query.toFixed is not a function whenever mcp_info.mcp_server_cost_info.default_cost_per_query (or any per-tool cost) landed in the JSONB column as a string instead of a number. Reported with full root-cause in #27095.

This PR coerces cost values defensively at every .toFixed(...) site in the two affected components, so one malformed value no longer blocks the whole settings page.

Why the crash happens

  • MCPServerCostInfo is a TypedDict (litellm/types/mcp.py) — no runtime validation, so the JSONB column round-trips whatever is written, including strings.
  • antd's InputNumber onChange real signature is (number | string | null) => void; the local handler in mcp_server_cost_config.tsx types it as (number | null) and writes the raw value, so the UI itself can persist a string.
  • Other write paths (YAML/JSON config import, direct API call, hand-edited DB rows) can also land non-numeric values.
  • Display components called .toFixed(4) directly, so one malformed cost crashed the whole page.

Fix

Add a small shared toFiniteNumber helper in src/utils/numberUtils.ts (per CLAUDE.md: "Shared utility functions belong in src/utils/"), and use it before every .toFixed(...) callsite in the two components — five total:

  • mcp_server_cost_display.tsx — default cost render (line 38), per-tool cost render (line 52), default cost in summary (line 67)
  • mcp_server_cost_config.tsx — default cost in summary (line 140), per-tool cost in summary (line 149)

Non-numeric / non-finite inputs (including stringified gibberish, NaN, Infinity, null, undefined, objects) coerce to null; the corresponding row is skipped instead of crashing.

Tests

8 new vitest cases in numberUtils.test.ts cover finite/non-finite numbers, parseable/unparseable strings, whitespace-only strings, null/undefined, and non-string/non-number values. The last case is an explicit regression test for #27095:

it("regression for litellm#27095: stringified cost survives without crashing", () => {
  const stringifiedCost = "0.005";
  const coerced = toFiniteNumber(stringifiedCost);
  expect(coerced).not.toBeNull();
  expect(coerced!.toFixed(4)).toBe("0.0050");
});
$ npx vitest run src/utils/numberUtils.test.ts
 ✓ src/utils/numberUtils.test.ts (8 tests) 3ms
 Test Files  1 passed (1)
      Tests  8 passed (8)

npx tsc --noEmit reports no new errors in the touched files. (The repo has pre-existing tsc errors in unrelated test files like returnUrlUtils.test.ts, roles.test.ts, top_key_view.test.tsx, useLogFilterLogic.min.test.tsx — those are out of scope here.)

Out of scope (intentional)

  • Backend Pydantic validation of MCPServerCostInfo. Reporter mentions converting the TypedDict to a Pydantic model so the proxy rejects non-numeric writes at the boundary. That's a more complete fix but a separate concern with a larger surface; happy to send a follow-up PR if you'd like.
  • antd InputNumber typing in mcp_server_cost_config.tsx. The local handleDefaultCostChange(defaultCost: number | null) signature is too narrow for antd's actual (number | string | null) emission. Tightening that properly means changing the function signatures and ripples through callers — defensive coercion at the render boundary is the smaller, safer fix for the symptom users are seeing today.
  • Migration off @tremor/react. CLAUDE.md notes the project is migrating to antd, but these files already use tremor and the bug fix doesn't require touching imports. Out of scope.

PR template checklist

  • At least 1 test added (ui/litellm-dashboard/src/utils/numberUtils.test.ts, 8 cases)
  • Touched files type-check cleanly with tsc --noEmit
  • No raw SQL / no Prisma changes / no schema impact
  • No new @tremor/react imports introduced
  • Followed CLAUDE.md guidance on shared util placement (src/utils/numberUtils.ts)

Reported and root-caused by @marty-sullivan in #27095.

Changed files

  • ui/litellm-dashboard/src/components/mcp_tools/mcp_server_cost_config.tsx (modified, +25/-21)
  • ui/litellm-dashboard/src/components/mcp_tools/mcp_server_cost_display.tsx (modified, +28/-34)
  • ui/litellm-dashboard/src/utils/numberUtils.test.ts (added, +59/-0)
  • ui/litellm-dashboard/src/utils/numberUtils.ts (added, +33/-0)

Code Example

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

Check for existing issues

  • I have searched the existing issues and checked that my issue is not a duplicate.

What happened?

What happened?

Opening the settings view for an MCP server 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 crash originates in ui/litellm-dashboard/src/components/mcp_tools/mcp_server_cost_display.tsx (and mcp_server_cost_config.tsx), which unconditionally calls .toFixed(4) on
costConfig.default_cost_per_query (and on every entry of tool_name_to_cost_per_query). The TypeScript type annotation is number | null, but the value retrieved from the server is a string for at
least some servers, so .toFixed does not exist on it.

Why it happens (root cause):

  • MCPServerCostInfo is declared as a TypedDict in litellm/types/mcp.py (no runtime validation), and the values are serialized into the mcp_info JSONB column via safe_dumps and re-read as-is. Whatever type goes in comes out.
  • The antd InputNumber in mcp_server_cost_config.tsx is typed locally as (defaultCost: number | null), but antd's own signature is (value: number | string | null) => void — under certain inputs/precisions it can emit a string, which the UI then persists verbatim.
  • Even if the UI always wrote numbers, values coming from external sources (YAML config import, direct API call, hand-populated DB rows) can land as strings without being rejected anywhere.
  • The render code does not defensively coerce before calling .toFixed, so one malformed value blocks the entire settings page.

Expected behavior:

The MCP server settings page renders regardless of whether cost values are stored as numbers or stringified numbers. Ideally, cost values are validated/normalized to float on the way in (backend
Pydantic model or UI coercion), and the display layer formats defensively.

Suggested fix (UI, minimally defensive):

Coerce before formatting, e.g. a small helper in mcp_server_cost_display.tsx / mcp_server_cost_config.tsx:

const toFiniteNumber = (v: unknown): number | null => { if (typeof v === "number") return Number.isFinite(v) ? v : null;
if (typeof v === "string" && v.trim() !== "") {
const n = Number(v);
return Number.isFinite(n) ? n : null;
}
return null;
};

…then gate hasDefaultCost and the .toFixed(4) calls on the coerced value. Same treatment for each entry in tool_name_to_cost_per_query.

A more complete fix would also validate MCPServerCostInfo as a Pydantic model (not a TypedDict) so the backend rejects non-numeric cost values at write time.

Steps to Reproduce

  1. Configure or import an MCP server where mcp_info.mcp_server_cost_info.default_cost_per_query is stored as a string (e.g. "0.005" instead of 0.005). This can be produced by: (a) setting the value via a YAML/JSON import path that preserves the string, (b) a direct API call with a stringified number, or (c) updating the antd InputNumber under conditions where it emits a string.
  2. Navigate to the MCP Servers page in the UI and click the affected server to open settings.
  3. The page throws Uncaught TypeError: e.default_cost_per_query.toFixed is not a function and fails to render.

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?

UI Dashboard

What LiteLLM version are you on ?

v1.83.14-stable (bug also present on main @ 934ecdca78, file unchanged since upstream PR #12526)

Twitter / LinkedIn details

No response

extent analysis

TL;DR

Coerce cost values to numbers before formatting to prevent the Uncaught TypeError.

Guidance

  • Verify that the default_cost_per_query value is indeed a string by checking the mcp_info JSONB column or the API response.
  • Use a helper function like toFiniteNumber to coerce the value to a number before calling toFixed(4).
  • Apply the same coercion to each entry in tool_name_to_cost_per_query to ensure consistent formatting.
  • Consider validating MCPServerCostInfo as a Pydantic model to reject non-numeric cost values at write time.

Example

const toFiniteNumber = (v: unknown): number | null => {
  if (typeof v === "number") return Number.isFinite(v) ? v : null;
  if (typeof v === "string" && v.trim() !== "") {
    const n = Number(v);
    return Number.isFinite(n) ? n : null;
  }
  return null;
};

Notes

This fix assumes that the cost values are intended to be numbers. If string values are valid, additional handling may be required.

Recommendation

Apply the workaround by coercing cost values to numbers before formatting, as this is a minimally invasive fix that can be implemented quickly. A more complete fix would involve validating MCPServerCostInfo as a Pydantic model, but this may require more significant changes.

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…

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 view crashes with "default_cost_per_query.toFixed is not a function" [1 pull requests, 1 comments, 1 participants]