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.
- package/index.js +206 -0
- package/package.json +2 -2
- 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
|
|
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.
|
|
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,
|
|
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
|
|
9
|
+
"version": "1.1.0",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "mcp-server-agentpay",
|
|
14
|
-
"version": "1.0
|
|
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,
|