paygate-mcp 8.0.0 → 8.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -107,6 +107,8 @@ Agent → PayGate (auth + billing) → Your MCP Server (stdio or HTTP)
107
107
  - **Batch Dry Run** — `POST /requests/dry-run/batch` simulates multiple tool calls at once — aggregate credit check, per-tool ACL validation, spending limit, returns per-tool results with total credits required and credits-after
108
108
  - **Tool Availability** — `GET /tools/available?key=...` returns per-key tool availability with pricing, affordability (canAfford), ACL enforcement (accessible/denyReason), and per-tool + global rate limit status
109
109
  - **Key Dashboard** — `GET /keys/dashboard?key=...` consolidated single-endpoint view with metadata, balance, health score, spending velocity, rate limits, quotas, usage summary, and recent activity timeline
110
+ - **Admin Notifications** — `GET /admin/notifications` scans all keys for actionable issues: expired/expiring keys, zero credits, credit depletion velocity, suspended keys, high error rates, and rate limit pressure — with severity filtering and priority sorting
111
+ - **System Dashboard** — `GET /admin/dashboard` system-wide overview with key counts (active/suspended/revoked/expired), credit summary (allocated/spent/remaining), usage breakdown with deny reasons, top consumers, top tools, notification counts, and uptime
110
112
  - **Config Hot Reload** — `POST /config/reload` reloads pricing, rate limits, webhooks, quotas, and behavior flags from config file without server restart
111
113
  - **Webhook Events** — POST batched usage events to any URL for external billing/alerting
112
114
  - **Config File Mode** — Load all settings from a JSON file (`--config`)
@@ -2360,6 +2362,111 @@ curl "http://localhost:3402/keys/dashboard?key=pg_..." \
2360
2362
 
2361
2363
  Combines metadata (status/namespace/group/tags), balance (credits/spent/allocated/spendingLimit), health score (0-100 composite), spending velocity with depletion forecast, rate limit and quota status, usage summary, and last 10 audit events. Supports alias keys. Works on suspended/revoked/expired keys. Read-only.
2362
2364
 
