Skip to main content
Glama
token-counter.ts6.46 kB
import { html, css, nothing } from 'lit'; import { customElement, property } from 'lit/decorators.js'; import { McpBaseElement } from './base.js'; import { formatNumber, clamp } from '../utils/index.js'; /** * Token usage counter with visual progress indicator. * Displays used tokens vs limit with warning states. * * @element mcp-token-counter * * @prop {number} used - Number of tokens used * @prop {number} limit - Maximum token limit * @prop {boolean} showLabel - Whether to show the label text * @prop {number} warningThreshold - Percentage threshold for warning state (0-100) * * @csspart container - The outer container * @csspart bar - The progress bar track * @csspart bar-fill - The progress bar fill * @csspart label - The text label * * @cssprop --mcp-token-bar-bg - Progress bar background * @cssprop --mcp-token-bar-fill - Progress bar fill color * @cssprop --mcp-token-bar-height - Progress bar height * @cssprop --mcp-token-warning - Warning state color * @cssprop --mcp-token-error - Error/exceeded state color * * @fires mcp-limit-warning - Fired when usage exceeds warning threshold * @fires mcp-limit-exceeded - Fired when usage exceeds 100% * * @example * <mcp-token-counter used="3500" limit="4096"></mcp-token-counter> * <mcp-token-counter used="100000" limit="128000" show-label></mcp-token-counter> */ @customElement('mcp-token-counter') export class McpTokenCounter extends McpBaseElement { static styles = [ ...McpBaseElement.baseStyles, css` :host { display: block; --_bar-bg: var(--mcp-token-bar-bg, var(--mcp-color-surface-sunken)); --_bar-fill: var(--mcp-token-bar-fill, var(--mcp-color-primary)); --_bar-height: var(--mcp-token-bar-height, 6px); --_warning: var(--mcp-token-warning, var(--mcp-color-warning)); --_error: var(--mcp-token-error, var(--mcp-color-error)); } .container { display: flex; flex-direction: column; gap: var(--mcp-space-2); } .bar { position: relative; width: 100%; height: var(--_bar-height); background: var(--_bar-bg); border-radius: var(--mcp-radius-full); overflow: hidden; } .bar-fill { position: absolute; top: 0; left: 0; height: 100%; background: var(--_bar-fill); border-radius: var(--mcp-radius-full); transition: width var(--mcp-transition-base), background var(--mcp-transition-base); } :host([state='warning']) .bar-fill { background: var(--_warning); } :host([state='error']) .bar-fill { background: var(--_error); } .label { display: flex; justify-content: space-between; align-items: center; font-size: var(--mcp-font-size-xs); color: var(--mcp-color-text-muted); } :host([state='warning']) .label { color: var(--_warning); } :host([state='error']) .label { color: var(--_error); } .count { font-variant-numeric: tabular-nums; } .percentage { font-weight: 500; } /* Compact variant */ :host([variant='compact']) .container { flex-direction: row; align-items: center; gap: var(--mcp-space-3); } :host([variant='compact']) .bar { flex: 1; } :host([variant='compact']) .label { flex-shrink: 0; width: auto; } `, ]; /** * Number of tokens used. */ @property({ type: Number }) used = 0; /** * Maximum token limit. */ @property({ type: Number }) limit = 4096; /** * Whether to show the label text. */ @property({ type: Boolean, attribute: 'show-label' }) showLabel = false; /** * Display variant. */ @property({ reflect: true }) variant: 'default' | 'compact' = 'default'; /** * Warning threshold percentage (0-100). */ @property({ type: Number, attribute: 'warning-threshold' }) warningThreshold = 80; private _lastEmittedState: string | null = null; willUpdate(changedProps: Map<string, unknown>) { if (changedProps.has('used') || changedProps.has('limit') || changedProps.has('warningThreshold')) { this._updateState(); } } private _updateState() { const percentage = this._percentage; let newState: 'normal' | 'warning' | 'error' = 'normal'; if (percentage >= 100) { newState = 'error'; } else if (percentage >= this.warningThreshold) { newState = 'warning'; } // Emit events only on state change if (newState !== this._lastEmittedState) { if (newState === 'warning') { this.emit('limit-warning', { used: this.used, limit: this.limit, percentage }); } else if (newState === 'error') { this.emit('limit-exceeded', { used: this.used, limit: this.limit, percentage }); } this._lastEmittedState = newState; } // Update reflected state attribute for CSS this.setAttribute('state', newState); } private get _percentage(): number { if (this.limit <= 0) return 0; return (this.used / this.limit) * 100; } private get _clampedPercentage(): number { return clamp(this._percentage, 0, 100); } render() { const percentage = this._clampedPercentage; return html` <div class="container" part="container" role="progressbar" aria-valuenow="${this.used}" aria-valuemin="0" aria-valuemax="${this.limit}" aria-label="Token usage: ${formatNumber(this.used)} of ${formatNumber(this.limit)}"> ${this.showLabel ? html` <div class="label" part="label"> <span class="count">${formatNumber(this.used)} / ${formatNumber(this.limit)} tokens</span> <span class="percentage">${Math.round(this._percentage)}%</span> </div> ` : nothing} <div class="bar" part="bar"> <div class="bar-fill" part="bar-fill" style="width: ${percentage}%"></div> </div> ${!this.showLabel ? html` <div class="label" part="label"> <span class="count">${formatNumber(this.used)} / ${formatNumber(this.limit)}</span> </div> ` : nothing} </div> `; } } declare global { interface HTMLElementTagNameMap { 'mcp-token-counter': McpTokenCounter; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/heyadam/mcpsystemdesign'

If you have feedback or need assistance with the MCP directory API, please join our Discord server