Skip to main content
Glama
usage-stats-sender.ts3.36 kB
import { Logger } from "winston"; import { hashObject } from "@mcpx/toolkit-core/data"; import { WebappBoundPayloadOf, wrapInEnvelope, EnvelopedMessage, UsageStatsTargetServerInput, } from "@mcpx/webapp-protocol/messages"; import { SystemState } from "@mcpx/shared-model"; export interface UsageStatsSocket { emit( event: "usage-stats", data: EnvelopedMessage<WebappBoundPayloadOf<"usage-stats">>, ): void; } export class UsageStatsSender { private intervalId: NodeJS.Timeout | null = null; private lastPayloadHash: string | null = null; constructor( private readonly logger: Logger, private readonly getUsageStats: () => WebappBoundPayloadOf<"usage-stats">, private readonly intervalMs: number, ) {} start(socket: UsageStatsSocket): void { this.stop(); this.sendIfChanged(socket); this.intervalId = setInterval(() => { this.sendIfChanged(socket); }, this.intervalMs); } stop(): void { if (this.intervalId) { clearInterval(this.intervalId); this.intervalId = null; } this.lastPayloadHash = null; } private sendIfChanged(socket: UsageStatsSocket): void { const payload = this.getUsageStats(); const payloadHash = this.hashPayload(payload); if (payloadHash === this.lastPayloadHash) { this.logger.debug("Usage stats unchanged, skipping send"); return; } const envelopedMessage = wrapInEnvelope(payload); this.logger.debug("Sending usage stats to Hub", { messageId: envelopedMessage.metadata.id, }); socket.emit("usage-stats", envelopedMessage); this.lastPayloadHash = payloadHash; } private hashPayload(payload: WebappBoundPayloadOf<"usage-stats">): string { return hashObject(payload); } } export function buildUsageStatsPayload( state: SystemState, ): WebappBoundPayloadOf<"usage-stats"> { const agents = state.connectedClients.map((client) => ({ clientInfo: { protocolVersion: client.clientInfo?.protocolVersion, name: client.clientInfo?.name, version: client.clientInfo?.version, }, })); const targetServers: Array<UsageStatsTargetServerInput> = []; for (const server of state.targetServers_new) { const originalToolNames = new Set( server.originalTools.map((tool) => tool.name), ); // Map "inactive" status to "connected" for usage stats // (inactive servers are still connected, just disabled) let status: "connected" | "pending-auth" | "connection-failed"; if (server.state.type === "inactive") { status = "connected"; } else if ( server.state.type === "connected" || server.state.type === "pending-auth" || server.state.type === "connection-failed" ) { status = server.state.type; } else { status = "connected"; } targetServers.push({ name: server.name, status: status, type: server._type, tools: Object.fromEntries( server.tools.map((tool) => [ tool.name, { description: tool.description, isCustom: !originalToolNames.has(tool.name), usage: { callCount: tool.usage.callCount, lastCalledAt: tool.usage.lastCalledAt?.toISOString(), }, }, ]), ), }); } return { agents, targetServers, }; }

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/TheLunarCompany/lunar'

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