2365
+ ### Admin Notifications
2366
+
2367
+ Get actionable notifications about keys that need attention:
2368
+
2369
+ ```bash
2370
+ # Get all notifications
2371
+ curl http://localhost:3402/admin/notifications \
2372
+ -H "X-Admin-Key: YOUR_ADMIN_KEY"
2373
+
2374
+ # Filter by severity
2375
+ curl "http://localhost:3402/admin/notifications?severity=critical" \
2376
+ -H "X-Admin-Key: YOUR_ADMIN_KEY"
2377
+ ```
2378
+
2379
+ **Response:**
2380
+
2381
+ ```json
2382
+ {
2383
+ "total": 4,
2384
+ "critical": 2,
2385
+ "warning": 1,
2386
+ "info": 1,
2387
+ "notifications": [
2388
+ {
2389
+ "severity": "critical",
2390
+ "category": "zero_credits",
2391
+ "message": "Key has zero credits remaining",
2392
+ "key": "pg_c815...09a6",
2393
+ "keyName": "production-agent"
2394
+ },
2395
+ {
2396
+ "severity": "critical",
2397
+ "category": "key_expiring_soon",
2398
+ "message": "Key expires within 8 hours",
2399
+ "key": "pg_a3f1...b2e4",
2400
+ "keyName": "staging-agent",
2401
+ "details": { "expiresAt": "2026-02-27T08:00:00.000Z", "hoursRemaining": 7.5 }
2402
+ },
2403
+ {
2404
+ "severity": "warning",
2405
+ "category": "credits_depleting",
2406
+ "message": "Credits will deplete in ~18 hours at current rate",
2407
+ "key": "pg_d7e2...f1a3",
2408
+ "keyName": "batch-worker",
2409
+ "details": { "credits": 90, "creditsPerHour": 5.1, "estimatedHoursRemaining": 17.6 }
2410
+ },
2411
+ {
2412
+ "severity": "info",
2413
+ "category": "key_suspended",
2414
+ "message": "Key is suspended",
2415
+ "key": "pg_b4c9...e8d5",
2416
+ "keyName": "deprecated-agent"
2417
+ }
2418
+ ]
2419
+ }
2420
+ ```
2421
+
2422
+ **Notification categories:**
2423
+ - `key_expired` (critical) — Key has passed its expiry date
2424
+ - `key_expiring_soon` (critical <24h, warning <7d) — Key approaching expiry
2425
+ - `zero_credits` (critical) — Key has no credits remaining
2426
+ - `credits_depleting` (critical <6h, warning <24h) — Spending velocity predicts depletion
2427
+ - `key_suspended` (info) — Key is suspended
2428
+ - `high_error_rate` (critical ≥50%, warning ≥25%) — High denial rate (min 10 calls)
2429
+ - `rate_limit_pressure` (warning ≥90%) — Rate limit nearly exhausted
2430
+
2431
+ Notifications are sorted by severity (critical first). Revoked keys are excluded. A single key can appear in multiple notifications (e.g., zero credits AND expiring soon). Filter with `?severity=critical|warning|info`. Read-only.
2432
+
2433
+ ### System Dashboard
2434
+
2435
+ Get a system-wide overview in a single request:
2436
+
2437
+ ```bash
2438
+ curl http://localhost:3402/admin/dashboard \
2439
+ -H "X-Admin-Key: YOUR_ADMIN_KEY"
2440
+ ```
2441
+
2442
+ **Response:**
2443
+
2444
+ ```json
2445
+ {
2446
+ "keys": { "total": 15, "active": 10, "suspended": 2, "revoked": 2, "expired": 1 },
2447
+ "credits": { "totalAllocated": 15000, "totalSpent": 4200, "totalRemaining": 10800 },
2448
+ "usage": {
2449
+ "totalCalls": 840,
2450
+ "totalAllowed": 790,
2451
+ "totalDenied": 50,
2452
+ "totalCreditsSpent": 4200,
2453
+ "denyReasons": [{ "reason": "insufficient_credits", "count": 30 }, { "reason": "rate_limited", "count": 20 }]
2454
+ },
2455
+ "topConsumers": [
2456
+ { "name": "production-agent", "calls": 320, "credits": 1600, "denied": 5 },
2457
+ { "name": "batch-worker", "calls": 210, "credits": 1050, "denied": 0 }
2458
+ ],
2459
+ "topTools": [
2460
+ { "tool": "search", "calls": 450, "credits": 2250, "denied": 20 },
2461
+ { "tool": "generate", "calls": 300, "credits": 1500, "denied": 10 }
2462
+ ],
2463
+ "notifications": { "critical": 2, "warning": 3, "info": 2 },
2464
+ "uptime": { "startedAt": "2026-02-27T00:00:00.000Z", "uptimeSeconds": 86400, "uptimeHours": 24 }
2465
+ }
2466
+ ```
2467
+
2468
+ Combines key counts by state, credit allocation and spending totals, usage breakdown with deny reasons, top 10 consumers ranked by credits spent, top 10 tools ranked by call count, notification severity counts, and server uptime. Read-only.
2469
+
2363
2470
  ### IP Allowlisting
2364
2471
 
2365
2472
  Restrict API keys to specific IP addresses or CIDR ranges:
