paygate-mcp 7.7.0 → 7.9.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
@@ -104,6 +104,8 @@ Agent → PayGate (auth + billing) → Your MCP Server (stdio or HTTP)
104
104
  - **Tool Stats** — `GET /tools/stats` per-tool analytics: call counts, success rate, avg/p95 latency, credits consumed, deny reason breakdown, top 10 consumers — optional `?tool=` for detailed single-tool view, `?since=` filter
105
105
  - **Request Log Export** — `GET /requests/export` exports the full request log as JSON or CSV with Content-Disposition headers — filter by key/tool/status/since/until, combined time-window queries, no pagination limit
106
106
  - **Tool Call Dry Run** — `POST /requests/dry-run` simulates a tool call without executing — checks key validity, ACL, rate limits, credits, and spending limits, returns predicted outcome with credits-after calculation and rate limit status
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
+ - **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
107
109
  - **Config Hot Reload** — `POST /config/reload` reloads pricing, rate limits, webhooks, quotas, and behavior flags from config file without server restart
108
110
  - **Webhook Events** — POST batched usage events to any URL for external billing/alerting
109
111
  - **Config File Mode** — Load all settings from a JSON file (`--config`)
@@ -2273,6 +2275,61 @@ curl -X POST http://localhost:3402/requests/dry-run \
2273
2275
 
2274
2276
  Checks key validity, suspension, tool ACL, rate limits, credit balance, and spending limits. Supports alias keys. Useful for agents that want to pre-flight check a call before committing.
2275
2277
 
2278
+ ### Batch Dry Run
2279
+
2280
+ Simulate multiple tool calls at once to check if an entire batch would succeed:
2281
+
2282
+ ```bash
2283
+ curl -X POST http://localhost:3402/requests/dry-run/batch \
2284
+ -H "X-Admin-Key: YOUR_ADMIN_KEY" \
2285
+ -d '{"key": "pg_...", "tools": [{"name": "tool_a"}, {"name": "tool_b"}]}'
2286
+ ```
2287
+
2288
+ **Response:**
2289
+
2290
+ ```json
2291
+ {
2292
+ "allAllowed": true,
2293
+ "totalCreditsRequired": 10,
2294
+ "creditsAvailable": 100,
2295
+ "creditsAfter": 90,
2296
+ "results": [
2297
+ { "tool": "tool_a", "allowed": true, "creditsRequired": 5 },
2298
+ { "tool": "tool_b", "allowed": true, "creditsRequired": 5 }
2299
+ ]
2300
+ }
2301
+ ```
2302
+
2303
+ Performs aggregate credit check (sum of all tool prices vs balance), per-tool ACL validation, spending limit, and rate limit checks. Returns per-tool results so you can see which specific tools would fail. Max 100 tools per batch. Supports alias keys.
2304
+
2305
+ ### Tool Availability
2306
+
2307
+ Check per-key tool availability including pricing, affordability, and rate limit status:
2308
+
2309
+ ```bash
2310
+ curl "http://localhost:3402/tools/available?key=pg_..." \
2311
+ -H "X-Admin-Key: YOUR_ADMIN_KEY"
2312
+ ```
2313
+
2314
+ **Response:**
2315
+
2316
+ ```json
2317
+ {
2318
+ "key": "pg_c815...09a6",
2319
+ "creditsAvailable": 100,
2320
+ "totalTools": 3,
2321
+ "accessibleTools": 2,
2322
+ "globalRateLimit": { "limit": 60, "used": 5, "remaining": 55, "resetInMs": 45000 },
2323
+ "tools": [
2324
+ { "tool": "tool_a", "accessible": true, "creditsPerCall": 10, "canAfford": true },
2325
+ { "tool": "tool_b", "accessible": false, "denyReason": "denied_by_acl", "creditsPerCall": 5, "canAfford": true },
2326
+ { "tool": "tool_c", "accessible": true, "creditsPerCall": 1, "canAfford": true, "rateLimit": { "limit": 10, "used": 3, "remaining": 7 } }
2327
+ ]
2328
+ }
2329
+ ```
2330
+
2331
+ Returns every discovered tool with: `accessible` (ACL check), `denyReason` (if blocked), `creditsPerCall`, `canAfford` (credits vs price), and per-tool `rateLimit` when configured. Includes global rate limit info. Supports alias keys. Works on suspended keys (informational). Read-only — does not deduct credits or increment rate counters.
2332
+
2276
2333
  ### IP Allowlisting
