@true402.dev/mcp-server 0.4.0 → 0.6.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.js +16 -8
- package/dist/tools/discover.d.ts +6 -0
- package/dist/tools/discover.js +93 -0
- package/dist/tools/stalls.d.ts +33 -0
- package/dist/tools/stalls.js +1 -1
- package/dist/x402-pay.d.ts +10 -0
- package/dist/x402-pay.js +62 -0
- package/package.json +4 -2
package/dist/index.js
CHANGED
|
@@ -17,21 +17,29 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
17
17
|
import { registerModelsTool } from "./tools/models.js";
|
|
18
18
|
import { registerChatTool } from "./tools/chat.js";
|
|
19
19
|
import { registerStallTools } from "./tools/stalls.js";
|
|
20
|
+
import { registerDiscoveredStalls } from "./tools/discover.js";
|
|
20
21
|
const SERVER_URL = process.env.SERVER_URL ?? "https://true402.dev/api";
|
|
21
22
|
const WALLET_PRIVATE_KEY = process.env.WALLET_PRIVATE_KEY;
|
|
22
23
|
const server = new McpServer({
|
|
23
24
|
name: "true402",
|
|
24
|
-
version: "0.
|
|
25
|
+
version: "0.6.0",
|
|
25
26
|
});
|
|
26
|
-
// Register tools
|
|
27
|
-
registerModelsTool(server, SERVER_URL);
|
|
28
|
-
registerChatTool(server, SERVER_URL, WALLET_PRIVATE_KEY);
|
|
29
|
-
// Non-LLM paid stalls: web utilities (seo_audit, web_extract, link_preview,
|
|
30
|
-
// robots_check, headers_check) + Base on-chain signals (token_safety, new_pairs,
|
|
31
|
-
// liquidity_pulls, whale_swaps) — all x402-gated, USDC on Base.
|
|
32
|
-
registerStallTools(server, SERVER_URL, WALLET_PRIVATE_KEY);
|
|
33
27
|
// Start server with stdio transport
|
|
34
28
|
async function main() {
|
|
29
|
+
// Always-present built-in tools (LLM router — chat uses a $ref body + streaming, so it stays
|
|
30
|
+
// hand-written rather than discovered).
|
|
31
|
+
registerModelsTool(server, SERVER_URL);
|
|
32
|
+
registerChatTool(server, SERVER_URL, WALLET_PRIVATE_KEY);
|
|
33
|
+
// Paid stalls: ASK THE SERVER. Fetch its OpenAPI spec and register an MCP tool for every paid
|
|
34
|
+
// POST endpoint (x-payment-info), so a new marketplace stall surfaces automatically with no MCP
|
|
35
|
+
// release. Falls back to the built-in stall list only if the server is unreachable at startup.
|
|
36
|
+
const discovered = await registerDiscoveredStalls(server, SERVER_URL, WALLET_PRIVATE_KEY);
|
|
37
|
+
if (discovered) {
|
|
38
|
+
console.error(`true402 MCP: discovered ${discovered.length} paid stalls from ${SERVER_URL} (${discovered.join(", ")})`);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
registerStallTools(server, SERVER_URL, WALLET_PRIVATE_KEY);
|
|
42
|
+
}
|
|
35
43
|
const transport = new StdioServerTransport();
|
|
36
44
|
await server.connect(transport);
|
|
37
45
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
/**
|
|
3
|
+
* Fetch `${baseUrl}/openapi.json` and register an MCP tool for each paid POST stall.
|
|
4
|
+
* Returns the registered tool names, or null if discovery failed (caller should fall back).
|
|
5
|
+
*/
|
|
6
|
+
export declare function registerDiscoveredStalls(server: McpServer, baseUrl: string, walletPrivateKey: string | undefined): Promise<string[] | null>;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { registerStall } from "./stalls.js";
|
|
3
|
+
/** Map one JSON-Schema property to a zod validator (covers the property kinds the stalls use). */
|
|
4
|
+
function toZod(prop) {
|
|
5
|
+
let base;
|
|
6
|
+
if (Array.isArray(prop.enum) && prop.enum.length > 0) {
|
|
7
|
+
base = z.enum(prop.enum.map(String));
|
|
8
|
+
}
|
|
9
|
+
else if (prop.type === "number" || prop.type === "integer") {
|
|
10
|
+
base = z.number();
|
|
11
|
+
}
|
|
12
|
+
else if (prop.type === "boolean") {
|
|
13
|
+
base = z.boolean();
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
base = z.string();
|
|
17
|
+
}
|
|
18
|
+
return prop.description ? base.describe(prop.description) : base;
|
|
19
|
+
}
|
|
20
|
+
/** Build a zod raw shape from a request-body JSON Schema's `properties` + `required`. */
|
|
21
|
+
function bodyToShape(schema) {
|
|
22
|
+
const shape = {};
|
|
23
|
+
const required = new Set(schema?.required ?? []);
|
|
24
|
+
for (const [key, prop] of Object.entries(schema?.properties ?? {})) {
|
|
25
|
+
const v = toZod(prop);
|
|
26
|
+
shape[key] = required.has(key) ? v : v.optional();
|
|
27
|
+
}
|
|
28
|
+
return shape;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Fetch `${baseUrl}/openapi.json` and register an MCP tool for each paid POST stall.
|
|
32
|
+
* Returns the registered tool names, or null if discovery failed (caller should fall back).
|
|
33
|
+
*/
|
|
34
|
+
export async function registerDiscoveredStalls(server, baseUrl, walletPrivateKey) {
|
|
35
|
+
let spec;
|
|
36
|
+
try {
|
|
37
|
+
const controller = new AbortController();
|
|
38
|
+
const timer = setTimeout(() => controller.abort(), 10_000);
|
|
39
|
+
const res = await fetch(`${baseUrl}/openapi.json`, {
|
|
40
|
+
headers: { accept: "application/json" },
|
|
41
|
+
signal: controller.signal,
|
|
42
|
+
});
|
|
43
|
+
clearTimeout(timer);
|
|
44
|
+
if (!res.ok)
|
|
45
|
+
throw new Error(`HTTP ${res.status}`);
|
|
46
|
+
spec = (await res.json());
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
50
|
+
console.error(`true402 MCP: stall discovery from ${baseUrl}/openapi.json failed (${msg}); using built-in stall list`);
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
// OpenAPI path keys carry the base path (e.g. /api/v1/x); strip it so the tool path is relative
|
|
54
|
+
// to baseUrl (which already includes /api), matching how payAndFetch joins baseUrl + path.
|
|
55
|
+
let basePath = "";
|
|
56
|
+
try {
|
|
57
|
+
basePath = new URL(baseUrl).pathname.replace(/\/+$/, "");
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
/* baseUrl is relative — keys are already root-relative */
|
|
61
|
+
}
|
|
62
|
+
const registered = [];
|
|
63
|
+
for (const [key, ops] of Object.entries(spec.paths ?? {})) {
|
|
64
|
+
const op = ops.post;
|
|
65
|
+
if (!op || op["x-payment-info"] === undefined)
|
|
66
|
+
continue; // paid stalls only
|
|
67
|
+
if (key.endsWith("/chat/completions"))
|
|
68
|
+
continue; // chat is special-cased (hardcoded)
|
|
69
|
+
const rel = basePath && key.startsWith(basePath) ? key.slice(basePath.length) : key;
|
|
70
|
+
const toolName = (rel.replace(/\/+$/, "").split("/").pop() ?? "").replace(/-/g, "_");
|
|
71
|
+
if (!toolName)
|
|
72
|
+
continue;
|
|
73
|
+
const bodySchema = op.requestBody?.content?.["application/json"]?.schema;
|
|
74
|
+
const description = (op.description ?? op.summary ?? `Paid x402 stall ${rel}`) +
|
|
75
|
+
" (PAID x402 service — USDC on Base; the MCP server needs a funded wallet to settle.)";
|
|
76
|
+
registerStall(server, baseUrl, walletPrivateKey, {
|
|
77
|
+
toolName,
|
|
78
|
+
path: rel,
|
|
79
|
+
description,
|
|
80
|
+
inputSchema: bodyToShape(bodySchema),
|
|
81
|
+
// The discovered zod shape already mirrors the body, so pass validated args straight through.
|
|
82
|
+
buildBody: (args) => {
|
|
83
|
+
const body = {};
|
|
84
|
+
for (const [k, v] of Object.entries(args ?? {}))
|
|
85
|
+
if (v !== undefined)
|
|
86
|
+
body[k] = v;
|
|
87
|
+
return body;
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
registered.push(toolName);
|
|
91
|
+
}
|
|
92
|
+
return registered;
|
|
93
|
+
}
|
package/dist/tools/stalls.d.ts
CHANGED
|
@@ -1,4 +1,37 @@
|
|
|
1
1
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import type { ZodRawShapeCompat, ShapeOutput } from "@modelcontextprotocol/sdk/server/zod-compat.js";
|
|
3
|
+
/**
|
|
4
|
+
* Paid, non-LLM stalls exposed by the true402 marketplace.
|
|
5
|
+
*
|
|
6
|
+
* Each of these is an x402-gated POST endpoint that takes a small JSON body and
|
|
7
|
+
* returns a structured JSON report. They are all PAID services (USDC on Base):
|
|
8
|
+
* the MCP server must have a funded wallet (WALLET_PRIVATE_KEY) configured for
|
|
9
|
+
* the calls to settle. With no wallet, the tools surface the 402 payment
|
|
10
|
+
* requirements instead of crashing.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* A generic paid stall: an MCP tool that POSTs a body to an x402-gated path and
|
|
14
|
+
* returns the parsed JSON report. The input type is inferred from the zod raw
|
|
15
|
+
* shape `Schema`, and the body is built from the validated tool args via
|
|
16
|
+
* `buildBody`.
|
|
17
|
+
*/
|
|
18
|
+
export interface StallSpec<Schema extends ZodRawShapeCompat> {
|
|
19
|
+
/** MCP tool name (e.g. "seo_audit"). */
|
|
20
|
+
toolName: string;
|
|
21
|
+
/** Server-side endpoint path (e.g. "/v1/seo-audit"). */
|
|
22
|
+
path: string;
|
|
23
|
+
/** Agent-facing description. Should make clear this is a PAID x402 service. */
|
|
24
|
+
description: string;
|
|
25
|
+
/** Zod raw shape (plain object of validators) describing the tool inputs. */
|
|
26
|
+
inputSchema: Schema;
|
|
27
|
+
/** Map validated args to the JSON request body sent to the endpoint. */
|
|
28
|
+
buildBody: (args: ShapeOutput<Schema>) => Record<string, unknown>;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Register a single paid stall as an MCP tool. The handler runs the shared
|
|
32
|
+
* x402 pay-and-retry flow and maps the result into the MCP content shape.
|
|
33
|
+
*/
|
|
34
|
+
export declare function registerStall<Schema extends ZodRawShapeCompat>(server: McpServer, baseUrl: string, walletPrivateKey: string | undefined, spec: StallSpec<Schema>): void;
|
|
2
35
|
/**
|
|
3
36
|
* Register all nine non-LLM paid stalls on the given MCP server: the web utilities
|
|
4
37
|
* (seo_audit, web_extract, link_preview, robots_check, headers_check) and the Base
|
package/dist/tools/stalls.js
CHANGED
|
@@ -4,7 +4,7 @@ import { payAndFetch } from "../x402-pay.js";
|
|
|
4
4
|
* Register a single paid stall as an MCP tool. The handler runs the shared
|
|
5
5
|
* x402 pay-and-retry flow and maps the result into the MCP content shape.
|
|
6
6
|
*/
|
|
7
|
-
function registerStall(server, baseUrl, walletPrivateKey, spec) {
|
|
7
|
+
export function registerStall(server, baseUrl, walletPrivateKey, spec) {
|
|
8
8
|
const handler = async (args) => {
|
|
9
9
|
const body = spec.buildBody(args);
|
|
10
10
|
const result = await payAndFetch(baseUrl, spec.path, body, walletPrivateKey);
|
package/dist/x402-pay.d.ts
CHANGED
|
@@ -47,6 +47,16 @@ export interface EVMPaymentRequirement {
|
|
|
47
47
|
version?: string;
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Guard a 402 requirement BEFORE signing: a hostile/buggy server must not make the wallet sign a
|
|
52
|
+
* draining amount or an off-spec asset. Returns a refusal string, or null if the payment is allowed.
|
|
53
|
+
* - amount must be ≤ `maxUsdc` (the client-side spend ceiling, MAX_PAYMENT_USDC).
|
|
54
|
+
* - asset must be the canonical USDC on a supported chain (no arbitrary token/verifyingContract).
|
|
55
|
+
*/
|
|
56
|
+
export declare function assessRequirement(req: EVMPaymentRequirement, maxUsdc: number): string | null;
|
|
57
|
+
/** Refuse to send a signed payment over cleartext (the X-PAYMENT header would be interceptable).
|
|
58
|
+
* Returns a refusal string, or null if the URL is https (or localhost for dev). */
|
|
59
|
+
export declare function requireSecureUrl(url: string): string | null;
|
|
50
60
|
/**
|
|
51
61
|
* Sign an EIP-3009 TransferWithAuthorization and return the x402 payment
|
|
52
62
|
* payload. The EIP-712 domain is derived from the requirement (network +
|
package/dist/x402-pay.js
CHANGED
|
@@ -65,6 +65,57 @@ function resolveChainId(network) {
|
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
|
+
// Canonical USDC per chain — the ONLY token/chain this client will ever sign a payment for, so a
|
|
69
|
+
// hostile or buggy server cannot redirect the wallet's signature to an arbitrary token/chain.
|
|
70
|
+
const USDC_BY_CHAIN = {
|
|
71
|
+
8453: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // Base mainnet
|
|
72
|
+
84532: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", // Base Sepolia
|
|
73
|
+
1: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // Ethereum mainnet
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Guard a 402 requirement BEFORE signing: a hostile/buggy server must not make the wallet sign a
|
|
77
|
+
* draining amount or an off-spec asset. Returns a refusal string, or null if the payment is allowed.
|
|
78
|
+
* - amount must be ≤ `maxUsdc` (the client-side spend ceiling, MAX_PAYMENT_USDC).
|
|
79
|
+
* - asset must be the canonical USDC on a supported chain (no arbitrary token/verifyingContract).
|
|
80
|
+
*/
|
|
81
|
+
export function assessRequirement(req, maxUsdc) {
|
|
82
|
+
const raw = req.amount ?? req.maxAmountRequired;
|
|
83
|
+
if (raw === undefined)
|
|
84
|
+
return "the 402 has no amount; refusing to sign.";
|
|
85
|
+
let amountUsdc;
|
|
86
|
+
try {
|
|
87
|
+
amountUsdc = Number(BigInt(raw)) / 1e6; // USDC has 6 decimals
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return "the 402 amount is unparseable; refusing to sign.";
|
|
91
|
+
}
|
|
92
|
+
if (!Number.isFinite(amountUsdc) || amountUsdc < 0)
|
|
93
|
+
return "the 402 amount is invalid; refusing to sign.";
|
|
94
|
+
if (Number.isFinite(maxUsdc) && amountUsdc > maxUsdc) {
|
|
95
|
+
return `refusing to auto-pay $${amountUsdc} USDC — it exceeds the MAX_PAYMENT_USDC ceiling of $${maxUsdc}. Raise MAX_PAYMENT_USDC if this is intended.`;
|
|
96
|
+
}
|
|
97
|
+
const usdc = USDC_BY_CHAIN[resolveChainId(req.network)];
|
|
98
|
+
if (!usdc || (req.asset ?? "").toLowerCase() !== usdc.toLowerCase()) {
|
|
99
|
+
return "refusing to pay: the 402's asset/network is not canonical USDC on a supported Base network.";
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
/** Refuse to send a signed payment over cleartext (the X-PAYMENT header would be interceptable).
|
|
104
|
+
* Returns a refusal string, or null if the URL is https (or localhost for dev). */
|
|
105
|
+
export function requireSecureUrl(url) {
|
|
106
|
+
let u;
|
|
107
|
+
try {
|
|
108
|
+
u = new URL(url);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return `invalid SERVER_URL: ${url}`;
|
|
112
|
+
}
|
|
113
|
+
const local = u.hostname === "localhost" || u.hostname === "127.0.0.1" || u.hostname === "::1";
|
|
114
|
+
if (u.protocol !== "https:" && !local) {
|
|
115
|
+
return `refusing to send a signed payment over cleartext (${u.protocol}//${u.hostname}); use an https SERVER_URL (or localhost for dev).`;
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
68
119
|
/**
|
|
69
120
|
* Sign an EIP-3009 TransferWithAuthorization and return the x402 payment
|
|
70
121
|
* payload. The EIP-712 domain is derived from the requirement (network +
|
|
@@ -150,6 +201,10 @@ export async function signEIP3009Payment(privateKey, requirement) {
|
|
|
150
201
|
*/
|
|
151
202
|
export async function payAndFetch(baseUrl, path, body, walletPrivateKey) {
|
|
152
203
|
const url = `${baseUrl}${path}`;
|
|
204
|
+
// Step 0: never sign/send a payment over cleartext (X-PAYMENT would be interceptable).
|
|
205
|
+
const insecure = requireSecureUrl(url);
|
|
206
|
+
if (insecure)
|
|
207
|
+
return { ok: false, message: insecure };
|
|
153
208
|
// Step 1: first request, no payment header.
|
|
154
209
|
let firstResponse;
|
|
155
210
|
try {
|
|
@@ -210,6 +265,13 @@ export async function payAndFetch(baseUrl, path, body, walletPrivateKey) {
|
|
|
210
265
|
].join("\n"),
|
|
211
266
|
};
|
|
212
267
|
}
|
|
268
|
+
// Spend ceiling + asset/network pin: a hostile server must not make our wallet sign a draining
|
|
269
|
+
// amount or a payment to an arbitrary token/chain. Refuse BEFORE signing.
|
|
270
|
+
const maxUsdc = Number(process.env.MAX_PAYMENT_USDC ?? "0.10");
|
|
271
|
+
const refusal = assessRequirement(evmRequirement, maxUsdc);
|
|
272
|
+
if (refusal) {
|
|
273
|
+
return { ok: false, paymentRequired: true, message: refusal };
|
|
274
|
+
}
|
|
213
275
|
// Step 4: sign the EIP-3009 authorization and base64-encode the payload.
|
|
214
276
|
let xPaymentHeader;
|
|
215
277
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@true402.dev/mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"mcpName": "dev.true402/mcp-server",
|
|
5
5
|
"description": "MCP server for the true402 machine-native marketplace — pay-per-call AI + web + Base on-chain tools over x402 (USDC on Base): LLM inference, SEO/GEO audit, web extract, link preview, robots/AI-crawler check, security headers, and on-chain DeFi trading signals (token rug/honeypot safety, new token pairs, liquidity-pull/rug alerts, whale swaps).",
|
|
6
6
|
"type": "module",
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"build": "tsc",
|
|
17
17
|
"start": "node dist/index.js",
|
|
18
18
|
"dev": "tsx src/index.ts",
|
|
19
|
+
"test": "vitest run",
|
|
19
20
|
"prepublishOnly": "npm run build"
|
|
20
21
|
},
|
|
21
22
|
"keywords": [
|
|
@@ -51,7 +52,8 @@
|
|
|
51
52
|
"devDependencies": {
|
|
52
53
|
"@types/node": "^22.0.0",
|
|
53
54
|
"tsx": "^4.19.0",
|
|
54
|
-
"typescript": "^5.7.0"
|
|
55
|
+
"typescript": "^5.7.0",
|
|
56
|
+
"vitest": "^2.1.0"
|
|
55
57
|
},
|
|
56
58
|
"engines": {
|
|
57
59
|
"node": ">=20.0.0"
|