package/dist/server.d.ts CHANGED
@@ -242,6 +242,8 @@ export declare class PayGateServer {
242
242
  private handleGetMaintenance;
243
243
  private handleSetMaintenance;
244
244
  private handleAdminEventStream;
245
+ private handleAdminNotifications;
246
+ private handleSystemDashboard;
245
247
  private handleGetNotes;
246
248
  private handleAddNote;
247
249
  private handleDeleteNote;
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
package/dist/server.js CHANGED
@@ -798,6 +798,18 @@ class PayGateServer {
798
798
  return this.handleRevokeAdminKey(req, res);
799
799
  case '/admin/events':
800
800
  return this.handleAdminEventStream(req, res);
801
+ case '/admin/notifications':
802
+ if (req.method === 'GET')
803
+ return this.handleAdminNotifications(req, res);
804
+ res.writeHead(405, { 'Content-Type': 'application/json' });
805
+ res.end(JSON.stringify({ error: 'Method not allowed. Use GET.' }));
806
+ return;
807
+ case '/admin/dashboard':
808
+ if (req.method === 'GET')
809
+ return this.handleSystemDashboard(req, res);
810
+ res.writeHead(405, { 'Content-Type': 'application/json' });
811
+ res.end(JSON.stringify({ error: 'Method not allowed. Use GET.' }));
812
+ return;
801
813
  // ─── Plugin endpoints ──────────────────────────────────────────────
802
814
  case '/plugins':
803
815
  return this.handleListPlugins(req, res);
@@ -1338,6 +1350,8 @@ class PayGateServer {
1338
1350
  toolStats: 'GET /tools/stats — Per-tool call counts, success rates, latency, credits, and top consumers (requires X-Admin-Key)',
1339
1351
  toolAvailability: 'GET /tools/available?key=... — Per-key tool availability with pricing, affordability, and rate limit status (requires X-Admin-Key)',
1340
1352
  keyDashboard: 'GET /keys/dashboard?key=... — Consolidated key overview with metadata, balance, health, velocity, rate limits, quotas, and recent activity (requires X-Admin-Key)',
1353
+ adminNotifications: 'GET /admin/notifications — Actionable notifications for expiring keys, low credits, high error rates, and rate limit pressure (requires X-Admin-Key)',
1354
+ systemDashboard: 'GET /admin/dashboard — System-wide overview with key stats, credit summary, usage breakdown, top consumers, and uptime (requires X-Admin-Key)',
1341
1355
  ...(this.oauth ? {
1342
1356
  oauthMetadata: 'GET /.well-known/oauth-authorization-server — OAuth 2.1 server metadata',
1343
1357
  oauthRegister: 'POST /oauth/register — Register OAuth client',
@@ -4296,6 +4310,248 @@ class PayGateServer {
4296
4310
  }
4297
4311
  });
4298
4312
  }
4313
+ // ─── /admin/notifications — Actionable notifications ──────────────────────
4314
+ handleAdminNotifications(req, res) {
4315
+ if (!this.checkAdmin(req, res))
4316
+ return;
4317
+ const urlParts = req.url?.split('?') || [];
4318
+ const params = new URLSearchParams(urlParts[1] || '');
4319
+ const severityFilter = params.get('severity'); // critical, warning, info, or null for all
4320
+ const notifications = [];
4321
+ // Scan all keys for issues
4322
+ const allRecords = this.gate.store.getAllRecords();
4323
+ for (const record of allRecords) {
4324
+ const maskedKey = record.key.slice(0, 7) + '...' + record.key.slice(-4);
4325
+ const keyName = record.name || undefined;
4326
+ // Skip revoked keys
4327
+ if (!record.active)
4328
+ continue;
4329
+ // ── Expiry checks ──
4330
+ if (record.expiresAt) {
4331
+ const msToExpiry = new Date(record.expiresAt).getTime() - Date.now();
4332
+ const hoursToExpiry = msToExpiry / 3_600_000;
4333
+ if (msToExpiry <= 0) {
4334
+ notifications.push({
4335
+ severity: 'critical', category: 'key_expired',
4336
+ message: `Key has expired`,
4337
+ key: maskedKey, keyName,
4338
+ details: { expiresAt: record.expiresAt },
4339
+ });
4340
+ }
4341
+ else if (hoursToExpiry <= 24) {
4342
+ notifications.push({
4343
+ severity: 'critical', category: 'key_expiring_soon',
4344
+ message: `Key expires within ${Math.ceil(hoursToExpiry)} hours`,
4345
+ key: maskedKey, keyName,
4346
+ details: { expiresAt: record.expiresAt, hoursRemaining: Math.round(hoursToExpiry * 10) / 10 },
4347
+ });
4348
+ }
4349
+ else if (hoursToExpiry <= 168) {
4350
+ notifications.push({
4351
+ severity: 'warning', category: 'key_expiring_soon',
4352
+ message: `Key expires within ${Math.ceil(hoursToExpiry / 24)} days`,
4353
+ key: maskedKey, keyName,
4354
+ details: { expiresAt: record.expiresAt, daysRemaining: Math.round(hoursToExpiry / 24 * 10) / 10 },
4355
+ });
4356
+ }
4357
+ }
4358
+ // ── Credit checks ──
4359
+ if (record.credits <= 0) {
4360
+ notifications.push({
4361
+ severity: 'critical', category: 'zero_credits',
4362
+ message: `Key has zero credits remaining`,
4363
+ key: maskedKey, keyName,
4364
+ });
4365
+ }
4366
+ else {
4367
+ // Check spending velocity for depletion
4368
+ const velocity = this.creditLedger.getSpendingVelocity(record.key, record.credits, 24);
4369
+ if (velocity.creditsPerHour > 0) {
4370
+ const hoursLeft = velocity.estimatedHoursRemaining ?? Infinity;
4371
+ if (hoursLeft <= 6) {
4372
+ notifications.push({
4373
+ severity: 'critical', category: 'credits_depleting',
4374
+ message: `Credits will deplete in ~${Math.ceil(hoursLeft)} hours at current rate`,
4375
+ key: maskedKey, keyName,
4376
+ details: { credits: record.credits, creditsPerHour: velocity.creditsPerHour, estimatedHoursRemaining: Math.round(hoursLeft * 10) / 10 },
4377
+ });
4378
+ }
4379
+ else if (hoursLeft <= 24) {
4380
+ notifications.push({
4381
+ severity: 'warning', category: 'credits_depleting',
4382
+ message: `Credits will deplete in ~${Math.ceil(hoursLeft)} hours at current rate`,
4383
+ key: maskedKey, keyName,
4384
+ details: { credits: record.credits, creditsPerHour: velocity.creditsPerHour, estimatedHoursRemaining: Math.round(hoursLeft * 10) / 10 },
4385
+ });
4386
+ }
4387
+ }
4388
+ }
4389
+ // ── Suspended key ──
4390
+ if (record.suspended) {
4391
+ notifications.push({
4392
+ severity: 'info', category: 'key_suspended',
4393
+ message: `Key is suspended`,
4394
+ key: maskedKey, keyName,
4395
+ });
4396
+ }
4397
+ // ── Error rate check ──
4398
+ const keyUsage = this.gate.meter.getKeyUsage(record.key);
4399
+ if (keyUsage.totalCalls >= 10) {
4400
+ const errorRate = keyUsage.totalDenied / keyUsage.totalCalls;
4401
+ if (errorRate >= 0.5) {
4402
+ notifications.push({
4403
+ severity: 'critical', category: 'high_error_rate',
4404
+ message: `${Math.round(errorRate * 100)}% error rate (${keyUsage.totalDenied}/${keyUsage.totalCalls} denied)`,
4405
+ key: maskedKey, keyName,
4406
+ details: { errorRate: Math.round(errorRate * 1000) / 1000, totalCalls: keyUsage.totalCalls, totalDenied: keyUsage.totalDenied },
4407
+ });
4408
+ }
4409
+ else if (errorRate >= 0.25) {
4410
+ notifications.push({
4411
+ severity: 'warning', category: 'high_error_rate',
4412
+ message: `${Math.round(errorRate * 100)}% error rate (${keyUsage.totalDenied}/${keyUsage.totalCalls} denied)`,
4413
+ key: maskedKey, keyName,
4414
+ details: { errorRate: Math.round(errorRate * 1000) / 1000, totalCalls: keyUsage.totalCalls, totalDenied: keyUsage.totalDenied },
4415
+ });
4416
+ }
4417
+ }
4418
+ // ── Rate limit pressure ──
4419
+ const rateStatus = this.gate.rateLimiter.getStatus(record.key);
4420
+ if (rateStatus.limit > 0) {
4421
+ const utilization = rateStatus.used / rateStatus.limit;
4422
+ if (utilization >= 0.9) {
4423
+ notifications.push({
4424
+ severity: 'warning', category: 'rate_limit_pressure',
4425
+ message: `${Math.round(utilization * 100)}% rate limit utilization (${rateStatus.used}/${rateStatus.limit})`,
4426
+ key: maskedKey, keyName,
4427
+ details: { used: rateStatus.used, limit: rateStatus.limit, remaining: rateStatus.remaining },
4428
+ });
4429
+ }
4430
+ }
4431
+ }
4432
+ // Apply severity filter
4433
+ let filtered = notifications;
4434
+ if (severityFilter) {
4435
+ filtered = notifications.filter(n => n.severity === severityFilter);
4436
+ }
4437
+ // Sort: critical first, then warning, then info
4438
+ const severityOrder = { critical: 0, warning: 1, info: 2 };
4439
+ filtered.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
4440
+ res.writeHead(200, { 'Content-Type': 'application/json' });
4441
+ res.end(JSON.stringify({
4442
+ total: filtered.length,
4443
+ critical: filtered.filter(n => n.severity === 'critical').length,
4444
+ warning: filtered.filter(n => n.severity === 'warning').length,
4445
+ info: filtered.filter(n => n.severity === 'info').length,
4446
+ notifications: filtered,
4447
+ }));
4448
+ }
4449
+ // ─── /admin/dashboard — System-wide overview ──────────────────────────────
4450
+ handleSystemDashboard(req, res) {
4451
+ if (!this.checkAdmin(req, res))
4452
+ return;
4453
+ const allRecords = this.gate.store.getAllRecords();
4454
+ const now = Date.now();
4455
+ // ── Key statistics ──
4456
+ let activeCount = 0;
4457
+ let suspendedCount = 0;
4458
+ let revokedCount = 0;
4459
+ let expiredCount = 0;
4460
+ let totalAllocated = 0;
4461
+ let totalSpent = 0;
4462
+ let totalRemaining = 0;
4463
+ for (const record of allRecords) {
4464
+ totalAllocated += record.credits + (record.totalSpent || 0);
4465
+ totalSpent += record.totalSpent || 0;
4466
+ totalRemaining += record.credits;
4467
+ if (!record.active) {
4468
+ revokedCount++;
4469
+ }
4470
+ else if (record.expiresAt && new Date(record.expiresAt).getTime() <= now) {
4471
+ expiredCount++;
4472
+ }
4473
+ else if (record.suspended) {
4474
+ suspendedCount++;
4475
+ }
4476
+ else {
4477
+ activeCount++;
4478
+ }
4479
+ }
4480
+ // ── Usage summary ──
4481
+ const usageSummary = this.gate.meter.getSummary();
4482
+ // ── Top consumers (by credits spent) ──
4483
+ const perKeyEntries = Object.entries(usageSummary.perKey)
4484
+ .map(([name, stats]) => ({ name, ...stats }))
4485
+ .sort((a, b) => b.credits - a.credits)
4486
+ .slice(0, 10);
4487
+ // ── Top tools (by call count) ──
4488
+ const perToolEntries = Object.entries(usageSummary.perTool)
4489
+ .map(([tool, stats]) => ({ tool, ...stats }))
4490
+ .sort((a, b) => b.calls - a.calls)
4491
+ .slice(0, 10);
4492
+ // ── Deny reason breakdown ──
4493
+ const denyReasons = Object.entries(usageSummary.denyReasons)
4494
+ .map(([reason, count]) => ({ reason, count }))
4495
+ .sort((a, b) => b.count - a.count);
4496
+ // ── Uptime ──
4497
+ const uptimeMs = now - this.startedAt;
4498
+ const uptimeSeconds = Math.floor(uptimeMs / 1000);
4499
+ const uptimeHours = Math.round(uptimeMs / 3_600_000 * 10) / 10;
4500
+ // ── Notification summary (counts only) ──
4501
+ const notifRecords = allRecords.filter(r => r.active);
4502
+ let criticalCount = 0;
4503
+ let warningCount = 0;
4504
+ let infoCount = 0;
4505
+ for (const record of notifRecords) {
4506
+ if (record.expiresAt) {
4507
+ const msToExpiry = new Date(record.expiresAt).getTime() - now;
4508
+ if (msToExpiry <= 0)
4509
+ criticalCount++;
4510
+ else if (msToExpiry <= 24 * 3_600_000)
4511
+ criticalCount++;
4512
+ else if (msToExpiry <= 168 * 3_600_000)
4513
+ warningCount++;
4514
+ }
4515
+ if (record.credits <= 0)
4516
+ criticalCount++;
4517
+ if (record.suspended)
4518
+ infoCount++;
4519
+ }
4520
+ res.writeHead(200, { 'Content-Type': 'application/json' });
4521
+ res.end(JSON.stringify({
4522
+ keys: {
4523
+ total: allRecords.length,
4524
+ active: activeCount,
4525
+ suspended: suspendedCount,
4526
+ revoked: revokedCount,
4527
+ expired: expiredCount,
4528
+ },
4529
+ credits: {
4530
+ totalAllocated,
4531
+ totalSpent,
4532
+ totalRemaining,
4533
+ },
4534
+ usage: {
4535
+ totalCalls: usageSummary.totalCalls,
4536
+ totalAllowed: usageSummary.totalCalls - usageSummary.totalDenied,
4537
+ totalDenied: usageSummary.totalDenied,
4538
+ totalCreditsSpent: usageSummary.totalCreditsSpent,
4539
+ denyReasons,
4540
+ },
4541
+ topConsumers: perKeyEntries,
4542
+ topTools: perToolEntries,
4543
+ notifications: {
4544
+ critical: criticalCount,
4545
+ warning: warningCount,
4546
+ info: infoCount,
4547
+ },
4548
+ uptime: {
4549
+ startedAt: new Date(this.startedAt).toISOString(),
4550
+ uptimeSeconds,
4551
+ uptimeHours,
4552
+ },
4553
+ }));
4554
+ }
4299
4555
  // ─── /keys/notes — Timestamped notes on API keys ─────────────────────────
4300
4556
  handleGetNotes(req, res) {
4301
4557
  if (!this.checkAdmin(req, res))