@mcpspend/proxy
Advanced tools
+1
-1
@@ -9,3 +9,3 @@ #!/usr/bin/env node | ||
| const http_bridge_js_1 = require("./http-bridge.js"); | ||
| const VERSION = '0.5.0'; | ||
| const VERSION = '0.5.1'; | ||
| const HELP = `mcpspend — observability proxy for MCP servers (v${VERSION}) | ||
@@ -12,0 +12,0 @@ |
+11
-0
@@ -42,2 +42,13 @@ import { ClientDefinition, WrapResult } from './clients.js'; | ||
| endpointError?: string; | ||
| account?: { | ||
| orgName: string; | ||
| plan: string; | ||
| callsThisMonth: number; | ||
| callsLimit: number; | ||
| }; | ||
| lastCall?: { | ||
| serverName: string; | ||
| toolName: string; | ||
| calledAt: string; | ||
| } | null; | ||
| clients: Array<{ | ||
@@ -44,0 +55,0 @@ client: ClientDefinition['id']; |
+93
-2
@@ -224,9 +224,9 @@ "use strict"; | ||
| }); | ||
| const baseUrl = cfg.endpoint || 'https://api.mcpspend.com'; | ||
| let endpointReachable; | ||
| let endpointError; | ||
| try { | ||
| const url = (cfg.endpoint || 'https://api.mcpspend.com') + '/health'; | ||
| const ac = new AbortController(); | ||
| const timer = setTimeout(() => ac.abort(), 5000); | ||
| const resp = await fetch(url, { signal: ac.signal }); | ||
| const resp = await fetch(baseUrl + '/health', { signal: ac.signal }); | ||
| clearTimeout(timer); | ||
@@ -241,2 +241,60 @@ endpointReachable = resp.ok; | ||
| } | ||
| // Probe the key — gives users actionable confirmation that the key resolves, | ||
| // which org it points at, and current usage. We catch every error so doctor | ||
| // never crashes on a broken backend; account just stays undefined. | ||
| let account; | ||
| let lastCall; | ||
| const key = process.env.MCPSPEND_API_KEY || cfg.apiKey; | ||
| if (key && endpointReachable) { | ||
| try { | ||
| const ac = new AbortController(); | ||
| const timer = setTimeout(() => ac.abort(), 5000); | ||
| const r = await fetch(baseUrl + '/api/stats/sessions?limit=1', { | ||
| headers: { Authorization: `Bearer ${key}` }, | ||
| signal: ac.signal, | ||
| }); | ||
| clearTimeout(timer); | ||
| if (r.ok) { | ||
| const sessions = (await r.json()); | ||
| if (sessions[0]) { | ||
| // No per-call endpoint here — last session's startedAt is the cheapest | ||
| // proxy for "we've seen activity recently". | ||
| lastCall = { | ||
| serverName: 'session', | ||
| toolName: sessions[0].id, | ||
| calledAt: sessions[0].startedAt, | ||
| }; | ||
| } | ||
| else { | ||
| lastCall = null; | ||
| } | ||
| } | ||
| } | ||
| catch { | ||
| // ignore — best-effort | ||
| } | ||
| // Org/plan probe via /api/auth/me equivalent — API keys reach this through | ||
| // the same auth middleware but only get back their own org details. | ||
| try { | ||
| const ac = new AbortController(); | ||
| const timer = setTimeout(() => ac.abort(), 5000); | ||
| const r = await fetch(baseUrl + '/api/organizations/current', { | ||
| headers: { Authorization: `Bearer ${key}` }, | ||
| signal: ac.signal, | ||
| }); | ||
| clearTimeout(timer); | ||
| if (r.ok) { | ||
| const org = (await r.json()); | ||
| account = { | ||
| orgName: org.name, | ||
| plan: org.plan, | ||
| callsThisMonth: org.callsThisMonth, | ||
| callsLimit: org.callsLimit, | ||
| }; | ||
| } | ||
| } | ||
| catch { | ||
| // ignore | ||
| } | ||
| } | ||
| return { | ||
@@ -248,2 +306,4 @@ cliVersion, | ||
| endpointError, | ||
| account, | ||
| lastCall, | ||
| clients, | ||
@@ -258,2 +318,19 @@ }; | ||
| lines.push(`Endpoint: ${report.endpointReachable ? '✓ reachable' : `✗ ${report.endpointError || 'unreachable'}`}`); | ||
| if (report.account) { | ||
| const a = report.account; | ||
| const pct = a.callsLimit > 0 ? Math.round((a.callsThisMonth / a.callsLimit) * 100) : 0; | ||
| lines.push(`Account: ✓ ${a.orgName} (${a.plan})`); | ||
| lines.push(`Usage: ${a.callsThisMonth.toLocaleString()} / ${a.callsLimit.toLocaleString()} calls this month (${pct}%)`); | ||
| } | ||
| else if (report.apiKeyConfigured && report.endpointReachable) { | ||
| lines.push(`Account: ✗ API key was rejected — generate a new one at https://mcpspend.com/dashboard/keys`); | ||
| } | ||
| if (report.lastCall) { | ||
| const when = report.lastCall.calledAt; | ||
| const ago = humanAgo(new Date(when)); | ||
| lines.push(`Last call: ${ago}`); | ||
| } | ||
| else if (report.account) { | ||
| lines.push(`Last call: none yet — make any tool call in your MCP client and it shows up here within seconds`); | ||
| } | ||
| lines.push(''); | ||
@@ -277,1 +354,15 @@ lines.push('Clients:'); | ||
| } | ||
| function humanAgo(d) { | ||
| const ms = Date.now() - d.getTime(); | ||
| const s = Math.floor(ms / 1000); | ||
| if (s < 60) | ||
| return `${s}s ago`; | ||
| const m = Math.floor(s / 60); | ||
| if (m < 60) | ||
| return `${m}m ago`; | ||
| const h = Math.floor(m / 60); | ||
| if (h < 24) | ||
| return `${h}h ago`; | ||
| const days = Math.floor(h / 24); | ||
| return `${days}d ago`; | ||
| } |
+1
-1
| { | ||
| "name": "@mcpspend/proxy", | ||
| "version": "0.5.0", | ||
| "version": "0.5.1", | ||
| "description": "Transparent proxy CLI for MCP servers — tracks tool calls, latency, and cost via MCPSpend.", | ||
@@ -5,0 +5,0 @@ "license": "MIT", |
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 2 instances
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
94058
4.19%2175
4.92%6
50%