paygate-mcp 8.5.0 → 8.7.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
@@ -112,6 +112,8 @@ Agent → PayGate (auth + billing) → Your MCP Server (stdio or HTTP)
112
112
  - **Key Lifecycle Report** — `GET /admin/lifecycle` aggregated lifecycle trends with daily creation/revocation/suspension buckets, average key lifetime, and at-risk keys (expiring, expired, zero credits)
113
113
  - **Cost Analysis** — `GET /admin/costs` cost-centric view with per-tool and per-namespace cost breakdowns, hourly spending trends, top spenders, average cost per call, and namespace filtering
114
114
  - **Rate Limit Analysis** — `GET /admin/rate-limits` rate limit utilization analysis with per-key and per-tool breakdown, denial trends, most throttled keys, and current window utilization
115
+ - **Quota Analysis** — `GET /admin/quotas` quota utilization analysis with per-key daily/monthly usage vs limits, per-tool denial breakdown, most constrained keys, and global/per-key quota source tracking
116
+ - **Denial Analysis** — `GET /admin/denials` comprehensive denial breakdown by reason type (insufficient_credits, rate_limited, quota_exceeded, key_suspended, etc.) with per-key and per-tool stats, hourly trends, and most denied keys
115
117
  - **Config Hot Reload** — `POST /config/reload` reloads pricing, rate limits, webhooks, quotas, and behavior flags from config file without server restart
116
118
  - **Webhook Events** — POST batched usage events to any URL for external billing/alerting
117
119
  - **Config File Mode** — Load all settings from a JSON file (`--config`)
@@ -2592,6 +2594,48 @@ curl http://localhost:3402/admin/rate-limits \
2592
2594
 
2593
2595
  Returns rate limit configuration, denial summary with throttle rate, per-key breakdown with current sliding window utilization, per-tool denial counts, hourly denial trends (last 24 hours), and top 10 most throttled keys ranked by denial count. Handles unlimited rate limits (globalLimitPerMin: 0). Read-only.
2594
2596
 
2597
+ ### Quota Analysis
2598
+
2599
+ ```bash
2600
+ curl http://localhost:3000/admin/quotas -H "X-Admin-Key: YOUR_ADMIN_KEY"
2601
+ ```
2602
+
2603
+ ```json
2604
+ {
2605
+ "config": { "globalQuota": { "dailyCallLimit": 100, "monthlyCallLimit": 1000, "dailyCreditLimit": 500, "monthlyCreditLimit": 5000 } },
2606
+ "summary": { "totalKeys": 5, "keysWithQuotas": 4, "totalQuotaDenials": 3, "quotaDenialRate": 0.02 },
2607
+ "perKey": [
2608
+ { "name": "heavy-user", "dailyCalls": 95, "monthlyCalls": 450, "dailyCredits": 475, "monthlyCredits": 2250, "dailyCallLimit": 100, "monthlyCallLimit": 1000, "dailyCreditLimit": 500, "monthlyCreditLimit": 5000, "dailyCallUtilization": 0.95, "monthlyCallUtilization": 0.45, "source": "global" }
2609
+ ],
2610
+ "perTool": [{ "tool": "summarize", "calls": 120, "quotaDenied": 2 }],
2611
+ "hourlyTrends": [{ "hour": "2025-01-15T14", "calls": 15, "quotaDenied": 1 }],
2612
+ "mostConstrained": [{ "name": "heavy-user", "dailyCalls": 95, "dailyCallLimit": 100, "dailyCallUtilization": 0.95, "monthlyCalls": 450, "monthlyCallLimit": 1000, "monthlyCallUtilization": 0.45 }]
2613
+ }
2614
+ ```
2615
+
2616
+ Returns quota configuration (global or null), key counts with/without quotas, denial summary with denial rate, per-key daily/monthly call and credit usage vs limits with utilization percentages, quota source (per-key/global/none), per-tool quota denial counts, hourly denial trends (last 24 hours), and top 10 most constrained keys ranked by daily call utilization. Read-only.
2617
+
2618
+ ### Denial Analysis
2619
+
2620
+ ```bash
2621
+ curl http://localhost:3000/admin/denials -H "X-Admin-Key: YOUR_ADMIN_KEY"
2622
+ ```
2623
+
2624
+ ```json
2625
+ {
2626
+ "summary": { "totalCalls": 150, "totalDenials": 12, "denialRate": 0.08 },
2627
+ "byReason": { "insufficient_credits": 5, "rate_limited": 4, "quota_exceeded": 2, "key_suspended": 1 },
2628
+ "perKey": [
2629
+ { "name": "heavy-user", "calls": 50, "denials": 8, "denialRate": 0.16, "topReason": "rate_limited" }
2630
+ ],
2631
+ "perTool": [{ "tool": "summarize", "calls": 80, "denials": 6, "denialRate": 0.075, "topReason": "insufficient_credits" }],
2632
+ "hourlyTrends": [{ "hour": "2025-01-15T14", "calls": 20, "denials": 3 }],
2633
+ "mostDenied": [{ "name": "heavy-user", "denials": 8, "calls": 50, "denialRate": 0.16, "topReason": "rate_limited" }]
2634
+ }
2635
+ ```
2636
+
2637
+ Returns denial summary with denial rate, breakdown by canonical reason type (insufficient_credits, rate_limited, tool_rate_limited, quota_exceeded, key_suspended, api_key_expired, invalid_api_key, missing_api_key, tool_not_allowed, ip_not_allowed, spending_limit_exceeded, etc.), per-key denial counts with top reason, per-tool denial counts, hourly denial trends (last 24 hours), and top 10 most denied keys. Read-only.
2638
+
2595
2639
  ### IP Allowlisting
