phoenix-mcp-server 1.1.0 → 1.3.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/dist/index.d.ts +8 -0
- package/dist/index.js +69 -140
- package/package.json +2 -2
- package/server.json +25 -7
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
export declare const CHAINS: readonly ["base", "arbitrum", "optimism", "zksync", "scroll", "mantle", "linea", "blast", "mode", "taiko", "polygon_zkevm", "casper"];
|
|
4
|
+
type Verdict = "PASS" | "DEGRADED" | "FAIL";
|
|
5
|
+
export declare function verdictFromChain(safe: unknown, reason: unknown): {
|
|
6
|
+
verdict: Verdict;
|
|
7
|
+
reason: string;
|
|
8
|
+
};
|
|
9
|
+
export declare const server: McpServer;
|
|
2
10
|
export {};
|
package/dist/index.js
CHANGED
|
@@ -3,174 +3,103 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
const PHOENIX_BASE = process.env.PHOENIX_API_URL ?? "https://rtt.phoenix-ai.work";
|
|
6
|
-
const CHAINS = [
|
|
6
|
+
export const CHAINS = [
|
|
7
7
|
"base", "arbitrum", "optimism", "zksync", "scroll",
|
|
8
8
|
"mantle", "linea", "blast", "mode", "taiko",
|
|
9
9
|
"polygon_zkevm", "casper",
|
|
10
10
|
];
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if (
|
|
19
|
-
return { verdict: "
|
|
20
|
-
}
|
|
21
|
-
return { verdict: "PASS", reason: "operational, data fresh, safe to execute" };
|
|
22
|
-
}
|
|
23
|
-
async function fetchHealth() {
|
|
24
|
-
const res = await fetch(`${PHOENIX_BASE}/api/health`);
|
|
25
|
-
if (!res.ok) {
|
|
26
|
-
throw new Error(`Phoenix API returned ${res.status}`);
|
|
27
|
-
}
|
|
28
|
-
return res.json();
|
|
11
|
+
// Verdict for ONE chain, derived from that chain's own safe/reason (oracle /v1/demo/safe).
|
|
12
|
+
// 1.2.x derived it from /api/health (this node's liveness), which said PASS for any chain
|
|
13
|
+
// whenever the server was up — fixed in 1.3.0.
|
|
14
|
+
export function verdictFromChain(safe, reason) {
|
|
15
|
+
const r = typeof reason === "string" ? reason : "unknown";
|
|
16
|
+
if (safe === true)
|
|
17
|
+
return { verdict: "PASS", reason: r };
|
|
18
|
+
if (r === "elevated_latency" || r === "elevated_revert")
|
|
19
|
+
return { verdict: "DEGRADED", reason: r };
|
|
20
|
+
return { verdict: "FAIL", reason: r }; // high_latency, high_revert, sequencer_stall, data_stale, warming_up, unknown
|
|
29
21
|
}
|
|
30
22
|
async function fetchDemoSafe(chain) {
|
|
31
|
-
const
|
|
32
|
-
? `${PHOENIX_BASE}/api/v1/demo/safe?chain=${chain}`
|
|
33
|
-
: `${PHOENIX_BASE}/api/v1/demo/safe`;
|
|
34
|
-
const res = await fetch(url);
|
|
23
|
+
const res = await fetch(`${PHOENIX_BASE}/api/v1/demo/safe?chain=${chain}`);
|
|
35
24
|
if (!res.ok) {
|
|
36
25
|
throw new Error(`Demo endpoint returned ${res.status}`);
|
|
37
26
|
}
|
|
38
27
|
return res.json();
|
|
39
28
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
|
|
29
|
+
const PAYMENT_RAILS = [
|
|
30
|
+
{ network: "Base (eip155:8453)", asset: "USDC" },
|
|
31
|
+
{ network: "Polygon (eip155:137)", asset: "USDC" },
|
|
32
|
+
{ network: "Arbitrum One (eip155:42161)", asset: "USDC" },
|
|
33
|
+
];
|
|
34
|
+
const text = (obj, isError = false) => ({
|
|
35
|
+
content: [{ type: "text", text: JSON.stringify(obj, null, 2) }],
|
|
36
|
+
...(isError ? { isError: true } : {}),
|
|
37
|
+
});
|
|
38
|
+
export const server = new McpServer({
|
|
49
39
|
name: "phoenix-zero",
|
|
50
|
-
version: "1.
|
|
40
|
+
version: "1.3.0",
|
|
51
41
|
});
|
|
52
|
-
server.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
.optional()
|
|
56
|
-
|
|
42
|
+
server.registerTool("check_safety_free", {
|
|
43
|
+
description: "FREE safety check for an L2 before sending a transaction (100 calls/IP/day). Returns safe/unsafe with a reason code for the chosen chain. The verdict is delayed ~60 s; the live verdict is the paid x402 endpoint GET /api/v1/safe ($0.01 USDC).",
|
|
44
|
+
inputSchema: {
|
|
45
|
+
chain: z.enum(CHAINS).optional().describe("L2 chain to check. Defaults to base."),
|
|
46
|
+
},
|
|
47
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
57
48
|
}, async ({ chain }) => {
|
|
58
49
|
try {
|
|
59
|
-
|
|
60
|
-
return {
|
|
61
|
-
content: [
|
|
62
|
-
{
|
|
63
|
-
type: "text",
|
|
64
|
-
text: JSON.stringify(data, null, 2),
|
|
65
|
-
},
|
|
66
|
-
],
|
|
67
|
-
};
|
|
50
|
+
return text(await fetchDemoSafe(chain ?? "base"));
|
|
68
51
|
}
|
|
69
52
|
catch (err) {
|
|
70
|
-
|
|
71
|
-
return {
|
|
72
|
-
content: [
|
|
73
|
-
{
|
|
74
|
-
type: "text",
|
|
75
|
-
text: JSON.stringify({ error: message, source: PHOENIX_BASE }, null, 2),
|
|
76
|
-
},
|
|
77
|
-
],
|
|
78
|
-
isError: true,
|
|
79
|
-
};
|
|
53
|
+
return text({ error: err instanceof Error ? err.message : String(err), source: PHOENIX_BASE }, true);
|
|
80
54
|
}
|
|
81
55
|
});
|
|
82
|
-
server.
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
.optional()
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
56
|
+
server.registerTool("preflight_network_health", {
|
|
57
|
+
description: "Decide whether an L2 is suitable for transaction execution right now: PASS / DEGRADED / FAIL for the chosen chain, from Phoenix Zero measurements (each chain polled every ~2 s against 2-3 independent RPC nodes; revert ratio for Base/Arbitrum; records signed BLAKE3+Ed25519). The free answer is delayed ~60 s; live data (p99 latency, revert ratio) is paid via x402 ($0.01 USDC on Base, Polygon or Arbitrum).",
|
|
58
|
+
inputSchema: {
|
|
59
|
+
chain: z.enum(CHAINS).optional().describe("L2 chain to check. Defaults to base."),
|
|
60
|
+
metric: z
|
|
61
|
+
.enum(["health_check", "latency", "revert_ratio"])
|
|
62
|
+
.default("health_check")
|
|
63
|
+
.describe("health_check = free delayed PASS/DEGRADED/FAIL. latency / revert_ratio = live values via paid x402 endpoints."),
|
|
64
|
+
},
|
|
65
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
91
66
|
}, async ({ chain, metric }) => {
|
|
67
|
+
const c = chain ?? "base";
|
|
92
68
|
try {
|
|
93
|
-
const
|
|
94
|
-
const { verdict, reason } =
|
|
69
|
+
const demo = await fetchDemoSafe(c);
|
|
70
|
+
const { verdict, reason } = verdictFromChain(demo.safe, demo.reason);
|
|
71
|
+
const base = {
|
|
72
|
+
chain: c,
|
|
73
|
+
verdict,
|
|
74
|
+
reason,
|
|
75
|
+
delayed_s: demo.delayed_s ?? null,
|
|
76
|
+
as_of: demo.as_of ?? null,
|
|
77
|
+
source: "rtt.phoenix-ai.work",
|
|
78
|
+
};
|
|
95
79
|
if (metric === "health_check") {
|
|
96
|
-
|
|
97
|
-
verdict,
|
|
98
|
-
reason,
|
|
99
|
-
oracle_uptime_s: health.uptime_s,
|
|
100
|
-
last_measurement_s: health.last_measurement_s,
|
|
101
|
-
chains_monitored: CHAINS.length,
|
|
102
|
-
source: "rtt.phoenix-ai.work",
|
|
103
|
-
integrity: "BLAKE3+Ed25519 on every measurement",
|
|
104
|
-
sampling_interval: "2s",
|
|
105
|
-
};
|
|
106
|
-
if (chain) {
|
|
107
|
-
result.chain = chain;
|
|
108
|
-
const chainData = await fetchChainHealth(chain);
|
|
109
|
-
if (chainData) {
|
|
110
|
-
result.chain_data = chainData;
|
|
111
|
-
}
|
|
112
|
-
else {
|
|
113
|
-
result.chain_detail = `Per-chain telemetry for ${chain} requires x402 payment ($0.01). Call: GET ${PHOENIX_BASE}/api/v1/chains/${chain}`;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
return {
|
|
117
|
-
content: [
|
|
118
|
-
{
|
|
119
|
-
type: "text",
|
|
120
|
-
text: JSON.stringify(result, null, 2),
|
|
121
|
-
},
|
|
122
|
-
],
|
|
123
|
-
};
|
|
80
|
+
return text({ ...base, live_verdict: `GET ${PHOENIX_BASE}/api/v1/safe?chain=${c} (x402, $0.01 USDC)` });
|
|
124
81
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
:
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
reason,
|
|
135
|
-
metric_requested: metric,
|
|
136
|
-
requires_payment: true,
|
|
137
|
-
price: "$0.01 USDC",
|
|
138
|
-
payment_protocol: "x402 (HTTP 402 micropayment)",
|
|
139
|
-
endpoint: premiumEndpoint,
|
|
140
|
-
preflight_endpoint: `${PHOENIX_BASE}/api/v1/preflight`,
|
|
141
|
-
preflight_note: "POST /api/v1/preflight with {chain} body returns deterministic PASS/DEGRADED/FAIL with evidence_id proving exact network state at decision time.",
|
|
142
|
-
payment_rails: [
|
|
143
|
-
{ network: "Base mainnet (eip155:8453)", asset: "USDC" },
|
|
144
|
-
{ network: "Hedera testnet", asset: "native HBAR" },
|
|
145
|
-
],
|
|
146
|
-
how_to_pay: "Send request to the endpoint. Server returns 402 with payment-required header containing x402 challenge. Complete payment flow to receive data.",
|
|
147
|
-
}, null, 2),
|
|
148
|
-
},
|
|
149
|
-
],
|
|
150
|
-
};
|
|
82
|
+
return text({
|
|
83
|
+
...base,
|
|
84
|
+
metric_requested: metric,
|
|
85
|
+
requires_payment: true,
|
|
86
|
+
price: "$0.01 USDC",
|
|
87
|
+
endpoint: metric === "latency" ? `${PHOENIX_BASE}/api/v1/chains/${c}` : `${PHOENIX_BASE}/api/v1/safe?chain=${c}`,
|
|
88
|
+
payment_protocol: "x402 v2 (HTTP 402; terms in the PAYMENT-REQUIRED header and the response body)",
|
|
89
|
+
payment_rails: PAYMENT_RAILS,
|
|
90
|
+
});
|
|
151
91
|
}
|
|
152
92
|
catch (err) {
|
|
153
|
-
|
|
154
|
-
return {
|
|
155
|
-
content: [
|
|
156
|
-
{
|
|
157
|
-
type: "text",
|
|
158
|
-
text: JSON.stringify({
|
|
159
|
-
verdict: "FAIL",
|
|
160
|
-
reason: `oracle unreachable: ${message}`,
|
|
161
|
-
source: PHOENIX_BASE,
|
|
162
|
-
}, null, 2),
|
|
163
|
-
},
|
|
164
|
-
],
|
|
165
|
-
isError: true,
|
|
166
|
-
};
|
|
93
|
+
return text({ error: err instanceof Error ? err.message : String(err), source: PHOENIX_BASE }, true);
|
|
167
94
|
}
|
|
168
95
|
});
|
|
169
96
|
async function main() {
|
|
170
97
|
const transport = new StdioServerTransport();
|
|
171
98
|
await server.connect(transport);
|
|
172
99
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
100
|
+
if (!process.env.VITEST) {
|
|
101
|
+
main().catch((err) => {
|
|
102
|
+
console.error("Fatal:", err);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
});
|
|
105
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "phoenix-mcp-server",
|
|
3
3
|
"mcpName": "io.github.kant19801201behax5/phoenix-mcp-server",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.3.0",
|
|
5
5
|
"description": "MCP server for Phoenix Zero L2 health oracle — kernel-level network telemetry for AI agents",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"url": "https://github.com/kant19801201behax5/x402-health-oracle"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
42
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
43
43
|
"zod": "^3.23.0"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
package/server.json
CHANGED
|
@@ -1,26 +1,44 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
3
|
"name": "io.github.kant19801201behax5/phoenix-mcp-server",
|
|
4
|
-
"title": "Phoenix Zero —
|
|
5
|
-
"description": "
|
|
4
|
+
"title": "Phoenix Zero — Execution Oracle for L2s",
|
|
5
|
+
"description": "Before sending a transaction on an L2: PASS/DEGRADED/FAIL for the chosen chain from per-chain measurements (every ~2 s, 2-3 RPC nodes). Free delayed verdict; live data via x402.",
|
|
6
6
|
"repository": {
|
|
7
7
|
"url": "https://github.com/kant19801201behax5/x402-health-oracle",
|
|
8
8
|
"source": "github"
|
|
9
9
|
},
|
|
10
|
-
"version": "1.0
|
|
10
|
+
"version": "1.3.0",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"registryBaseUrl": "https://registry.npmjs.org",
|
|
15
15
|
"identifier": "phoenix-mcp-server",
|
|
16
|
-
"version": "1.0
|
|
17
|
-
"transport": {
|
|
16
|
+
"version": "1.3.0",
|
|
17
|
+
"transport": {
|
|
18
|
+
"type": "stdio"
|
|
19
|
+
}
|
|
18
20
|
}
|
|
19
21
|
],
|
|
20
22
|
"tools": [
|
|
23
|
+
{
|
|
24
|
+
"name": "check_safety_free",
|
|
25
|
+
"description": "FREE safety check for an L2 (100 calls/IP/day): safe/unsafe with reason for the chosen chain, delayed ~60 s. Live verdict: paid x402 GET /api/v1/safe.",
|
|
26
|
+
"annotations": {
|
|
27
|
+
"readOnlyHint": true,
|
|
28
|
+
"destructiveHint": false,
|
|
29
|
+
"idempotentHint": true,
|
|
30
|
+
"openWorldHint": true
|
|
31
|
+
}
|
|
32
|
+
},
|
|
21
33
|
{
|
|
22
34
|
"name": "preflight_network_health",
|
|
23
|
-
"description": "
|
|
35
|
+
"description": "PASS/DEGRADED/FAIL for the chosen L2 from its own measurements (latency across 2-3 RPC nodes every ~2 s; revert ratio for Base/Arbitrum). Free answer delayed ~60 s; live latency/revert_ratio via x402 ($0.01 USDC on Base, Polygon or Arbitrum).",
|
|
36
|
+
"annotations": {
|
|
37
|
+
"readOnlyHint": true,
|
|
38
|
+
"destructiveHint": false,
|
|
39
|
+
"idempotentHint": true,
|
|
40
|
+
"openWorldHint": true
|
|
41
|
+
}
|
|
24
42
|
}
|
|
25
43
|
]
|
|
26
|
-
}
|
|
44
|
+
}
|