Files
hermes-agent/apps/desktop/src/lib/mcp-cost.ts
T
Teknium e22fa90769 feat(desktop): MCP fleet cost/usage overlay with schema token estimates and 30-day usage
Each configured server row on the MCP Capabilities page now shows what it
costs and whether it earns its keep:

- ~per-call token estimate of the server's tool schemas, summed over ENABLED
  tools only (ceil(schema_chars/4) via the existing include/exclude filter)
- 30-day usage count from getUsageAnalytics(30), cached per scope profile
  like the Toolsets tab's toolCallsCache, mapped to servers via the
  mcp__<server>__<tool> registry-name convention (tools/mcp_tool.py)
- a subtle muted "unused" pill on enabled, probed-ok servers with nonzero
  schema cost and zero 30-day uses — never a dialog

Backend: the /api/mcp/servers/{name}/test probe now fills an additive
per-tool `schema_chars` (length of the SAME converted registry schema the
agent registers). Older backends omit it → renderer shows counts only;
older renderers ignore the extra key. Display-only: nothing changes what
schemas are sent to models, no config knobs.

i18n keys (costTokens/usage30d/unusedPill) added to types/en/zh/zh-hant/ja
(ar inherits en via defineLocale overrides). Pure math lives in
lib/mcp-cost.ts with unit tests; Python wire shape pinned in
tests/hermes_cli/test_web_server_profile_unification.py.
2026-08-16 02:24:32 -07:00

75 lines
2.7 KiB
TypeScript

// MCP fleet cost/usage math — pure functions behind the Capabilities page's
// per-server cost overlay ("~4.2k tok, 3 uses/30d"). Display-only: nothing
// here changes what schemas are sent to models.
import { isToolEnabled } from '@/lib/mcp-tool-filter'
/** One probed tool as the desktop's /test endpoint reports it. `schema_chars`
* (the JSON-stringified registry schema's length) is additive-optional —
* older backends omit it and the UI simply shows no token estimate. */
export interface McpProbedTool {
description?: string
name: string
schema_chars?: number
}
/**
* Approximate per-call prompt-token cost of a server's tool schemas: every
* enabled tool's schema rides along on every model call, so the sum of
* `ceil(schema_chars / 4)` over ENABLED tools (per the server's
* `tools.include`/`tools.exclude` filter) is what the server "adds per call".
*
* Returns `null` when no enabled tool carries `schema_chars` (older backend,
* failed probe, or everything filtered out) — the caller shows no estimate.
*/
export function estimateServerTokens(
server: Record<string, unknown> | null | undefined,
tools: McpProbedTool[]
): null | number {
let total = 0
let sawSchema = false
for (const tool of tools) {
if (typeof tool.schema_chars !== 'number' || !Number.isFinite(tool.schema_chars) || tool.schema_chars <= 0) {
continue
}
if (!isToolEnabled(server, tool.name)) {
continue
}
sawSchema = true
total += Math.ceil(tool.schema_chars / 4)
}
return sawSchema ? total : null
}
/** Mirror of `sanitize_mcp_name_component` in tools/mcp_tool.py: hyphens (and
* anything else outside `[A-Za-z0-9_]`) become underscores. */
export const sanitizeMcpNameComponent = (value: string): string => value.replace(/[^A-Za-z0-9_]/g, '_')
/** Registry-name prefix for one server's tools. MCP tools register (and show
* up in usage analytics) as `mcp__<server>__<tool>` — the convention pinned
* by `mcp_prefixed_tool_name` in tools/mcp_tool.py. */
export const mcpServerUsagePrefix = (serverName: string): string => `mcp__${sanitizeMcpNameComponent(serverName)}__`
/**
* Sum 30-day analytics call counts across one server's tools (including the
* per-server utility tools like `…__list_resources`, which also carry the
* server prefix). Analytics keys are the registry names, so a prefix match is
* the same grouping the agent's own registry uses.
*/
export function serverUsageCount(serverName: string, toolCalls: Record<string, number>): number {
const prefix = mcpServerUsagePrefix(serverName)
let total = 0
for (const [tool, count] of Object.entries(toolCalls)) {
if (tool.startsWith(prefix) && typeof count === 'number' && Number.isFinite(count)) {
total += count
}
}
return total
}