mcp-server-agentpay 1.0.4 → 1.1.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.
Files changed (3) hide show
  1. package/index.js +206 -0
  2. package/package.json +2 -2
  3. package/server.json +10 -3
package/index.js CHANGED
@@ -10,6 +10,7 @@ const { version } = require("./package.json");
10
10
  // ── Config ──────────────────────────────────────────────────────────
11
11
 
12
12
  const GATEWAY_KEY = process.env.AGENTPAY_GATEWAY_KEY || "";
13
+ const ADMIN_KEY = process.env.AGENTPAY_ADMIN_KEY || "";
13
14
  const BASE_URL = (process.env.AGENTPAY_URL || "https://agentpay.metaltorque.dev").replace(/\/$/, "");
14
15
 
15
16
  // ── HTTP helper ─────────────────────────────────────────────────────
@@ -76,6 +77,66 @@ function noKeyError() {
76
77
  };
77
78
  }
78
79
 
80
+ function noAdminKeyError() {
81
+ return {
82
+ content: [{ type: "text", text: "Error: AGENTPAY_ADMIN_KEY environment variable is required for reliability endpoints.\n\nThis is the admin key configured on the AgentPay server." }],
83
+ };
84
+ }
85
+
86
+ function adminRequest(method, urlPath, body, timeout = 30_000) {
87
+ return new Promise((resolve, reject) => {
88
+ const fullUrl = `${BASE_URL}${urlPath}`;
89
+ const parsed = new URL(fullUrl);
90
+ const isHttps = parsed.protocol === "https:";
91
+
92
+ if (!isHttps && ADMIN_KEY) {
93
+ return reject(new Error("Refusing to send admin key over insecure HTTP. Use HTTPS."));
94
+ }
95
+
96
+ const mod = isHttps ? https : http;
97
+
98
+ const headers = {
99
+ "Content-Type": "application/json",
100
+ "User-Agent": `mcp-server-agentpay/${version}`,
101
+ "Authorization": `Bearer ${ADMIN_KEY}`,
102
+ };
103
+
104
+ const opts = {
105
+ hostname: parsed.hostname,
106
+ port: parsed.port || (isHttps ? 443 : 80),
107
+ path: parsed.pathname + parsed.search,
108
+ method,
109
+ headers,
110
+ timeout,
111
+ };
112
+
113
+ const req = mod.request(opts, (res) => {
114
+ let data = "";
115
+ let size = 0;
116
+ res.on("data", (c) => {
117
+ size += c.length;
118
+ if (size > MAX_RESPONSE_SIZE) { req.destroy(); return reject(new Error("Response too large")); }
119
+ data += c;
120
+ });
121
+ res.on("end", () => {
122
+ try {
123
+ const json = JSON.parse(data);
124
+ if (res.statusCode >= 400) return reject(new Error(json.error || `HTTP ${res.statusCode}`));
125
+ resolve(json);
126
+ } catch {
127
+ if (res.statusCode >= 400) return reject(new Error(`HTTP ${res.statusCode}: ${data.slice(0, 300)}`));
128
+ resolve(data);
129
+ }
130
+ });
131
+ });
132
+
133
+ req.on("error", reject);
134
+ req.on("timeout", () => { req.destroy(); reject(new Error("Request timed out")); });
135
+ if (body) req.write(JSON.stringify(body));
136
+ req.end();
137
+ });
138
+ }
139
+
79
140
  // ── MCP Server ──────────────────────────────────────────────────────
80
141
 
