paygate-mcp 7.6.0 → 7.8.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
@@ -103,6 +103,8 @@ Agent → PayGate (auth + billing) → Your MCP Server (stdio or HTTP)
103
103
  - **Request Log** — `GET /requests` queryable log of every tool call with timing, credits charged, status (allowed/denied), deny reason, key, and request ID — filter by key/tool/status/since, pagination, summary statistics (totals + avg duration), 5000-entry ring buffer
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
+ - **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
106
108
  - **Config Hot Reload** — `POST /config/reload` reloads pricing, rate limits, webhooks, quotas, and behavior flags from config file without server restart
107
109
  - **Webhook Events** — POST batched usage events to any URL for external billing/alerting
108
110
  - **Config File Mode** — Load all settings from a JSON file (`--config`)
@@ -2235,6 +2237,70 @@ curl "http://localhost:3402/requests/export?tool=my_tool&status=denied&since=202
2235
2237
 
2236
2238
  Both formats include Content-Disposition headers for automatic file download. Unlike `/requests`, the export endpoint returns **all** matching entries (no pagination limit). CSV includes proper quoting for values with commas.
2237
2239
 
2240
+ ### Tool Call Dry Run
2241
+
2242
+ Simulate a tool call to check if it would be allowed — without deducting credits or incrementing rate limits:
2243
+
2244
+ ```bash
2245
+ curl -X POST http://localhost:3402/requests/dry-run \
2246
+ -H "X-Admin-Key: YOUR_ADMIN_KEY" \
2247
+ -d '{"key": "pg_...", "tool": "my_tool"}'
2248
+ ```
2249
+
2250
+ **Response (allowed):**
2251
+
2252
+ ```json
2253
+ {
2254
+ "allowed": true,
2255
+ "tool": "my_tool",
2256
+ "creditsRequired": 5,
2257
+ "creditsAvailable": 100,
2258
+ "creditsAfter": 95,
2259
+ "rateLimit": { "used": 3, "limit": 60, "remaining": 57, "resetInMs": 45000 }
2260
+ }
2261
+ ```
2262
+
2263
+ **Response (denied):**
2264
+
2265
+ ```json
2266
+ {
2267
+ "allowed": false,
2268
+ "reason": "insufficient_credits: need 5, have 2",
2269
+ "tool": "my_tool",
2270
+ "creditsRequired": 5,
2271
+ "creditsAvailable": 2
2272
+ }
2273
+ ```
2274
+
2275
+ 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.
2276
+
2277
+ ### Batch Dry Run
2278
+
2279
+ Simulate multiple tool calls at once to check if an entire batch would succeed:
2280
+
2281
+ ```bash
2282
+ curl -X POST http://localhost:3402/requests/dry-run/batch \
2283
+ -H "X-Admin-Key: YOUR_ADMIN_KEY" \
2284
+ -d '{"key": "pg_...", "tools": [{"name": "tool_a"}, {"name": "tool_b"}]}'
2285
+ ```
2286
+
2287
+ **Response:**
2288
+
2289
+ ```json
2290
+ {
2291
+ "allAllowed": true,
2292
+ "totalCreditsRequired": 10,
2293
+ "creditsAvailable": 100,
2294
+ "creditsAfter": 90,
2295
+ "results": [
2296
+ { "tool": "tool_a", "allowed": true, "creditsRequired": 5 },
2297
+ { "tool": "tool_b", "allowed": true, "creditsRequired": 5 }
2298
+ ]
2299
+ }
2300
+ ```
2301
+
2302
+ 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.
2303
+
2238
2304
  ### IP Allowlisting
2239
2305
 
2240
2306
  Restrict API keys to specific IP addresses or CIDR ranges:
package/dist/server.d.ts CHANGED
@@ -316,6 +316,8 @@ export declare class PayGateServer {
316
316
  gracefulStop(timeoutMs?: number): Promise<void>;
317
317
  private handleRequestLog;
318
318
  private handleToolStats;
319
+ private handleRequestDryRun;
320
+ private handleRequestDryRunBatch;
319
321
  private handleRequestLogExport;
320
322
  /** Calculate percentile from an array of numbers. */
321
323
  private percentile;
@@ -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
@@ -684,6 +684,18 @@ class PayGateServer {
684
684
  res.writeHead(405, { 'Content-Type': 'application/json' });
685
685
  res.end(JSON.stringify({ error: 'Method not allowed. Use GET.' }));
686
686
  return;
687
+ case '/requests/dry-run':
688
+ if (req.method === 'POST')
689
+ return this.handleRequestDryRun(req, res);
690
+ res.writeHead(405, { 'Content-Type': 'application/json' });
691
+ res.end(JSON.stringify({ error: 'Method not allowed. Use POST.' }));
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;
687
699
  // ─── Registry / Discovery endpoints ──────────────────────────────
688
700
  case '/.well-known/mcp-payment':
689
701
  return this.handlePaymentMetadata(req, res);
@@ -1309,6 +1321,8 @@ class PayGateServer {
1309
1321
  creditReservations: 'POST /keys/reserve to hold credits, POST /keys/reserve/commit to deduct, POST /keys/reserve/release to release, GET /keys/reserve to list (requires X-Admin-Key)',
1310
1322
  requestLog: 'GET /requests — Queryable log of tool call requests with timing, credits, status (requires X-Admin-Key)',
1311
1323
  requestLogExport: 'GET /requests/export — Export request log as JSON or CSV with filters (requires X-Admin-Key)',
1324
+ requestDryRun: 'POST /requests/dry-run — Simulate a tool call without executing (requires X-Admin-Key)',
1325
+ requestDryRunBatch: 'POST /requests/dry-run/batch — Simulate multiple tool calls without executing (requires X-Admin-Key)',
1312
1326
  toolStats: 'GET /tools/stats — Per-tool call counts, success rates, latency, credits, and top consumers (requires X-Admin-Key)',
1313
1327
  ...(this.oauth ? {
1314
1328
  oauthMetadata: 'GET /.well-known/oauth-authorization-server — OAuth 2.1 server metadata',
@@ -6449,6 +6463,277 @@ class PayGateServer {
6449
6463
  tools,
6450
6464
  }));
6451
6465
  }
6466
+ // ─── /requests/dry-run — Simulate a tool call without executing ─────────────
6467
+ async handleRequestDryRun(req, res) {
6468
+ if (!this.checkAdmin(req, res))
6469
+ return;
6470
+ try {
6471
+ const raw = await this.readBody(req);
6472
+ try {
6473
+ const params = JSON.parse(raw);
6474
+ const apiKey = params.key;
6475
+ const toolName = params.tool;
6476
+ if (!apiKey || typeof apiKey !== 'string') {
6477
+ res.writeHead(400, { 'Content-Type': 'application/json' });
6478
+ res.end(JSON.stringify({ error: 'Missing required field: key' }));
6479
+ return;
6480
+ }
6481
+ if (!toolName || typeof toolName !== 'string') {
6482
+ res.writeHead(400, { 'Content-Type': 'application/json' });
6483
+ res.end(JSON.stringify({ error: 'Missing required field: tool' }));
6484
+ return;
6485
+ }
6486
+ // Step 1: Key lookup (resolveKeyRaw handles alias resolution)
6487
+ const keyRecord = this.gate.store.resolveKeyRaw(apiKey);
6488
+ if (!keyRecord) {
6489
+ const isExpired = this.gate.store.isExpired(apiKey);
6490
+ const reason = isExpired ? 'api_key_expired' : 'invalid_api_key';
6491
+ res.writeHead(200, { 'Content-Type': 'application/json' });
6492
+ res.end(JSON.stringify({
6493
+ allowed: false,
6494
+ reason,
6495
+ tool: toolName,
6496
+ creditsRequired: 0,
6497
+ creditsAvailable: 0,
6498
+ }));
6499
+ return;
6500
+ }
6501
+ // Step 2: Suspended?
6502
+ if (keyRecord.suspended) {
6503
+ res.writeHead(200, { 'Content-Type': 'application/json' });
6504
+ res.end(JSON.stringify({
6505
+ allowed: false,
6506
+ reason: 'key_suspended',
6507
+ tool: toolName,
6508
+ creditsRequired: 0,
6509
+ creditsAvailable: keyRecord.credits,
6510
+ }));
6511
+ return;
6512
+ }
6513
+ // Step 3: Tool ACL
6514
+ const effectiveAllowed = keyRecord.allowedTools || [];
6515
+ const effectiveDenied = keyRecord.deniedTools || [];
6516
+ if (effectiveDenied.includes(toolName)) {
6517
+ res.writeHead(200, { 'Content-Type': 'application/json' });
6518
+ res.end(JSON.stringify({
6519
+ allowed: false,
6520
+ reason: `tool_not_allowed: ${toolName} is in deniedTools`,
6521
+ tool: toolName,
6522
+ creditsRequired: 0,
6523
+ creditsAvailable: keyRecord.credits,
6524
+ }));
6525
+ return;
6526
+ }
6527
+ if (effectiveAllowed.length > 0 && !effectiveAllowed.includes(toolName)) {
6528
+ res.writeHead(200, { 'Content-Type': 'application/json' });
6529
+ res.end(JSON.stringify({
6530
+ allowed: false,
6531
+ reason: `tool_not_allowed: ${toolName} not in allowedTools`,
6532
+ tool: toolName,
6533
+ creditsRequired: 0,
6534
+ creditsAvailable: keyRecord.credits,
6535
+ }));
6536
+ return;
6537
+ }
6538
+ // Step 4: Rate limit check (read-only)
6539
+ const rateStatus = this.gate.rateLimiter.getStatus(keyRecord.key);
6540
+ if (rateStatus.limit > 0 && rateStatus.remaining <= 0) {
6541
+ res.writeHead(200, { 'Content-Type': 'application/json' });
6542
+ res.end(JSON.stringify({
6543
+ allowed: false,
6544
+ reason: 'rate_limited',
6545
+ tool: toolName,
6546
+ creditsRequired: 0,
6547
+ creditsAvailable: keyRecord.credits,
6548
+ rateLimit: rateStatus,
6549
+ }));
6550
+ return;
6551
+ }
6552
+ // Step 5: Credits check
6553
+ const creditsRequired = this.gate.getToolPrice(toolName, params.arguments);
6554
+ if (keyRecord.credits < creditsRequired) {
6555
+ res.writeHead(200, { 'Content-Type': 'application/json' });
6556
+ res.end(JSON.stringify({
6557
+ allowed: false,
6558
+ reason: `insufficient_credits: need ${creditsRequired}, have ${keyRecord.credits}`,
6559
+ tool: toolName,
6560
+ creditsRequired,
6561
+ creditsAvailable: keyRecord.credits,
6562
+ }));
6563
+ return;
6564
+ }
6565
+ // Step 6: Spending limit
6566
+ if (keyRecord.spendingLimit > 0) {
6567
+ const wouldSpend = keyRecord.totalSpent + creditsRequired;
6568
+ if (wouldSpend > keyRecord.spendingLimit) {
6569
+ res.writeHead(200, { 'Content-Type': 'application/json' });
6570
+ res.end(JSON.stringify({
6571
+ allowed: false,
6572
+ reason: `spending_limit_exceeded: limit ${keyRecord.spendingLimit}, spent ${keyRecord.totalSpent}, need ${creditsRequired}`,
6573
+ tool: toolName,
6574
+ creditsRequired,
6575
+ creditsAvailable: keyRecord.credits,
6576
+ }));
6577
+ return;
6578
+ }
6579
+ }
6580
+ // All checks passed — would be allowed
6581
+ res.writeHead(200, { 'Content-Type': 'application/json' });
6582
+ res.end(JSON.stringify({
6583
+ allowed: true,
6584
+ tool: toolName,
6585
+ creditsRequired,
6586
+ creditsAvailable: keyRecord.credits,
6587
+ creditsAfter: keyRecord.credits - creditsRequired,
6588
+ ...(rateStatus.limit > 0 ? { rateLimit: rateStatus } : {}),
6589
+ }));
6590
+ }
6591
+ catch {
6592
+ res.writeHead(400, { 'Content-Type': 'application/json' });
6593
+ res.end(JSON.stringify({ error: 'Invalid JSON body' }));
6594
+ }
6595
+ }
6596
+ catch {
6597
+ res.writeHead(400, { 'Content-Type': 'application/json' });
6598
+ res.end(JSON.stringify({ error: 'Failed to read request body' }));
6599
+ }
6600
+ }
6601
+ // ─── /requests/dry-run/batch — Simulate multiple tool calls without executing ──
6602
+ async handleRequestDryRunBatch(req, res) {
6603
+ if (!this.checkAdmin(req, res))
6604
+ return;
6605
+ try {
6606
+ const raw = await this.readBody(req);
6607
+ try {
6608
+ const params = JSON.parse(raw);
6609
+ const apiKey = params.key;
6610
+ const tools = params.tools;
6611
+ if (!apiKey || typeof apiKey !== 'string') {
6612
+ res.writeHead(400, { 'Content-Type': 'application/json' });
6613
+ res.end(JSON.stringify({ error: 'Missing required field: key' }));
6614
+ return;
6615
+ }
6616
+ if (!Array.isArray(tools) || tools.length === 0) {
6617
+ res.writeHead(400, { 'Content-Type': 'application/json' });
6618
+ res.end(JSON.stringify({ error: 'Missing required field: tools (non-empty array of {name} objects)' }));
6619
+ return;
6620
+ }
6621
+ if (tools.length > 100) {
6622
+ res.writeHead(400, { 'Content-Type': 'application/json' });
6623
+ res.end(JSON.stringify({ error: 'Maximum 100 tools per batch dry run' }));
6624
+ return;
6625
+ }
6626
+ // Validate tool entries
6627
+ for (let i = 0; i < tools.length; i++) {
6628
+ if (!tools[i]?.name || typeof tools[i].name !== 'string') {
6629
+ res.writeHead(400, { 'Content-Type': 'application/json' });
6630
+ res.end(JSON.stringify({ error: `tools[${i}] missing required "name" field` }));
6631
+ return;
6632
+ }
6633
+ }
6634
+ // Step 1: Key lookup (resolveKeyRaw handles aliases)
6635
+ const keyRecord = this.gate.store.resolveKeyRaw(apiKey);
6636
+ if (!keyRecord) {
6637
+ const isExpired = this.gate.store.isExpired(apiKey);
6638
+ const reason = isExpired ? 'api_key_expired' : 'invalid_api_key';
6639
+ res.writeHead(200, { 'Content-Type': 'application/json' });
6640
+ res.end(JSON.stringify({
6641
+ allAllowed: false,
6642
+ reason,
6643
+ totalCreditsRequired: 0,
6644
+ results: tools.map((t) => ({ tool: t.name, allowed: false, reason, creditsRequired: 0 })),
6645
+ }));
6646
+ return;
6647
+ }
6648
+ // Step 2: Suspended?
6649
+ if (keyRecord.suspended) {
6650
+ res.writeHead(200, { 'Content-Type': 'application/json' });
6651
+ res.end(JSON.stringify({
6652
+ allAllowed: false,
6653
+ reason: 'key_suspended',
6654
+ totalCreditsRequired: 0,
6655
+ creditsAvailable: keyRecord.credits,
6656
+ results: tools.map((t) => ({ tool: t.name, allowed: false, reason: 'key_suspended', creditsRequired: 0 })),
6657
+ }));
6658
+ return;
6659
+ }
6660
+ // Step 3: Rate limit check (read-only)
6661
+ const rateStatus = this.gate.rateLimiter.getStatus(keyRecord.key);
6662
+ if (rateStatus.limit > 0 && rateStatus.remaining <= 0) {
6663
+ res.writeHead(200, { 'Content-Type': 'application/json' });
6664
+ res.end(JSON.stringify({
6665
+ allAllowed: false,
6666
+ reason: 'rate_limited',
6667
+ totalCreditsRequired: 0,
6668
+ creditsAvailable: keyRecord.credits,
6669
+ rateLimit: rateStatus,
6670
+ results: tools.map((t) => ({ tool: t.name, allowed: false, reason: 'rate_limited', creditsRequired: 0 })),
6671
+ }));
6672
+ return;
6673
+ }
6674
+ // Step 4: Per-tool checks
6675
+ const results = [];
6676
+ let totalCreditsRequired = 0;
6677
+ let allAllowed = true;
6678
+ let firstDenyReason;
6679
+ for (const toolEntry of tools) {
6680
+ const toolName = toolEntry.name;
6681
+ const toolArgs = toolEntry.arguments;
6682
+ // ACL check
6683
+ const effectiveAllowed = keyRecord.allowedTools || [];
6684
+ const effectiveDenied = keyRecord.deniedTools || [];
6685
+ if (effectiveDenied.includes(toolName)) {
6686
+ results.push({ tool: toolName, allowed: false, reason: `tool_not_allowed: ${toolName} is in deniedTools`, creditsRequired: 0 });
6687
+ allAllowed = false;
6688
+ if (!firstDenyReason)
6689
+ firstDenyReason = `tool_not_allowed: ${toolName}`;
6690
+ continue;
6691
+ }
6692
+ if (effectiveAllowed.length > 0 && !effectiveAllowed.includes(toolName)) {
6693
+ results.push({ tool: toolName, allowed: false, reason: `tool_not_allowed: ${toolName} not in allowedTools`, creditsRequired: 0 });
6694
+ allAllowed = false;
6695
+ if (!firstDenyReason)
6696
+ firstDenyReason = `tool_not_allowed: ${toolName}`;
6697
+ continue;
6698
+ }
6699
+ const creditsRequired = this.gate.getToolPrice(toolName, toolArgs);
6700
+ totalCreditsRequired += creditsRequired;
6701
+ results.push({ tool: toolName, allowed: true, creditsRequired });
6702
+ }
6703
+ // Step 5: Aggregate credits check
6704
+ if (allAllowed && keyRecord.credits < totalCreditsRequired) {
6705
+ allAllowed = false;
6706
+ firstDenyReason = `insufficient_credits: need ${totalCreditsRequired}, have ${keyRecord.credits}`;
6707
+ }
6708
+ // Step 6: Spending limit
6709
+ if (allAllowed && keyRecord.spendingLimit > 0) {
6710
+ const wouldSpend = keyRecord.totalSpent + totalCreditsRequired;
6711
+ if (wouldSpend > keyRecord.spendingLimit) {
6712
+ allAllowed = false;
6713
+ firstDenyReason = `spending_limit_exceeded: limit ${keyRecord.spendingLimit}, spent ${keyRecord.totalSpent}, need ${totalCreditsRequired}`;
6714
+ }
6715
+ }
6716
+ res.writeHead(200, { 'Content-Type': 'application/json' });
6717
+ res.end(JSON.stringify({
6718
+ allAllowed,
6719
+ ...(firstDenyReason ? { reason: firstDenyReason } : {}),
6720
+ totalCreditsRequired,
6721
+ creditsAvailable: keyRecord.credits,
6722
+ ...(allAllowed ? { creditsAfter: keyRecord.credits - totalCreditsRequired } : {}),
6723
+ ...(rateStatus.limit > 0 ? { rateLimit: rateStatus } : {}),
6724
+ results,
6725
+ }));
6726
+ }
6727
+ catch {
6728
+ res.writeHead(400, { 'Content-Type': 'application/json' });
6729
+ res.end(JSON.stringify({ error: 'Invalid JSON body' }));
6730
+ }
6731
+ }
6732
+ catch {
6733
+ res.writeHead(400, { 'Content-Type': 'application/json' });
6734
+ res.end(JSON.stringify({ error: 'Failed to read request body' }));
6735
+ }
6736
+ }
6452
6737
  // ─── /requests/export — Export request log as JSON or CSV ───────────────────
6453
6738
  handleRequestLogExport(req, res) {
6454
6739
  if (!this.checkAdmin(req, res))