2596
2640
 
2597
2641
  Restrict API keys to specific IP addresses or CIDR ranges:
package/dist/server.d.ts CHANGED
@@ -247,6 +247,8 @@ export declare class PayGateServer {
247
247
  private handleKeyLifecycleReport;
248
248
  private handleCostAnalysis;
249
249
  private handleRateLimitAnalysis;
250
+ private handleQuotaAnalysis;
251
+ private handleDenialAnalysis;
250
252
  private handleGetNotes;
251
253
  private handleAddNote;
252
254
  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
@@ -828,6 +828,18 @@ class PayGateServer {
828
828
  res.writeHead(405, { 'Content-Type': 'application/json' });
829
829
  res.end(JSON.stringify({ error: 'Method not allowed. Use GET.' }));
830
830
  return;
831
+ case '/admin/quotas':
832
+ if (req.method === 'GET')
833
+ return this.handleQuotaAnalysis(req, res);
834
+ res.writeHead(405, { 'Content-Type': 'application/json' });
835
+ res.end(JSON.stringify({ error: 'Method not allowed. Use GET.' }));
836
+ return;
837
+ case '/admin/denials':
838
+ if (req.method === 'GET')
839
+ return this.handleDenialAnalysis(req, res);
840
+ res.writeHead(405, { 'Content-Type': 'application/json' });
841
+ res.end(JSON.stringify({ error: 'Method not allowed. Use GET.' }));
842
+ return;
831
843
  // ─── Plugin endpoints ──────────────────────────────────────────────
832
844
  case '/plugins':
833
845
  return this.handleListPlugins(req, res);
@@ -1373,6 +1385,8 @@ class PayGateServer {
1373
1385
  keyLifecycle: 'GET /admin/lifecycle — Key lifecycle report with creation/revocation/expiry trends, average lifetime, and at-risk keys (requires X-Admin-Key)',
1374
1386
  costAnalysis: 'GET /admin/costs — Cost analysis with per-tool, per-namespace breakdown, hourly trends, and top spenders (requires X-Admin-Key)',
1375
1387
  rateLimitAnalysis: 'GET /admin/rate-limits — Rate limit utilization analysis with per-key and per-tool breakdown, denial trends, and most throttled keys (requires X-Admin-Key)',
1388
+ quotaAnalysis: 'GET /admin/quotas — Quota utilization analysis with per-key and per-tool breakdown, denial trends, most constrained keys, and configuration display (requires X-Admin-Key)',
1389
+ denialAnalysis: 'GET /admin/denials — Comprehensive denial breakdown by reason type with per-key and per-tool stats, hourly trends, and most denied keys (requires X-Admin-Key)',
1376
1390
  ...(this.oauth ? {
1377
1391
  oauthMetadata: 'GET /.well-known/oauth-authorization-server — OAuth 2.1 server metadata',
1378
1392
  oauthRegister: 'POST /oauth/register — Register OAuth client',
@@ -4898,6 +4912,244 @@ class PayGateServer {
4898
4912
  mostThrottled,
4899
4913
  }));
4900
4914
  }
4915
+ // ─── /admin/quotas — Quota utilization analysis ───────────────────────────
4916
+ handleQuotaAnalysis(req, res) {
4917
+ if (!this.checkAdmin(req, res))
4918
+ return;
4919
+ const globalQuota = this.config.globalQuota || null;
4920
+ const allRecords = this.gate.store.getAllRecords();
4921
+ const events = this.gate.meter.getEvents();
4922
+ // ── Summary ──
4923
+ const totalKeys = allRecords.length;
4924
+ let keysWithQuotas = 0;
4925
+ for (const rec of allRecords) {
4926
+ if (rec.quota || globalQuota)
4927
+ keysWithQuotas++;
4928
+ }
4929
+ let totalCalls = 0;
4930
+ let totalQuotaDenials = 0;
4931
+ for (const e of events) {
4932
+ totalCalls++;
4933
+ if (!e.allowed && e.denyReason?.includes('quota_exceeded')) {
4934
+ totalQuotaDenials++;
4935
+ }
4936
+ }
4937
+ const quotaDenialRate = totalCalls > 0
4938
+ ? Math.round(totalQuotaDenials / totalCalls * 10000) / 10000
4939
+ : 0;
4940
+ // ── Per-key breakdown with current quota counters ──
4941
+ const perKey = allRecords.map(rec => {
4942
+ const quota = rec.quota || globalQuota;
4943
+ const dailyCallLimit = quota?.dailyCallLimit || 0;
4944
+ const monthlyCallLimit = quota?.monthlyCallLimit || 0;
4945
+ const dailyCreditLimit = quota?.dailyCreditLimit || 0;
4946
+ const monthlyCreditLimit = quota?.monthlyCreditLimit || 0;
4947
+ const dailyCallUtilization = dailyCallLimit > 0
4948
+ ? Math.round(rec.quotaDailyCalls / dailyCallLimit * 10000) / 10000
4949
+ : 0;
4950
+ const monthlyCallUtilization = monthlyCallLimit > 0
4951
+ ? Math.round(rec.quotaMonthlyCalls / monthlyCallLimit * 10000) / 10000
4952
+ : 0;
4953
+ return {
4954
+ name: rec.name,
4955
+ dailyCalls: rec.quotaDailyCalls,
4956
+ monthlyCalls: rec.quotaMonthlyCalls,
4957
+ dailyCredits: rec.quotaDailyCredits,
4958
+ monthlyCredits: rec.quotaMonthlyCredits,
4959
+ dailyCallLimit,
4960
+ monthlyCallLimit,
4961
+ dailyCreditLimit,
4962
+ monthlyCreditLimit,
4963
+ dailyCallUtilization,
4964
+ monthlyCallUtilization,
4965
+ source: rec.quota ? 'per-key' : (globalQuota ? 'global' : 'none'),
4966
+ };
4967
+ }).sort((a, b) => b.dailyCallUtilization - a.dailyCallUtilization);
4968
+ // ── Per-tool breakdown ──
4969
+ const toolMap = new Map();
4970
+ for (const e of events) {
4971
+ if (!toolMap.has(e.tool))
4972
+ toolMap.set(e.tool, { calls: 0, quotaDenied: 0 });
4973
+ const t = toolMap.get(e.tool);
4974
+ t.calls++;
4975
+ if (!e.allowed && e.denyReason?.includes('quota_exceeded')) {
4976
+ t.quotaDenied++;
4977
+ }
4978
+ }
4979
+ const perTool = Array.from(toolMap.entries())
4980
+ .map(([tool, stats]) => ({ tool, ...stats }))
4981
+ .sort((a, b) => b.quotaDenied - a.quotaDenied);
4982
+ // ── Hourly trends (quota denials) ──
4983
+ const hourBuckets = new Map();
4984
+ for (const e of events) {
4985
+ const hour = e.timestamp.slice(0, 13); // YYYY-MM-DDTHH
4986
+ if (!hourBuckets.has(hour))
4987
+ hourBuckets.set(hour, { calls: 0, quotaDenied: 0 });
4988
+ const h = hourBuckets.get(hour);
4989
+ h.calls++;
4990
+ if (!e.allowed && e.denyReason?.includes('quota_exceeded')) {
4991
+ h.quotaDenied++;
4992
+ }
4993
+ }
4994
+ const hourlyTrends = Array.from(hourBuckets.entries())
4995
+ .map(([hour, stats]) => ({ hour, ...stats }))
4996
+ .sort((a, b) => a.hour.localeCompare(b.hour))
4997
+ .slice(-24);
4998
+ // ── Most constrained keys (by daily call utilization) ──
4999
+ const mostConstrained = perKey
5000
+ .filter(k => k.dailyCallLimit > 0 || k.monthlyCallLimit > 0)
5001
+ .map(k => ({
5002
+ name: k.name,
5003
+ dailyCalls: k.dailyCalls,
5004
+ dailyCallLimit: k.dailyCallLimit,
5005
+ dailyCallUtilization: k.dailyCallUtilization,
5006
+ monthlyCalls: k.monthlyCalls,
5007
+ monthlyCallLimit: k.monthlyCallLimit,
5008
+ monthlyCallUtilization: k.monthlyCallUtilization,
5009
+ }))
5010
+ .sort((a, b) => b.dailyCallUtilization - a.dailyCallUtilization)
5011
+ .slice(0, 10);
5012
+ res.writeHead(200, { 'Content-Type': 'application/json' });
5013
+ res.end(JSON.stringify({
5014
+ config: { globalQuota },
5015
+ summary: { totalKeys, keysWithQuotas, totalQuotaDenials, quotaDenialRate },
5016
+ perKey,
5017
+ perTool,
5018
+ hourlyTrends,
5019
+ mostConstrained,
5020
+ }));
5021
+ }
5022
+ // ─── /admin/denials — Comprehensive denial analysis ───────────────────────
5023
+ handleDenialAnalysis(req, res) {
5024
+ if (!this.checkAdmin(req, res))
5025
+ return;
5026
+ const events = this.gate.meter.getEvents();
5027
+ // ── Categorize deny reasons into canonical types ──
5028
+ function categorize(reason) {
5029
+ if (reason.includes('rate_limited') && !reason.includes('tool_rate_limited'))
5030
+ return 'rate_limited';
5031
+ if (reason.includes('tool_rate_limited'))
5032
+ return 'tool_rate_limited';
5033
+ if (reason.includes('insufficient_credits'))
5034
+ return 'insufficient_credits';
5035
+ if (reason.includes('key_suspended'))
5036
+ return 'key_suspended';
5037
+ if (reason.includes('api_key_expired'))
5038
+ return 'api_key_expired';
5039
+ if (reason.includes('invalid_api_key'))
5040
+ return 'invalid_api_key';
5041
+ if (reason.includes('missing_api_key'))
5042
+ return 'missing_api_key';
5043
+ if (reason.includes('tool_not_allowed') && !reason.includes('token_tool_not_allowed'))
5044
+ return 'tool_not_allowed';
5045
+ if (reason.includes('token_tool_not_allowed'))
5046
+ return 'token_tool_not_allowed';
5047
+ if (reason.includes('ip_not_allowed'))
5048
+ return 'ip_not_allowed';
5049
+ if (reason.includes('spending_limit_exceeded'))
5050
+ return 'spending_limit_exceeded';
5051
+ if (reason.includes('quota_exceeded'))
5052
+ return 'quota_exceeded';
5053
+ if (reason.includes('team_budget'))
5054
+ return 'team_budget_exceeded';
5055
+ return 'other';
5056
+ }
5057
+ // ── Summary ──
5058
+ let totalCalls = 0;
5059
+ let totalDenials = 0;
5060
+ const byReason = {};
5061
+ for (const e of events) {
5062
+ totalCalls++;
5063
+ if (!e.allowed && e.denyReason) {
5064
+ totalDenials++;
5065
+ const cat = categorize(e.denyReason);
5066
+ byReason[cat] = (byReason[cat] || 0) + 1;
5067
+ }
5068
+ }
5069
+ const denialRate = totalCalls > 0
5070
+ ? Math.round(totalDenials / totalCalls * 10000) / 10000
5071
+ : 0;
5072
+ // ── Per-key breakdown ──
5073
+ const keyMap = new Map();
5074
+ for (const e of events) {
5075
+ const name = e.keyName || e.apiKey.slice(0, 10);
5076
+ if (!keyMap.has(name))
5077
+ keyMap.set(name, { name, calls: 0, denials: 0, reasons: {} });
5078
+ const k = keyMap.get(name);
5079
+ k.calls++;
5080
+ if (!e.allowed && e.denyReason) {
5081
+ k.denials++;
5082
+ const cat = categorize(e.denyReason);
5083
+ k.reasons[cat] = (k.reasons[cat] || 0) + 1;
5084
+ }
5085
+ }
5086
+ const perKey = Array.from(keyMap.values()).map(k => ({
5087
+ name: k.name,
5088
+ calls: k.calls,
5089
+ denials: k.denials,
5090
+ denialRate: k.calls > 0 ? Math.round(k.denials / k.calls * 10000) / 10000 : 0,
5091
+ topReason: Object.entries(k.reasons).sort((a, b) => b[1] - a[1])[0]?.[0] || null,
5092
+ })).sort((a, b) => b.denials - a.denials);
5093
+ // ── Per-tool breakdown ──
5094
+ const toolMap = new Map();
5095
+ for (const e of events) {
5096
+ if (!toolMap.has(e.tool))
5097
+ toolMap.set(e.tool, { calls: 0, denials: 0, reasons: {} });
5098
+ const t = toolMap.get(e.tool);
5099
+ t.calls++;
5100
+ if (!e.allowed && e.denyReason) {
5101
+ t.denials++;
5102
+ const cat = categorize(e.denyReason);
5103
+ t.reasons[cat] = (t.reasons[cat] || 0) + 1;
5104
+ }
5105
+ }
5106
+ const perTool = Array.from(toolMap.entries())
5107
+ .map(([tool, stats]) => ({
5108
+ tool,
5109
+ calls: stats.calls,
5110
+ denials: stats.denials,
5111
+ denialRate: stats.calls > 0 ? Math.round(stats.denials / stats.calls * 10000) / 10000 : 0,
5112
+ topReason: Object.entries(stats.reasons).sort((a, b) => b[1] - a[1])[0]?.[0] || null,
5113
+ }))
5114
+ .sort((a, b) => b.denials - a.denials);
5115
+ // ── Hourly trends ──
5116
+ const hourBuckets = new Map();
5117
+ for (const e of events) {
5118
+ const hour = e.timestamp.slice(0, 13);
5119
+ if (!hourBuckets.has(hour))
5120
+ hourBuckets.set(hour, { calls: 0, denials: 0 });
5121
+ const h = hourBuckets.get(hour);
5122
+ h.calls++;
5123
+ if (!e.allowed && e.denyReason) {
5124
+ h.denials++;
5125
+ }
5126
+ }
5127
+ const hourlyTrends = Array.from(hourBuckets.entries())
5128
+ .map(([hour, stats]) => ({ hour, ...stats }))
5129
+ .sort((a, b) => a.hour.localeCompare(b.hour))
5130
+ .slice(-24);
5131
+ // ── Most denied keys ──
5132
+ const mostDenied = Array.from(keyMap.values())
5133
+ .filter(k => k.denials > 0)
5134
+ .map(k => ({
5135
+ name: k.name,
5136
+ denials: k.denials,
5137
+ calls: k.calls,
5138
+ denialRate: k.calls > 0 ? Math.round(k.denials / k.calls * 10000) / 10000 : 0,
5139
+ topReason: Object.entries(k.reasons).sort((a, b) => b[1] - a[1])[0]?.[0] || null,
5140
+ }))
5141
+ .sort((a, b) => b.denials - a.denials)
5142
+ .slice(0, 10);
5143
+ res.writeHead(200, { 'Content-Type': 'application/json' });
5144
+ res.end(JSON.stringify({
5145
+ summary: { totalCalls, totalDenials, denialRate },
5146
+ byReason,
5147
+ perKey,
5148
+ perTool,
5149
+ hourlyTrends,
5150
+ mostDenied,
5151
+ }));
5152
+ }
4901
5153
  // ─── /keys/notes — Timestamped notes on API keys ─────────────────────────
4902
5154
  handleGetNotes(req, res) {
4903
5155
  if (!this.checkAdmin(req, res))