81
142
  const server = new McpServer({
@@ -281,6 +342,151 @@ server.tool(
281
342
  }
282
343
  );
283
344
 
345
+ // ── Tool: reliability_dashboard ────────────────────────────────────
346
+
347
+ server.tool(
348
+ "reliability_dashboard",
349
+ "View the full reliability dashboard — circuit breaker states, health metrics (latency p50/p95/p99, success rates), and recent events for all tool backends. Requires admin key.",
350
+ {},
351
+ async () => {
352
+ if (!ADMIN_KEY) return noAdminKeyError();
353
+ try {
354
+ const result = await adminRequest("GET", "/admin/reliability");
355
+ const circuits = result.circuits || {};
356
+ const health = result.health || {};
357
+ const events = result.events || [];
358
+
359
+ let text = "=== Reliability Dashboard ===\n\n";
360
+
361
+ // Circuit states
362
+ text += "CIRCUITS:\n";
363
+ const toolIds = Object.keys(circuits);
364
+ if (toolIds.length === 0) {
365
+ text += " No circuit data yet (no tool calls made since restart)\n";
366
+ } else {
367
+ for (const id of toolIds) {
368
+ const c = circuits[id];
369
+ text += ` ${id}: ${c.state} (failures: ${c.failureCount}, successes: ${c.successCount})\n`;
370
+ }
371
+ }
372
+
373
+ // Health metrics
374
+ text += "\nHEALTH:\n";
375
+ const healthIds = Object.keys(health);
376
+ if (healthIds.length === 0) {
377
+ text += " No health data yet\n";
378
+ } else {
379
+ for (const id of healthIds) {
380
+ const h = health[id];
381
+ text += ` ${id}: ${h.successRate}% success (${h.requests.total} calls) — latency avg ${h.latency.avg}ms, p95 ${h.latency.p95}ms\n`;
382
+ }
383
+ }
384
+
385
+ // Recent events
386
+ if (events.length > 0) {
387
+ text += `\nRECENT EVENTS (last ${events.length}):\n`;
388
+ for (const e of events.slice(-10)) {
389
+ const status = e.success ? "OK" : "FAIL";
390
+ text += ` [${e.timestamp}] ${e.toolId}.${e.method} ${status} ${e.latencyMs}ms${e.error ? " — " + e.error : ""}\n`;
391
+ }
392
+ if (events.length > 10) text += ` ... and ${events.length - 10} more\n`;
393
+ }
394
+
395
+ return { content: [{ type: "text", text }] };
396
+ } catch (e) {
397
+ return { content: [{ type: "text", text: `Error: ${e.message}` }] };
398
+ }
399
+ }
400
+ );
401
+
402
+ // ── Tool: reliability_tool_detail ─────────────────────────────────
403
+
404
+ server.tool(
405
+ "reliability_tool_detail",
406
+ "Get detailed reliability info for a specific tool — circuit breaker state, config (thresholds, timeouts), health metrics, and recent events.",
407
+ {
408
+ tool_id: z.string().describe("Tool ID (e.g. 'agent-audit', 'indexforge')"),
409
+ },
410
+ async ({ tool_id }) => {
411
+ if (!ADMIN_KEY) return noAdminKeyError();
412
+ try {
413
+ const result = await adminRequest("GET", `/admin/reliability/${encodeURIComponent(tool_id)}`);
414
+ const c = result.circuit || {};
415
+ const cfg = result.config || {};
416
+ const h = result.health || {};
417
+ const events = result.events || [];
418
+
419
+ let text = `=== ${tool_id} Reliability ===\n\n`;
420
+ text += `CIRCUIT: ${c.state} (failures: ${c.failureCount}, successes: ${c.successCount})\n`;
421
+ text += `CONFIG: threshold=${cfg.failureThreshold} failures, recovery=${cfg.recoveryTimeoutMs}ms, timeout=${cfg.requestTimeoutMs}ms\n`;
422
+ text += `HEALTH: ${h.successRate}% success (${h.requests?.total || 0} calls)\n`;
423
+ text += `LATENCY: avg=${h.latency?.avg || 0}ms, p50=${h.latency?.p50 || 0}ms, p95=${h.latency?.p95 || 0}ms, p99=${h.latency?.p99 || 0}ms (${h.latency?.samples || 0} samples)\n`;
424
+ if (h.lastCall) text += `LAST CALL: ${h.lastCall}\n`;
425
+
426
+ if (events.length > 0) {
427
+ text += `\nEVENTS (${events.length}):\n`;
428
+ for (const e of events.slice(-20)) {
429
+ const status = e.success ? "OK" : "FAIL";
430
+ text += ` [${e.timestamp}] ${e.method} ${status} ${e.latencyMs}ms${e.error ? " — " + e.error : ""}\n`;
431
+ }
432
+ }
433
+
434
+ return { content: [{ type: "text", text }] };
435
+ } catch (e) {
436
+ return { content: [{ type: "text", text: `Error: ${e.message}` }] };
437
+ }
438
+ }
439
+ );
440
+
441
+ // ── Tool: reliability_reset ───────────────────────────────────────
442
+
443
+ server.tool(
444
+ "reliability_reset",
445
+ "Force-close a tripped circuit breaker for a tool, allowing calls to resume immediately. Use when a backend has recovered but the circuit hasn't automatically reset yet.",
446
+ {
447
+ tool_id: z.string().describe("Tool ID to reset (e.g. 'agent-audit', 'indexforge')"),
448
+ },
449
+ async ({ tool_id }) => {
450
+ if (!ADMIN_KEY) return noAdminKeyError();
451
+ try {
452
+ const result = await adminRequest("POST", `/admin/reliability/${encodeURIComponent(tool_id)}/reset`);
453
+ return { content: [{ type: "text", text: `Circuit reset for ${tool_id}: ${result.previousState} → ${result.currentState}` }] };
454
+ } catch (e) {
455
+ return { content: [{ type: "text", text: `Error: ${e.message}` }] };
456
+ }
457
+ }
458
+ );
459
+
460
+ // ── Tool: reliability_config ──────────────────────────────────────
461
+
462
+ server.tool(
463
+ "reliability_config",
464
+ "Update reliability settings for a specific tool — failure threshold, recovery timeout, and request timeout. Changes take effect immediately.",
465
+ {
466
+ tool_id: z.string().describe("Tool ID to configure (e.g. 'agent-audit', 'indexforge')"),
467
+ failure_threshold: z.number().int().min(1).max(100).optional().describe("Number of consecutive failures before circuit opens (1-100)"),
468
+ recovery_timeout_ms: z.number().int().min(5000).max(600000).optional().describe("Milliseconds before OPEN circuit tries recovery (5000-600000)"),
469
+ request_timeout_ms: z.number().int().min(1000).max(600000).optional().describe("Per-request timeout in milliseconds (1000-600000)"),
470
+ },
471
+ async ({ tool_id, failure_threshold, recovery_timeout_ms, request_timeout_ms }) => {
472
+ if (!ADMIN_KEY) return noAdminKeyError();
473
+ const body = {};
474
+ if (failure_threshold !== undefined) body.failureThreshold = failure_threshold;
475
+ if (recovery_timeout_ms !== undefined) body.recoveryTimeoutMs = recovery_timeout_ms;
476
+ if (request_timeout_ms !== undefined) body.requestTimeoutMs = request_timeout_ms;
477
+ if (Object.keys(body).length === 0) {
478
+ return { content: [{ type: "text", text: "Error: Provide at least one setting to update (failure_threshold, recovery_timeout_ms, request_timeout_ms)" }] };
479
+ }
480
+ try {
481
+ const result = await adminRequest("POST", `/admin/reliability/${encodeURIComponent(tool_id)}/config`, body);
482
+ const cfg = result.config || {};
483
+ return { content: [{ type: "text", text: `Config updated for ${tool_id}:\n failureThreshold: ${cfg.failureThreshold}\n recoveryTimeoutMs: ${cfg.recoveryTimeoutMs}\n requestTimeoutMs: ${cfg.requestTimeoutMs}` }] };
484
+ } catch (e) {
485
+ return { content: [{ type: "text", text: `Error: ${e.message}` }] };
486
+ }
487
+ }
488
+ );
489
+
284
490
  // ── Start ───────────────────────────────────────────────────────────
285
491
 
286
492
  async function main() {
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "mcp-server-agentpay",
3
- "version": "1.0.4",
3
+ "version": "1.1.0",
4
4
  "mcpName": "io.github.joepangallo/agent-pay",
5
- "description": "MCP server for AgentPay — the payment gateway for autonomous AI agents. Lets agents discover, provision, and pay for MCP tool APIs with a single gateway key.",
5
+ "description": "MCP server for AgentPay — the payment gateway for autonomous AI agents. Discover, provision, and pay for MCP tool APIs. Includes reliability monitoring with circuit breakers and health metrics.",
6
6
  "bin": {
7
7
  "mcp-server-agentpay": "index.js"
8
8
  },
package/server.json CHANGED
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
3
  "name": "io.github.joepangallo/agent-pay",
4
- "description": "Payment gateway for AI agents. Tool discovery, provisioning, and pay-per-call metering.",
4
+ "description": "Payment gateway for AI agents. Tool discovery, provisioning, pay-per-call metering, and reliability monitoring with circuit breakers.",
5
5
  "repository": {
6
6
  "url": "https://github.com/joepangallo/agent-pay",
7
7
  "source": "github"
8
8
  },
9
- "version": "1.0.4",
9
+ "version": "1.1.0",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "mcp-server-agentpay",
14
- "version": "1.0.4",
14
+ "version": "1.1.0",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },
@@ -23,6 +23,13 @@
23
23
  "isSecret": true,
24
24
  "name": "AGENTPAY_GATEWAY_KEY"
25
25
  },
26
+ {
27
+ "description": "Admin key for reliability endpoints (circuit breakers, health metrics, config)",
28
+ "isRequired": false,
29
+ "format": "string",
30
+ "isSecret": true,
31
+ "name": "AGENTPAY_ADMIN_KEY"
32
+ },
26
33
  {
27
34
  "description": "AgentPay API URL",
28
35
  "isRequired": false,