2277
2334
 
2278
2335
  Restrict API keys to specific IP addresses or CIDR ranges:
package/dist/server.d.ts CHANGED
@@ -317,7 +317,9 @@ export declare class PayGateServer {
317
317
  private handleRequestLog;
318
318
  private handleToolStats;
319
319
  private handleRequestDryRun;
320
+ private handleRequestDryRunBatch;
320
321
  private handleRequestLogExport;
322
+ private handleToolAvailability;
321
323
  /** Calculate percentile from an array of numbers. */
322
324
  private percentile;
323
325
  }
@@ -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
@@ -690,6 +690,12 @@ class PayGateServer {
690
690
  res.writeHead(405, { 'Content-Type': 'application/json' });
691
691
  res.end(JSON.stringify({ error: 'Method not allowed. Use POST.' }));
692
692
  return;
693
+ case '/requests/dry-run/batch':
694
+ if (req.method === 'POST')
695
+ return this.handleRequestDryRunBatch(req, res);
696
+ res.writeHead(405, { 'Content-Type': 'application/json' });
697
+ res.end(JSON.stringify({ error: 'Method not allowed. Use POST.' }));
698
+ return;
693
699
  // ─── Registry / Discovery endpoints ──────────────────────────────
694
700
  case '/.well-known/mcp-payment':
695
701
  return this.handlePaymentMetadata(req, res);
@@ -705,6 +711,12 @@ class PayGateServer {
705
711
  res.writeHead(405, { 'Content-Type': 'application/json' });
706
712
  res.end(JSON.stringify({ error: 'Method not allowed. Use GET.' }));
707
713
  return;
714
+ case '/tools/available':
715
+ if (req.method === 'GET')
716
+ return this.handleToolAvailability(req, res);
717
+ res.writeHead(405, { 'Content-Type': 'application/json' });
718
+ res.end(JSON.stringify({ error: 'Method not allowed. Use GET.' }));
719
+ return;
708
720
  case '/alerts':
709
721
  if (req.method === 'GET')
710
722
  return this.handleGetAlerts(req, res);
@@ -1316,7 +1328,9 @@ class PayGateServer {
1316
1328
  requestLog: 'GET /requests — Queryable log of tool call requests with timing, credits, status (requires X-Admin-Key)',
1317
1329
  requestLogExport: 'GET /requests/export — Export request log as JSON or CSV with filters (requires X-Admin-Key)',
1318
1330
  requestDryRun: 'POST /requests/dry-run — Simulate a tool call without executing (requires X-Admin-Key)',
1331
+ requestDryRunBatch: 'POST /requests/dry-run/batch — Simulate multiple tool calls without executing (requires X-Admin-Key)',
1319
1332
  toolStats: 'GET /tools/stats — Per-tool call counts, success rates, latency, credits, and top consumers (requires X-Admin-Key)',
1333
+ toolAvailability: 'GET /tools/available?key=... — Per-key tool availability with pricing, affordability, and rate limit status (requires X-Admin-Key)',
1320
1334
  ...(this.oauth ? {
1321
1335
  oauthMetadata: 'GET /.well-known/oauth-authorization-server — OAuth 2.1 server metadata',
1322
1336
  oauthRegister: 'POST /oauth/register — Register OAuth client',
@@ -6591,6 +6605,142 @@ class PayGateServer {
6591
6605
  res.end(JSON.stringify({ error: 'Failed to read request body' }));
6592
6606
  }
6593
6607
  }
6608
+ // ─── /requests/dry-run/batch — Simulate multiple tool calls without executing ──
6609
+ async handleRequestDryRunBatch(req, res) {
6610
+ if (!this.checkAdmin(req, res))
6611
+ return;
6612
+ try {
6613
+ const raw = await this.readBody(req);
6614
+ try {
6615
+ const params = JSON.parse(raw);
6616
+ const apiKey = params.key;
6617
+ const tools = params.tools;
6618
+ if (!apiKey || typeof apiKey !== 'string') {
6619
+ res.writeHead(400, { 'Content-Type': 'application/json' });
6620
+ res.end(JSON.stringify({ error: 'Missing required field: key' }));
6621
+ return;
6622
+ }
6623
+ if (!Array.isArray(tools) || tools.length === 0) {
6624
+ res.writeHead(400, { 'Content-Type': 'application/json' });
6625
+ res.end(JSON.stringify({ error: 'Missing required field: tools (non-empty array of {name} objects)' }));
6626
+ return;
6627
+ }
6628
+ if (tools.length > 100) {
6629
+ res.writeHead(400, { 'Content-Type': 'application/json' });
6630
+ res.end(JSON.stringify({ error: 'Maximum 100 tools per batch dry run' }));
6631
+ return;
6632
+ }
6633
+ // Validate tool entries
6634
+ for (let i = 0; i < tools.length; i++) {
6635
+ if (!tools[i]?.name || typeof tools[i].name !== 'string') {
6636
+ res.writeHead(400, { 'Content-Type': 'application/json' });
6637
+ res.end(JSON.stringify({ error: `tools[${i}] missing required "name" field` }));
6638
+ return;
6639
+ }
6640
+ }
6641
+ // Step 1: Key lookup (resolveKeyRaw handles aliases)
6642
+ const keyRecord = this.gate.store.resolveKeyRaw(apiKey);
6643
+ if (!keyRecord) {
6644
+ const isExpired = this.gate.store.isExpired(apiKey);
6645
+ const reason = isExpired ? 'api_key_expired' : 'invalid_api_key';
6646
+ res.writeHead(200, { 'Content-Type': 'application/json' });
6647
+ res.end(JSON.stringify({
6648
+ allAllowed: false,
6649
+ reason,
6650
+ totalCreditsRequired: 0,
6651
+ results: tools.map((t) => ({ tool: t.name, allowed: false, reason, creditsRequired: 0 })),
6652
+ }));
6653
+ return;
6654
+ }
6655
+ // Step 2: Suspended?
6656
+ if (keyRecord.suspended) {
6657
+ res.writeHead(200, { 'Content-Type': 'application/json' });
6658
+ res.end(JSON.stringify({
6659
+ allAllowed: false,
6660
+ reason: 'key_suspended',
6661
+ totalCreditsRequired: 0,
6662
+ creditsAvailable: keyRecord.credits,
6663
+ results: tools.map((t) => ({ tool: t.name, allowed: false, reason: 'key_suspended', creditsRequired: 0 })),
6664
+ }));
6665
+ return;
6666
+ }
6667
+ // Step 3: Rate limit check (read-only)
6668
+ const rateStatus = this.gate.rateLimiter.getStatus(keyRecord.key);
6669
+ if (rateStatus.limit > 0 && rateStatus.remaining <= 0) {
6670
+ res.writeHead(200, { 'Content-Type': 'application/json' });
6671
+ res.end(JSON.stringify({
6672
+ allAllowed: false,
6673
+ reason: 'rate_limited',
6674
+ totalCreditsRequired: 0,
6675
+ creditsAvailable: keyRecord.credits,
6676
+ rateLimit: rateStatus,
6677
+ results: tools.map((t) => ({ tool: t.name, allowed: false, reason: 'rate_limited', creditsRequired: 0 })),
6678
+ }));
6679
+ return;
6680
+ }
6681
+ // Step 4: Per-tool checks
6682
+ const results = [];
6683
+ let totalCreditsRequired = 0;
6684
+ let allAllowed = true;
6685
+ let firstDenyReason;
6686
+ for (const toolEntry of tools) {
6687
+ const toolName = toolEntry.name;
6688
+ const toolArgs = toolEntry.arguments;
6689
+ // ACL check
6690
+ const effectiveAllowed = keyRecord.allowedTools || [];
6691
+ const effectiveDenied = keyRecord.deniedTools || [];
6692
+ if (effectiveDenied.includes(toolName)) {
6693
+ results.push({ tool: toolName, allowed: false, reason: `tool_not_allowed: ${toolName} is in deniedTools`, creditsRequired: 0 });
6694
+ allAllowed = false;
6695
+ if (!firstDenyReason)
6696
+ firstDenyReason = `tool_not_allowed: ${toolName}`;
6697
+ continue;
6698
+ }
6699
+ if (effectiveAllowed.length > 0 && !effectiveAllowed.includes(toolName)) {
6700
+ results.push({ tool: toolName, allowed: false, reason: `tool_not_allowed: ${toolName} not in allowedTools`, creditsRequired: 0 });
6701
+ allAllowed = false;
6702
+ if (!firstDenyReason)
6703
+ firstDenyReason = `tool_not_allowed: ${toolName}`;
6704
+ continue;
6705
+ }
6706
+ const creditsRequired = this.gate.getToolPrice(toolName, toolArgs);
6707
+ totalCreditsRequired += creditsRequired;
6708
+ results.push({ tool: toolName, allowed: true, creditsRequired });
6709
+ }
6710
+ // Step 5: Aggregate credits check
6711
+ if (allAllowed && keyRecord.credits < totalCreditsRequired) {
6712
+ allAllowed = false;
6713
+ firstDenyReason = `insufficient_credits: need ${totalCreditsRequired}, have ${keyRecord.credits}`;
6714
+ }
6715
+ // Step 6: Spending limit
6716
+ if (allAllowed && keyRecord.spendingLimit > 0) {
6717
+ const wouldSpend = keyRecord.totalSpent + totalCreditsRequired;
6718
+ if (wouldSpend > keyRecord.spendingLimit) {
6719
+ allAllowed = false;
6720
+ firstDenyReason = `spending_limit_exceeded: limit ${keyRecord.spendingLimit}, spent ${keyRecord.totalSpent}, need ${totalCreditsRequired}`;
6721
+ }
6722
+ }
6723
+ res.writeHead(200, { 'Content-Type': 'application/json' });
6724
+ res.end(JSON.stringify({
6725
+ allAllowed,
6726
+ ...(firstDenyReason ? { reason: firstDenyReason } : {}),
6727
+ totalCreditsRequired,
6728
+ creditsAvailable: keyRecord.credits,
6729
+ ...(allAllowed ? { creditsAfter: keyRecord.credits - totalCreditsRequired } : {}),
6730
+ ...(rateStatus.limit > 0 ? { rateLimit: rateStatus } : {}),
6731
+ results,
6732
+ }));
6733
+ }
6734
+ catch {
6735
+ res.writeHead(400, { 'Content-Type': 'application/json' });
6736
+ res.end(JSON.stringify({ error: 'Invalid JSON body' }));
6737
+ }
6738
+ }
6739
+ catch {
6740
+ res.writeHead(400, { 'Content-Type': 'application/json' });
6741
+ res.end(JSON.stringify({ error: 'Failed to read request body' }));
6742
+ }
6743
+ }
6594
6744
  // ─── /requests/export — Export request log as JSON or CSV ───────────────────
6595
6745
  handleRequestLogExport(req, res) {
6596
6746
  if (!this.checkAdmin(req, res))
@@ -6672,6 +6822,73 @@ class PayGateServer {
6672
6822
  res.end(JSON.stringify({ count: sorted.length, requests: sorted }, null, 2));
6673
6823
  }
6674
6824
  }
6825
+ // ─── /tools/available — Per-key tool availability with pricing ──────────────
6826
+ handleToolAvailability(req, res) {
6827
+ if (!this.checkAdmin(req, res))
6828
+ return;
6829
+ const urlParts = req.url?.split('?') || [];
6830
+ const params = new URLSearchParams(urlParts[1] || '');
6831
+ const keyParam = params.get('key');
6832
+ if (!keyParam) {
6833
+ res.writeHead(400, { 'Content-Type': 'application/json' });
6834
+ res.end(JSON.stringify({ error: 'Missing required parameter: key' }));
6835
+ return;
6836
+ }
6837
+ // Resolve alias
6838
+ const keyRecord = this.gate.store.resolveKeyRaw(keyParam);
6839
+ if (!keyRecord) {
6840
+ const isExpired = this.gate.store.isExpired(keyParam);
6841
+ const reason = isExpired ? 'api_key_expired' : 'invalid_api_key';
6842
+ res.writeHead(200, { 'Content-Type': 'application/json' });
6843
+ res.end(JSON.stringify({ error: reason, tools: [] }));
6844
+ return;
6845
+ }
6846
+ // Get all discovered tools with pricing
6847
+ const allToolPricing = this.registry.getFullPricing().tools;
6848
+ // Build per-tool availability
6849
+ const effectiveAllowed = keyRecord.allowedTools || [];
6850
+ const effectiveDenied = keyRecord.deniedTools || [];
6851
+ const tools = allToolPricing.map(toolInfo => {
6852
+ const toolName = toolInfo.name;
6853
+ // ACL check
6854
+ let accessible = true;
6855
+ let denyReason;
6856
+ if (effectiveDenied.includes(toolName)) {
6857
+ accessible = false;
6858
+ denyReason = 'denied_by_acl';
6859
+ }
6860
+ else if (effectiveAllowed.length > 0 && !effectiveAllowed.includes(toolName)) {
6861
+ accessible = false;
6862
+ denyReason = 'not_in_allowed_list';
6863
+ }
6864
+ // Affordability
6865
+ const creditsRequired = toolInfo.creditsPerCall;
6866
+ const canAfford = keyRecord.credits >= creditsRequired;
6867
+ // Per-tool rate limit status
6868
+ const perToolRateLimit = toolInfo.rateLimitPerMin > 0
6869
+ ? this.gate.rateLimiter.getStatus(`${keyRecord.key}:tool:${toolName}`, toolInfo.rateLimitPerMin)
6870
+ : null;
6871
+ return {
6872
+ tool: toolName,
6873
+ accessible,
6874
+ ...(denyReason ? { denyReason } : {}),
6875
+ creditsPerCall: creditsRequired,
6876
+ canAfford,
6877
+ ...(perToolRateLimit ? { rateLimit: perToolRateLimit } : {}),
6878
+ };
6879
+ });
6880
+ // Global rate limit
6881
+ const globalRateLimit = this.gate.rateLimiter.getStatus(keyRecord.key);
6882
+ res.writeHead(200, { 'Content-Type': 'application/json' });
6883
+ res.end(JSON.stringify({
6884
+ key: keyRecord.key.slice(0, 7) + '...' + keyRecord.key.slice(-4),
6885
+ creditsAvailable: keyRecord.credits,
6886
+ totalTools: tools.length,
6887
+ accessibleTools: tools.filter(t => t.accessible).length,
6888
+ ...(globalRateLimit.limit > 0 ? { globalRateLimit } : {}),
6889
+ tools,
6890
+ }));
6891
+ }
6675
6892
  /** Calculate percentile from an array of numbers. */
6676
6893
  percentile(values, p) {
6677
6894
  if (values.length === 0)