@molpha/mcp 0.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/LICENSE +21 -0
- package/README.md +270 -0
- package/dist/cli/doctor.js +27 -0
- package/dist/cli/provision.js +84 -0
- package/dist/src/apiconfig.js +47 -0
- package/dist/src/artifacts.js +80 -0
- package/dist/src/clients.js +51 -0
- package/dist/src/config.js +102 -0
- package/dist/src/determinism.js +31 -0
- package/dist/src/env.js +14 -0
- package/dist/src/errors.js +104 -0
- package/dist/src/feed.js +53 -0
- package/dist/src/guardrails.js +62 -0
- package/dist/src/hex.js +45 -0
- package/dist/src/mcp.js +82 -0
- package/dist/src/sdk.js +12 -0
- package/dist/src/server.js +17 -0
- package/dist/src/setup-validation.js +340 -0
- package/dist/src/signer/backends/memory.js +41 -0
- package/dist/src/signer/backends/privy.js +48 -0
- package/dist/src/signer/backends/turnkey.js +53 -0
- package/dist/src/signer/factory.js +37 -0
- package/dist/src/signer/types.js +1 -0
- package/dist/src/solana-address.js +29 -0
- package/dist/src/solana-compat.js +22 -0
- package/dist/src/submit.js +46 -0
- package/dist/src/subscription.js +48 -0
- package/dist/src/tools/agent_status.js +26 -0
- package/dist/src/tools/derive_feed.js +39 -0
- package/dist/src/tools/describe_feed.js +44 -0
- package/dist/src/tools/execute.js +60 -0
- package/dist/src/tools/fetch_verified.js +116 -0
- package/dist/src/tools/get_capabilities.js +45 -0
- package/dist/src/tools/get_latest.js +22 -0
- package/dist/src/tools/index.js +19 -0
- package/dist/src/tools/schemas.js +9 -0
- package/dist/src/tools/types.js +1 -0
- package/dist/src/tools/verify.js +28 -0
- package/dist/src/verifiers.js +95 -0
- package/dist/src/x402.js +552 -0
- package/package.json +81 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { isAbsolute, resolve } from "node:path";
|
|
4
|
+
import { getSdkExport } from "./sdk.js";
|
|
5
|
+
const DEFAULT_SOLANA_RPC = "https://api.devnet.solana.com";
|
|
6
|
+
const FALLBACK_GATEWAY_ENDPOINT = "https://dev-gateway.molpha.io";
|
|
7
|
+
export function loadConfig(env = process.env) {
|
|
8
|
+
const sdkDefaultGateway = getSdkExport("DEFAULT_GATEWAY_ENDPOINT");
|
|
9
|
+
return {
|
|
10
|
+
gatewayEndpoints: parseCsv(resolveEnvString(env.GATEWAY_ENDPOINTS) ?? sdkDefaultGateway ?? FALLBACK_GATEWAY_ENDPOINT),
|
|
11
|
+
solanaRpc: resolveEnvString(env.SOLANA_RPC) ?? DEFAULT_SOLANA_RPC,
|
|
12
|
+
ownerKeypair: resolveEnvString(env.OWNER_KEYPAIR ?? env.AGENT_KEYPAIR),
|
|
13
|
+
evmNetworks: parseCsv(resolveEnvString(env.MOLPHA_EVM_NETWORKS) ?? "evm-sepolia"),
|
|
14
|
+
starknetNetworks: parseCsv(resolveEnvString(env.MOLPHA_STARKNET_NETWORKS) ?? "starknet-sepolia"),
|
|
15
|
+
guardrails: {
|
|
16
|
+
maxExecutesPerDay: parsePositiveInt(resolveEnvString(env.MOLPHA_MAX_EXECUTES_PER_DAY), 100),
|
|
17
|
+
dryRunDefault: (() => {
|
|
18
|
+
const dryRun = resolveEnvString(env.MOLPHA_DRY_RUN);
|
|
19
|
+
return dryRun === "1" || dryRun === "true";
|
|
20
|
+
})()
|
|
21
|
+
},
|
|
22
|
+
x402: {
|
|
23
|
+
maxPriceUsdcAtomic: parseUsdcAtomic(resolveEnvString(env.MOLPHA_X402_MAX_PRICE_USDC), 1000000n),
|
|
24
|
+
maxSpendPerDayUsdcAtomic: parseUsdcAtomic(resolveEnvString(env.MOLPHA_X402_MAX_SPEND_PER_DAY_USDC), 10000000n),
|
|
25
|
+
gatewayPda: resolveEnvString(env.MOLPHA_X402_GATEWAY_PDA)
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export function loadOwnerKeypair(config) {
|
|
30
|
+
if (!config.ownerKeypair) {
|
|
31
|
+
throw new Error("OWNER_KEYPAIR is required for the Molpha MCP runtime (Model A owner key)");
|
|
32
|
+
}
|
|
33
|
+
return loadKeypair(config.ownerKeypair);
|
|
34
|
+
}
|
|
35
|
+
/** @deprecated Use loadOwnerKeypair — AGENT_KEYPAIR alias retained for compatibility. */
|
|
36
|
+
export function loadAgentKeypair(config) {
|
|
37
|
+
return loadOwnerKeypair(config);
|
|
38
|
+
}
|
|
39
|
+
export function loadKeypair(pathOrJson) {
|
|
40
|
+
const raw = isInlineKeypair(pathOrJson) ? pathOrJson : readFileSync(resolvePath(pathOrJson), "utf8");
|
|
41
|
+
const secretKey = JSON.parse(raw);
|
|
42
|
+
if (!Array.isArray(secretKey) || secretKey.length !== 64) {
|
|
43
|
+
throw new Error("keypair must be a JSON array of 64 secret-key bytes");
|
|
44
|
+
}
|
|
45
|
+
return Uint8Array.from(secretKey);
|
|
46
|
+
}
|
|
47
|
+
function parseCsv(value) {
|
|
48
|
+
return value
|
|
49
|
+
.split(",")
|
|
50
|
+
.map((part) => part.trim())
|
|
51
|
+
.filter(Boolean);
|
|
52
|
+
}
|
|
53
|
+
/** Parses a decimal USDC amount (e.g. "1.5") into base units (6 decimals). */
|
|
54
|
+
function parseUsdcAtomic(value, fallback) {
|
|
55
|
+
if (!value?.trim()) {
|
|
56
|
+
return fallback;
|
|
57
|
+
}
|
|
58
|
+
const match = /^(\d+)(?:\.(\d{1,6}))?$/.exec(value.trim());
|
|
59
|
+
if (!match) {
|
|
60
|
+
throw new Error(`expected a decimal USDC amount, got "${value}"`);
|
|
61
|
+
}
|
|
62
|
+
const [, whole = "0", fraction = ""] = match;
|
|
63
|
+
return BigInt(whole) * 1000000n + BigInt(fraction.padEnd(6, "0"));
|
|
64
|
+
}
|
|
65
|
+
function parsePositiveInt(value, fallback) {
|
|
66
|
+
if (!value) {
|
|
67
|
+
return fallback;
|
|
68
|
+
}
|
|
69
|
+
const parsed = Number.parseInt(value, 10);
|
|
70
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
71
|
+
throw new Error(`expected a positive integer, got "${value}"`);
|
|
72
|
+
}
|
|
73
|
+
return parsed;
|
|
74
|
+
}
|
|
75
|
+
const UNRESOLVED_MCP_USER_CONFIG = /^\$\{user_config\.[a-z0-9_]+\}$/i;
|
|
76
|
+
/** Treats empty strings and unresolved MCP bundle `${user_config.*}` placeholders as unset. */
|
|
77
|
+
export function resolveEnvString(value) {
|
|
78
|
+
if (!value) {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
const trimmed = value.trim();
|
|
82
|
+
if (trimmed.length === 0 || UNRESOLVED_MCP_USER_CONFIG.test(trimmed)) {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
return value;
|
|
86
|
+
}
|
|
87
|
+
export function resolvePath(path) {
|
|
88
|
+
if (path.startsWith("~/")) {
|
|
89
|
+
return resolve(homedir(), path.slice(2));
|
|
90
|
+
}
|
|
91
|
+
return isAbsolute(path) ? path : resolve(process.cwd(), path);
|
|
92
|
+
}
|
|
93
|
+
/** An OWNER_KEYPAIR value can be a file path or an inline JSON secret-key array. */
|
|
94
|
+
export function isInlineKeypair(pathOrJson) {
|
|
95
|
+
return pathOrJson.trim().startsWith("[");
|
|
96
|
+
}
|
|
97
|
+
/** Formats a USDC atomic (6-decimal) amount as a decimal string, e.g. 1_500_000n -> "1.5". */
|
|
98
|
+
export function formatUsdcAtomic(atomic) {
|
|
99
|
+
const whole = atomic / 1000000n;
|
|
100
|
+
const fraction = atomic % 1000000n;
|
|
101
|
+
return fraction === 0n ? whole.toString() : `${whole}.${fraction.toString().padStart(6, "0").replace(/0+$/, "")}`;
|
|
102
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** Heuristic guard for API configs that may not converge across independent nodes. */
|
|
2
|
+
const LIVE_DRIFTING_PATTERNS = [
|
|
3
|
+
/\/ticker\b/i,
|
|
4
|
+
/\/price\b/i,
|
|
5
|
+
/\/latest\b/i,
|
|
6
|
+
/\/stream\b/i,
|
|
7
|
+
/\/realtime\b/i,
|
|
8
|
+
/\/websocket\b/i,
|
|
9
|
+
/\/ws\b/i,
|
|
10
|
+
/\/live\b/i
|
|
11
|
+
];
|
|
12
|
+
export function checkApiConfigDeterminism(apiConfig) {
|
|
13
|
+
const warnings = [];
|
|
14
|
+
const url = typeof apiConfig.url === "string" ? apiConfig.url : "";
|
|
15
|
+
if (!url) {
|
|
16
|
+
return { ok: false, warnings: ["apiConfig.url is required"] };
|
|
17
|
+
}
|
|
18
|
+
for (const pattern of LIVE_DRIFTING_PATTERNS) {
|
|
19
|
+
if (pattern.test(url)) {
|
|
20
|
+
warnings.push(`URL "${url}" may return live-drifting data. Independent nodes must converge on a byte-identical value to co-sign. Prefer settled/finalized sources.`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const method = typeof apiConfig.method === "string" ? apiConfig.method.toUpperCase() : "GET";
|
|
24
|
+
if (method !== "GET" && method !== "POST") {
|
|
25
|
+
warnings.push(`method "${method}" is unusual; prefer GET for public deterministic APIs`);
|
|
26
|
+
}
|
|
27
|
+
if (!apiConfig.responseParser) {
|
|
28
|
+
return { ok: false, warnings: [...warnings, "apiConfig.responseParser is required"] };
|
|
29
|
+
}
|
|
30
|
+
return { ok: warnings.length === 0, warnings };
|
|
31
|
+
}
|
package/dist/src/env.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { config } from "dotenv";
|
|
4
|
+
export function loadDotenv() {
|
|
5
|
+
const candidates = [process.env.MOLPHA_ENV_FILE, ".env"].filter((value) => Boolean(value && value.trim().length > 0));
|
|
6
|
+
for (const candidate of candidates) {
|
|
7
|
+
const path = resolve(process.cwd(), candidate);
|
|
8
|
+
if (existsSync(path)) {
|
|
9
|
+
config({ path, override: false });
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
loadDotenv();
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/** Runs `run()`, capturing a failure as a normalized error instead of throwing. */
|
|
2
|
+
export async function settle(label, run) {
|
|
3
|
+
try {
|
|
4
|
+
return { ok: true, value: await run() };
|
|
5
|
+
}
|
|
6
|
+
catch (error) {
|
|
7
|
+
return { ok: false, label, error: normalizeError(error) };
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export function normalizeError(error) {
|
|
11
|
+
const status = getStatus(error);
|
|
12
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
13
|
+
if (status === 400) {
|
|
14
|
+
return withStatus("invalid_request", message, status);
|
|
15
|
+
}
|
|
16
|
+
if (status === 401) {
|
|
17
|
+
return {
|
|
18
|
+
...withStatus("unauthorized", message, status),
|
|
19
|
+
remediation: "Ensure OWNER_KEYPAIR matches the job owner or an authorized request signer."
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
if (status === 402) {
|
|
23
|
+
const payload = getX402Payload(error);
|
|
24
|
+
return {
|
|
25
|
+
...withStatus("payment_required", message, status),
|
|
26
|
+
remediation: "Fund the x402 escrow (see molpha_agent_status) and retry, or use payment: \"subscription\" with an active subscription.",
|
|
27
|
+
...(payload !== undefined ? { details: payload } : {})
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
if (status === 503 || isTimeout(error)) {
|
|
31
|
+
return withStatus("round_timeout", message, status);
|
|
32
|
+
}
|
|
33
|
+
if (message.includes("OWNER_KEYPAIR") || message.includes("AGENT_KEYPAIR")) {
|
|
34
|
+
return {
|
|
35
|
+
code: "missing_config",
|
|
36
|
+
message,
|
|
37
|
+
remediation: "Set OWNER_KEYPAIR to the funded owner keypair JSON path in the MCP server env."
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
if (message.includes("must be a valid Solana address") ||
|
|
41
|
+
message.includes("Non-base58 character")) {
|
|
42
|
+
return {
|
|
43
|
+
code: "invalid_config",
|
|
44
|
+
message,
|
|
45
|
+
remediation: "Check PRIVY_WALLET_ADDRESS / TURNKEY_WALLET_ADDRESS (and optional MOLPHA_X402_GATEWAY_PDA) in your MCP env. Use a real Solana devnet pubkey — not placeholders like <base58-solana-address>."
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
if (message.includes("Subscription") || message.includes("subscription")) {
|
|
49
|
+
return {
|
|
50
|
+
code: "subscription_inactive",
|
|
51
|
+
message,
|
|
52
|
+
remediation: "Run the bootstrap CLI to subscribe, or use payment: \"x402\" for a self-funded round."
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
if (message.includes("cap reached")) {
|
|
56
|
+
return { code: "guardrail_exceeded", message };
|
|
57
|
+
}
|
|
58
|
+
if (message.includes('"jsonrpc"') && message.includes("Method not found")) {
|
|
59
|
+
return {
|
|
60
|
+
code: "invalid_config",
|
|
61
|
+
message,
|
|
62
|
+
remediation: "GATEWAY_ENDPOINTS is pointing at a Solana RPC URL, not a Molpha gateway. Set it to the Molpha gateway base URL (see README / npm run doctor) and keep SOLANA_RPC separate."
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (message.includes("/v1/agent/execute") && message.includes("page not found")) {
|
|
66
|
+
return {
|
|
67
|
+
code: "invalid_config",
|
|
68
|
+
message,
|
|
69
|
+
remediation: "This gateway host exposes /v1/nodes but not signing routes. Use https://dev-gateway.molpha.io (run npm run doctor to verify)."
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (message.includes("determinism") || message.includes("live-drifting")) {
|
|
73
|
+
return { code: "determinism_rejected", message };
|
|
74
|
+
}
|
|
75
|
+
return withStatus("internal_error", message, status);
|
|
76
|
+
}
|
|
77
|
+
function withStatus(code, message, status) {
|
|
78
|
+
return status === undefined ? { code, message } : { code, message, status };
|
|
79
|
+
}
|
|
80
|
+
function getStatus(error) {
|
|
81
|
+
if (!error || typeof error !== "object") {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
const record = error;
|
|
85
|
+
const response = record.response;
|
|
86
|
+
const cause = record.cause;
|
|
87
|
+
const status = record.status ?? record.statusCode ?? response?.status ?? cause?.status;
|
|
88
|
+
return typeof status === "number" ? status : undefined;
|
|
89
|
+
}
|
|
90
|
+
function getX402Payload(error) {
|
|
91
|
+
if (!error || typeof error !== "object") {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
return error.x402;
|
|
95
|
+
}
|
|
96
|
+
function isTimeout(error) {
|
|
97
|
+
if (!error || typeof error !== "object") {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
const record = error;
|
|
101
|
+
const code = typeof record.code === "string" ? record.code : undefined;
|
|
102
|
+
const message = error instanceof Error ? error.message.toLowerCase() : "";
|
|
103
|
+
return code === "ETIMEDOUT" || code === "ECONNRESET" || message.includes("timeout");
|
|
104
|
+
}
|
package/dist/src/feed.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feed-account presentation helpers.
|
|
3
|
+
*
|
|
4
|
+
* Anchor encodes the on-chain `FeedValueKind` enum as a single-key variant
|
|
5
|
+
* object — `{ value: {} }` / `{ hash: {} }` — which reads like an empty stub in
|
|
6
|
+
* tool output. Flatten it to a string and say what it means.
|
|
7
|
+
*/
|
|
8
|
+
const VALUE_KIND_MEANING = {
|
|
9
|
+
value: "`value` holds the raw oracle payload (<= 32 bytes), stored verbatim on-chain.",
|
|
10
|
+
hash: "`value` holds a keccak digest of the payload; the preimage lives off-chain."
|
|
11
|
+
};
|
|
12
|
+
export function decodeFeedValueKind(feed) {
|
|
13
|
+
const raw = feed?.valueKind ?? feed?.value_kind;
|
|
14
|
+
if (typeof raw === "string") {
|
|
15
|
+
const lowered = raw.toLowerCase();
|
|
16
|
+
return lowered === "value" || lowered === "hash" ? lowered : null;
|
|
17
|
+
}
|
|
18
|
+
if (raw && typeof raw === "object") {
|
|
19
|
+
const key = Object.keys(raw)[0]?.toLowerCase();
|
|
20
|
+
return key === "value" || key === "hash" ? key : null;
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
/** Feed account with `valueKind` flattened to a string and explained. */
|
|
25
|
+
export function presentFeed(feed) {
|
|
26
|
+
if (!feed) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
const kind = decodeFeedValueKind(feed);
|
|
30
|
+
if (!kind) {
|
|
31
|
+
return { ...feed };
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
...feed,
|
|
35
|
+
valueKind: kind,
|
|
36
|
+
valueKindMeaning: VALUE_KIND_MEANING[kind]
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Molpha attests *which encoding* the stored bytes use (`FeedValueKind` is only
|
|
41
|
+
* `value | hash`) but not their scale: there is no decimals field on the Feed
|
|
42
|
+
* account or in the signed DataUpdate. Any scale a consumer applies comes from
|
|
43
|
+
* the off-chain apiConfig that produced the number, so report it as unsigned
|
|
44
|
+
* provenance — verbatim, never parsed into a decimals count we cannot attest.
|
|
45
|
+
*/
|
|
46
|
+
export function describeValueEncoding(valueTransform) {
|
|
47
|
+
return {
|
|
48
|
+
attested: false,
|
|
49
|
+
source: "apiConfig.valueTransform (off-chain; not part of the signed payload)",
|
|
50
|
+
valueTransform: valueTransform ?? null,
|
|
51
|
+
note: "Molpha does not attest scale/decimals on-chain — FeedValueKind is only value|hash. A verifier contract must be configured with this feed's scale out of band; do not infer it from the integer alone."
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { formatUsdcAtomic } from "./config.js";
|
|
2
|
+
const executes = { day: "", count: 0 };
|
|
3
|
+
const x402Spend = { day: "", spentAtomic: 0n };
|
|
4
|
+
function todayKey() {
|
|
5
|
+
return new Date().toISOString().slice(0, 10);
|
|
6
|
+
}
|
|
7
|
+
function bump(counter, max, label) {
|
|
8
|
+
const day = todayKey();
|
|
9
|
+
if (counter.day !== day) {
|
|
10
|
+
counter.day = day;
|
|
11
|
+
counter.count = 0;
|
|
12
|
+
}
|
|
13
|
+
if (counter.count >= max) {
|
|
14
|
+
throw new Error(`${label} cap reached (${max} per day). Adjust MOLPHA_MAX_${label.toUpperCase().replace(/ /g, "_")}_PER_DAY or wait until tomorrow.`);
|
|
15
|
+
}
|
|
16
|
+
counter.count += 1;
|
|
17
|
+
}
|
|
18
|
+
/** Reset counters — exposed for tests. */
|
|
19
|
+
export function resetGuardrailCounters() {
|
|
20
|
+
executes.day = "";
|
|
21
|
+
executes.count = 0;
|
|
22
|
+
x402Spend.day = "";
|
|
23
|
+
x402Spend.spentAtomic = 0n;
|
|
24
|
+
}
|
|
25
|
+
export function enforceExecuteCap(config) {
|
|
26
|
+
bump(executes, config.maxExecutesPerDay, "execute");
|
|
27
|
+
}
|
|
28
|
+
/** Checks a proposed round price against the per-round cap only. */
|
|
29
|
+
export function checkX402PerRoundCap(priceAtomic, maxPriceUsdcAtomic) {
|
|
30
|
+
if (priceAtomic > maxPriceUsdcAtomic) {
|
|
31
|
+
throw new Error(`x402 per-round price cap reached: round price (${formatUsdcAtomic(priceAtomic)} USDC) exceeds MOLPHA_X402_MAX_PRICE_USDC (${formatUsdcAtomic(maxPriceUsdcAtomic)} USDC).`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** Checks a proposed wallet outflow against the daily spend cap only. */
|
|
35
|
+
export function checkX402DailySpendCap(amountAtomic, maxSpendPerDayUsdcAtomic) {
|
|
36
|
+
const day = todayKey();
|
|
37
|
+
const spentToday = x402Spend.day === day ? x402Spend.spentAtomic : 0n;
|
|
38
|
+
if (spentToday + amountAtomic > maxSpendPerDayUsdcAtomic) {
|
|
39
|
+
throw new Error(`x402 daily spend cap reached (${formatUsdcAtomic(maxSpendPerDayUsdcAtomic)} USDC per day, ${formatUsdcAtomic(spentToday)} USDC already spent). Adjust MOLPHA_X402_MAX_SPEND_PER_DAY_USDC or wait until tomorrow.`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Checks a proposed x402 round's price against the per-round and daily
|
|
44
|
+
* spend caps, without recording the spend (call {@link recordX402Spend}
|
|
45
|
+
* once the round actually settles/funds).
|
|
46
|
+
*/
|
|
47
|
+
export function checkX402SpendCap(priceAtomic, maxPriceUsdcAtomic, maxSpendPerDayUsdcAtomic) {
|
|
48
|
+
checkX402PerRoundCap(priceAtomic, maxPriceUsdcAtomic);
|
|
49
|
+
checkX402DailySpendCap(priceAtomic, maxSpendPerDayUsdcAtomic);
|
|
50
|
+
}
|
|
51
|
+
/** Records an actual x402 spend against the daily cap after funding succeeds. */
|
|
52
|
+
export function recordX402Spend(priceAtomic) {
|
|
53
|
+
const day = todayKey();
|
|
54
|
+
if (x402Spend.day !== day) {
|
|
55
|
+
x402Spend.day = day;
|
|
56
|
+
x402Spend.spentAtomic = 0n;
|
|
57
|
+
}
|
|
58
|
+
x402Spend.spentAtomic += priceAtomic;
|
|
59
|
+
}
|
|
60
|
+
export function previewWrite(action, summary) {
|
|
61
|
+
return { dryRun: true, action, summary };
|
|
62
|
+
}
|
package/dist/src/hex.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The gateway's REST surface (e.g. subscription round execute) expects a
|
|
3
|
+
* bare hex feedId with no `0x` prefix, but feedIds are handed back to callers
|
|
4
|
+
* (feed derivation output, on-chain reads) with the prefix attached. Passing a
|
|
5
|
+
* prefixed feedId straight through causes the gateway to 400 on that lookup.
|
|
6
|
+
*
|
|
7
|
+
* Normalize once at the tool boundary so callers can pass either form.
|
|
8
|
+
*/
|
|
9
|
+
export function normalizeFeedId(feedId) {
|
|
10
|
+
return feedId.startsWith("0x") || feedId.startsWith("0X") ? feedId.slice(2) : feedId;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The gateway emits fixed-width fields as minimal hex — a one-signer bitmap
|
|
14
|
+
* comes back as `"4"`, not 32 zero-padded bytes. Every consumer downstream is
|
|
15
|
+
* strict: the SDK's `toFixedHex`/`toFixedBytes` reject anything whose hex
|
|
16
|
+
* length is not exactly `bytes * 2` (`signersBitmap: expected 32 bytes, got
|
|
17
|
+
* 0.5`), and the Solana program reads the bitmap as a fixed 32-byte array
|
|
18
|
+
* indexed from the end (`bitmap[31 - (bit >> 3)]`).
|
|
19
|
+
*
|
|
20
|
+
* Both readings are big-endian, so left-zero-padding is the canonical widening.
|
|
21
|
+
* Do it at the server boundary — on everything emitted and everything ingested —
|
|
22
|
+
* rather than making callers memorize a width the server already knows.
|
|
23
|
+
*/
|
|
24
|
+
export function toCanonicalHex(value, bytes, label) {
|
|
25
|
+
const clean = stripQuotes(String(value))
|
|
26
|
+
.replace(/^0[xX]/, "")
|
|
27
|
+
.toLowerCase();
|
|
28
|
+
if (clean.length === 0) {
|
|
29
|
+
throw new Error(`${label}: expected ${bytes} bytes of hex, got an empty value`);
|
|
30
|
+
}
|
|
31
|
+
if (!/^[0-9a-f]+$/.test(clean)) {
|
|
32
|
+
throw new Error(`${label}: expected hex, got "${value}"`);
|
|
33
|
+
}
|
|
34
|
+
// Over-width is corruption (wrong field, wrong encoding), not a format nit.
|
|
35
|
+
if (clean.length > bytes * 2) {
|
|
36
|
+
throw new Error(`${label}: expected at most ${bytes} bytes (${bytes * 2} hex chars), got ${clean.length / 2}`);
|
|
37
|
+
}
|
|
38
|
+
return `0x${clean.padStart(bytes * 2, "0")}`;
|
|
39
|
+
}
|
|
40
|
+
function stripQuotes(value) {
|
|
41
|
+
const trimmed = value.trim();
|
|
42
|
+
const quoted = (trimmed.startsWith("'") && trimmed.endsWith("'")) ||
|
|
43
|
+
(trimmed.startsWith('"') && trimmed.endsWith('"'));
|
|
44
|
+
return quoted ? trimmed.slice(1, -1).trim() : trimmed;
|
|
45
|
+
}
|
package/dist/src/mcp.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { normalizeError } from "./errors.js";
|
|
2
|
+
export function jsonResult(value) {
|
|
3
|
+
return {
|
|
4
|
+
content: [
|
|
5
|
+
{
|
|
6
|
+
type: "text",
|
|
7
|
+
text: stringifyToolJson(value)
|
|
8
|
+
}
|
|
9
|
+
]
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export function errorResult(error) {
|
|
13
|
+
return {
|
|
14
|
+
isError: true,
|
|
15
|
+
content: [
|
|
16
|
+
{
|
|
17
|
+
type: "text",
|
|
18
|
+
text: stringifyToolJson(normalizeError(error))
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export function toolHandler(handler) {
|
|
24
|
+
return async (args) => {
|
|
25
|
+
try {
|
|
26
|
+
return jsonResult(await handler(args));
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
return errorResult(error);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export function stringifyToolJson(value) {
|
|
34
|
+
return JSON.stringify(toJsonSafe(value), null, 2) ?? "null";
|
|
35
|
+
}
|
|
36
|
+
function toJsonSafe(value, seen = new WeakSet()) {
|
|
37
|
+
if (value === null || value === undefined) {
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
if (typeof value === "bigint") {
|
|
41
|
+
return value.toString();
|
|
42
|
+
}
|
|
43
|
+
if (typeof value !== "object") {
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
if (value instanceof Uint8Array) {
|
|
47
|
+
return bytesToHex(value);
|
|
48
|
+
}
|
|
49
|
+
if (isPublicKeyLike(value)) {
|
|
50
|
+
return value.toBase58();
|
|
51
|
+
}
|
|
52
|
+
if (isBnLike(value)) {
|
|
53
|
+
return value.toString(10);
|
|
54
|
+
}
|
|
55
|
+
if (seen.has(value)) {
|
|
56
|
+
return "[Circular]";
|
|
57
|
+
}
|
|
58
|
+
seen.add(value);
|
|
59
|
+
if (Array.isArray(value)) {
|
|
60
|
+
const out = value.map((item) => toJsonSafe(item, seen));
|
|
61
|
+
seen.delete(value);
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
const out = {};
|
|
65
|
+
for (const [key, item] of Object.entries(value)) {
|
|
66
|
+
out[key] = toJsonSafe(item, seen);
|
|
67
|
+
}
|
|
68
|
+
seen.delete(value);
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
function isPublicKeyLike(value) {
|
|
72
|
+
return value.constructor.name === "PublicKey" && "toBase58" in value && typeof value.toBase58 === "function";
|
|
73
|
+
}
|
|
74
|
+
function isBnLike(value) {
|
|
75
|
+
return (value.constructor.name === "BN" &&
|
|
76
|
+
"toString" in value &&
|
|
77
|
+
typeof value.toString === "function" &&
|
|
78
|
+
"words" in value);
|
|
79
|
+
}
|
|
80
|
+
function bytesToHex(bytes) {
|
|
81
|
+
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
82
|
+
}
|
package/dist/src/sdk.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import * as MolphaSdkModule from "@molpha-oracle/sdk";
|
|
2
|
+
export const MolphaSdk = MolphaSdkModule;
|
|
3
|
+
export function getSdkExport(name) {
|
|
4
|
+
return MolphaSdk[name];
|
|
5
|
+
}
|
|
6
|
+
export function requireSdkExport(name) {
|
|
7
|
+
const value = getSdkExport(name);
|
|
8
|
+
if (value === undefined || value === null) {
|
|
9
|
+
throw new Error(`@molpha-oracle/sdk does not export ${name}`);
|
|
10
|
+
}
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "./env.js";
|
|
3
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
import { registerTools } from "./tools/index.js";
|
|
6
|
+
const server = new McpServer({
|
|
7
|
+
name: "molpha-mcp",
|
|
8
|
+
version: "0.1.0"
|
|
9
|
+
});
|
|
10
|
+
registerTools(server);
|
|
11
|
+
try {
|
|
12
|
+
await server.connect(new StdioServerTransport());
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
console.error(error instanceof Error ? error.message : error);
|
|
16
|
+
process.exitCode = 1;
|
|
17
|
+
}
|