@hpp-io/x402-mcp-bridge 0.0.1

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.
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Env parsing + validation. The bridge runs as a stdio subprocess spawned
3
+ * by the MCP host; *all* of its inputs come from env. We surface clear
4
+ * errors before doing any I/O so misconfigured Claude Desktop entries fail
5
+ * loudly at startup rather than mid-call.
6
+ */
7
+ import { z } from "zod";
8
+ declare const Schema: z.ZodObject<{
9
+ DELEGATE_PRIVATE_KEY: z.ZodString;
10
+ SAFE_ADDRESS: z.ZodString;
11
+ ALLOWANCE_MODULE_ADDRESS: z.ZodString;
12
+ USDCE_ADDRESS: z.ZodString;
13
+ RESOURCE_SERVER_URL: z.ZodString;
14
+ HPP_RPC_URL: z.ZodString;
15
+ HPP_NETWORK: z.ZodString;
16
+ TOPUP_AMOUNT_ATOMIC: z.ZodOptional<z.ZodString>;
17
+ TOPUP_HEADROOM_X: z.ZodDefault<z.ZodOptional<z.ZodString>>;
18
+ LOG_LEVEL: z.ZodDefault<z.ZodOptional<z.ZodEnum<["off", "info", "debug"]>>>;
19
+ }, "strip", z.ZodTypeAny, {
20
+ DELEGATE_PRIVATE_KEY: string;
21
+ SAFE_ADDRESS: string;
22
+ ALLOWANCE_MODULE_ADDRESS: string;
23
+ USDCE_ADDRESS: string;
24
+ RESOURCE_SERVER_URL: string;
25
+ HPP_RPC_URL: string;
26
+ HPP_NETWORK: string;
27
+ TOPUP_HEADROOM_X: string;
28
+ LOG_LEVEL: "off" | "info" | "debug";
29
+ TOPUP_AMOUNT_ATOMIC?: string | undefined;
30
+ }, {
31
+ DELEGATE_PRIVATE_KEY: string;
32
+ SAFE_ADDRESS: string;
33
+ ALLOWANCE_MODULE_ADDRESS: string;
34
+ USDCE_ADDRESS: string;
35
+ RESOURCE_SERVER_URL: string;
36
+ HPP_RPC_URL: string;
37
+ HPP_NETWORK: string;
38
+ TOPUP_AMOUNT_ATOMIC?: string | undefined;
39
+ TOPUP_HEADROOM_X?: string | undefined;
40
+ LOG_LEVEL?: "off" | "info" | "debug" | undefined;
41
+ }>;
42
+ export type Config = z.infer<typeof Schema> & {
43
+ chainId: number;
44
+ };
45
+ export declare function loadConfig(env?: NodeJS.ProcessEnv): Config;
46
+ export {};
package/dist/config.js ADDED
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Env parsing + validation. The bridge runs as a stdio subprocess spawned
3
+ * by the MCP host; *all* of its inputs come from env. We surface clear
4
+ * errors before doing any I/O so misconfigured Claude Desktop entries fail
5
+ * loudly at startup rather than mid-call.
6
+ */
7
+ import { z } from "zod";
8
+ const HEX_ADDR = /^0x[0-9a-fA-F]{40}$/;
9
+ const HEX_KEY = /^0x[0-9a-fA-F]{64}$/;
10
+ const Schema = z.object({
11
+ // Delegate EOA — signs EIP-3009 payments + executeAllowanceTransfer.
12
+ // NOTE: PoC accepts the raw key. Future signers (keychain, server-wallet)
13
+ // will short-circuit this field.
14
+ DELEGATE_PRIVATE_KEY: z.string().regex(HEX_KEY, "expected 0x + 64 hex chars"),
15
+ // Safe wallet that holds the user's USDC.e.
16
+ SAFE_ADDRESS: z.string().regex(HEX_ADDR),
17
+ // Deployed AllowanceModule.
18
+ ALLOWANCE_MODULE_ADDRESS: z.string().regex(HEX_ADDR),
19
+ // Asset paid in (USDC.e on HPP).
20
+ USDCE_ADDRESS: z.string().regex(HEX_ADDR),
21
+ // Remote MCP server we proxy to (the seller).
22
+ RESOURCE_SERVER_URL: z.string().url(),
23
+ // Chain RPC.
24
+ HPP_RPC_URL: z.string().url(),
25
+ // CAIP-2 network identifier used by @x402/evm.
26
+ // e.g. "eip155:181228" (HPP Sepolia) / "eip155:190415" (HPP Mainnet).
27
+ HPP_NETWORK: z.string().regex(/^eip155:\d+$/),
28
+ // Optional knobs ------------------------------------------------------
29
+ // How much to pull on each topup, in atomic USDC.e units. If unset,
30
+ // the bridge tops up "just enough" for the upcoming payment + small
31
+ // headroom (10x the price, capped to remaining allowance).
32
+ TOPUP_AMOUNT_ATOMIC: z
33
+ .string()
34
+ .regex(/^\d+$/)
35
+ .optional(),
36
+ // Headroom multiplier for the dynamic topup amount above. 10 = pull
37
+ // 10× the next payment so subsequent calls don't always trigger a
38
+ // topup tx. Defaults to 10.
39
+ TOPUP_HEADROOM_X: z
40
+ .string()
41
+ .regex(/^\d+$/)
42
+ .optional()
43
+ .default("10"),
44
+ // Logging. "off" = silent (good for clean stdio), "debug" = verbose stderr.
45
+ LOG_LEVEL: z.enum(["off", "info", "debug"]).optional().default("info"),
46
+ });
47
+ export function loadConfig(env = process.env) {
48
+ const parsed = Schema.safeParse(env);
49
+ if (!parsed.success) {
50
+ const issues = parsed.error.issues
51
+ .map((i) => ` - ${i.path.join(".") || "(root)"}: ${i.message}`)
52
+ .join("\n");
53
+ throw new Error("x402-mcp-bridge: invalid config from env\n" + issues + "\n");
54
+ }
55
+ const v = parsed.data;
56
+ const chainId = Number(v.HPP_NETWORK.split(":")[1]);
57
+ return { ...v, chainId };
58
+ }
59
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,MAAM,QAAQ,GAAG,qBAAqB,CAAC;AACvC,MAAM,OAAO,GAAG,qBAAqB,CAAC;AAEtC,MAAM,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC;IACtB,qEAAqE;IACrE,0EAA0E;IAC1E,iCAAiC;IACjC,oBAAoB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,4BAA4B,CAAC;IAE7E,4CAA4C;IAC5C,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC;IAExC,4BAA4B;IAC5B,wBAAwB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC;IAEpD,iCAAiC;IACjC,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC;IAEzC,8CAA8C;IAC9C,mBAAmB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE;IAErC,aAAa;IACb,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE;IAE7B,+CAA+C;IAC/C,sEAAsE;IACtE,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,cAAc,CAAC;IAE7C,wEAAwE;IACxE,oEAAoE;IACpE,oEAAoE;IACpE,2DAA2D;IAC3D,mBAAmB,EAAE,CAAC;SACnB,MAAM,EAAE;SACR,KAAK,CAAC,OAAO,CAAC;SACd,QAAQ,EAAE;IAEb,oEAAoE;IACpE,kEAAkE;IAClE,4BAA4B;IAC5B,gBAAgB,EAAE,CAAC;SAChB,MAAM,EAAE;SACR,KAAK,CAAC,OAAO,CAAC;SACd,QAAQ,EAAE;SACV,OAAO,CAAC,IAAI,CAAC;IAEhB,4EAA4E;IAC5E,SAAS,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC;CACvE,CAAC,CAAC;AAMH,MAAM,UAAU,UAAU,CAAC,MAAyB,OAAO,CAAC,GAAG;IAC7D,MAAM,MAAM,GAAG,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;IACrC,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACpB,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM;aAC/B,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,QAAQ,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC;aAC/D,IAAI,CAAC,IAAI,CAAC,CAAC;QACd,MAAM,IAAI,KAAK,CACb,4CAA4C,GAAG,MAAM,GAAG,IAAI,CAC7D,CAAC;IACJ,CAAC;IACD,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC;IACtB,MAAM,OAAO,GAAG,MAAM,CAAC,CAAC,CAAC,WAAW,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACpD,OAAO,EAAE,GAAG,CAAC,EAAE,OAAO,EAAE,CAAC;AAC3B,CAAC"}
@@ -0,0 +1 @@
1
+ export declare function runBridge(env?: NodeJS.ProcessEnv): Promise<void>;
package/dist/index.js ADDED
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Bridge entry — wires config → signer → autoTopup → upstream client →
3
+ * stdio server, then runs forever (until the host closes our stdin).
4
+ *
5
+ * Public surface for callers (npx, programmatic):
6
+ * - default export: `runBridge()` — runs main loop
7
+ * - named exports: types only (config, signer, autoTopup) for tests
8
+ */
9
+ import { loadConfig } from "./config.js";
10
+ import { setLogLevel, log } from "./log.js";
11
+ import { RawEoaSigner } from "./signers/raw-eoa.js";
12
+ import { AutoTopup } from "./autoTopup.js";
13
+ import { connectUpstream } from "./client.js";
14
+ import { startBridgeServer } from "./server.js";
15
+ const PKG_NAME = "@hpp-io/x402-mcp-bridge";
16
+ const PKG_VERSION = "0.0.1"; // mirrors package.json — bump together
17
+ export async function runBridge(env = process.env) {
18
+ const cfg = loadConfig(env);
19
+ setLogLevel(cfg.LOG_LEVEL);
20
+ log.info("bridge.start", {
21
+ name: PKG_NAME,
22
+ version: PKG_VERSION,
23
+ chain: cfg.HPP_NETWORK,
24
+ safe: cfg.SAFE_ADDRESS,
25
+ resourceServer: cfg.RESOURCE_SERVER_URL,
26
+ });
27
+ // ---- Signer (PoC: raw EOA from env) ---------------------------------
28
+ const signer = new RawEoaSigner(cfg.DELEGATE_PRIVATE_KEY);
29
+ log.info("signer.loaded", { address: signer.address });
30
+ // ---- AutoTopup --------------------------------------------------------
31
+ const autoTopup = new AutoTopup(signer, cfg.SAFE_ADDRESS, cfg.ALLOWANCE_MODULE_ADDRESS, cfg.USDCE_ADDRESS, cfg.chainId, cfg.HPP_RPC_URL, {
32
+ headroomX: BigInt(cfg.TOPUP_HEADROOM_X),
33
+ fixedAmountAtomic: cfg.TOPUP_AMOUNT_ATOMIC
34
+ ? BigInt(cfg.TOPUP_AMOUNT_ATOMIC)
35
+ : undefined,
36
+ });
37
+ // ---- Upstream MCP client (with x402 payment wrapper) ----------------
38
+ const upstream = await connectUpstream({
39
+ url: cfg.RESOURCE_SERVER_URL,
40
+ network: cfg.HPP_NETWORK,
41
+ signer,
42
+ autoTopup,
43
+ bridgeName: PKG_NAME,
44
+ bridgeVersion: PKG_VERSION,
45
+ });
46
+ // ---- stdio MCP server (host-facing) ---------------------------------
47
+ await startBridgeServer({
48
+ upstream,
49
+ name: PKG_NAME,
50
+ version: PKG_VERSION,
51
+ });
52
+ // Graceful shutdown: when the host closes our stdin (or Claude Desktop
53
+ // restarts), node exits. Belt-and-suspenders: handle signals too.
54
+ const shutdown = async (sig) => {
55
+ log.info("bridge.shutdown", { signal: sig });
56
+ await upstream.close();
57
+ process.exit(0);
58
+ };
59
+ process.on("SIGINT", () => void shutdown("SIGINT"));
60
+ process.on("SIGTERM", () => void shutdown("SIGTERM"));
61
+ }
62
+ // Auto-run when invoked directly (npx, node dist/index.js).
63
+ // `import.meta.url` matches `process.argv[1]` only for the entry script.
64
+ if (import.meta.url === `file://${process.argv[1]}`) {
65
+ runBridge().catch((err) => {
66
+ log.error("bridge.fatal", { err: err.message });
67
+ process.exit(1);
68
+ });
69
+ }
70
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/log.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ /**
2
+ * stderr-only logger.
3
+ *
4
+ * stdin/stdout are reserved for the MCP JSON-RPC protocol; any log line
5
+ * on stdout corrupts the protocol stream and will crash the host. All
6
+ * logging goes to stderr (which Claude Desktop / OpenClaw discard or
7
+ * route to their own debug output).
8
+ */
9
+ type Level = "off" | "info" | "debug";
10
+ export declare function setLogLevel(level: Level): void;
11
+ export declare const log: {
12
+ info: (msg: string, extra?: Record<string, unknown>) => void;
13
+ debug: (msg: string, extra?: Record<string, unknown>) => void;
14
+ error: (msg: string, extra?: Record<string, unknown>) => void;
15
+ };
16
+ export {};
package/dist/log.js ADDED
@@ -0,0 +1,24 @@
1
+ const order = { off: 0, info: 1, debug: 2 };
2
+ let current = "info";
3
+ export function setLogLevel(level) {
4
+ current = level;
5
+ }
6
+ function emit(level, msg, extra) {
7
+ if (order[current] < order[level])
8
+ return;
9
+ const line = extra
10
+ ? `[${level}] ${msg} ${JSON.stringify(extra)}`
11
+ : `[${level}] ${msg}`;
12
+ process.stderr.write(line + "\n");
13
+ }
14
+ export const log = {
15
+ info: (msg, extra) => emit("info", msg, extra),
16
+ debug: (msg, extra) => emit("debug", msg, extra),
17
+ error: (msg, extra) => {
18
+ // Errors always emit, regardless of level (off still suppresses).
19
+ if (current === "off")
20
+ return;
21
+ process.stderr.write(`[error] ${msg}` + (extra ? " " + JSON.stringify(extra) : "") + "\n");
22
+ },
23
+ };
24
+ //# sourceMappingURL=log.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"log.js","sourceRoot":"","sources":["../src/log.ts"],"names":[],"mappings":"AAUA,MAAM,KAAK,GAA0B,EAAE,GAAG,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC;AAEnE,IAAI,OAAO,GAAU,MAAM,CAAC;AAE5B,MAAM,UAAU,WAAW,CAAC,KAAY;IACtC,OAAO,GAAG,KAAK,CAAC;AAClB,CAAC;AAED,SAAS,IAAI,CAAC,KAAY,EAAE,GAAW,EAAE,KAA+B;IACtE,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC;QAAE,OAAO;IAC1C,MAAM,IAAI,GAAG,KAAK;QAChB,CAAC,CAAC,IAAI,KAAK,KAAK,GAAG,IAAI,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE;QAC9C,CAAC,CAAC,IAAI,KAAK,KAAK,GAAG,EAAE,CAAC;IACxB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;AACpC,CAAC;AAED,MAAM,CAAC,MAAM,GAAG,GAAG;IACjB,IAAI,EAAE,CAAC,GAAW,EAAE,KAA+B,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,KAAK,CAAC;IAChF,KAAK,EAAE,CAAC,GAAW,EAAE,KAA+B,EAAE,EAAE,CACtD,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,KAAK,CAAC;IAC3B,KAAK,EAAE,CAAC,GAAW,EAAE,KAA+B,EAAE,EAAE;QACtD,kEAAkE;QAClE,IAAI,OAAO,KAAK,KAAK;YAAE,OAAO;QAC9B,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,WAAW,GAAG,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,IAAI,CACrE,CAAC;IACJ,CAAC;CACF,CAAC"}
@@ -0,0 +1,7 @@
1
+ import type { UpstreamClient } from "./client.js";
2
+ export interface BridgeServerOptions {
3
+ upstream: UpstreamClient;
4
+ name: string;
5
+ version: string;
6
+ }
7
+ export declare function startBridgeServer(opts: BridgeServerOptions): Promise<void>;
package/dist/server.js ADDED
@@ -0,0 +1,56 @@
1
+ /**
2
+ * stdio MCP server — what the host (Claude Desktop / OpenClaw / Cursor)
3
+ * actually connects to.
4
+ *
5
+ * It's a thin proxy: tools are pulled from upstream on listTools, and
6
+ * callTool forwards through the payment-wrapped upstream client. The host
7
+ * sees a normal MCP server with paid tools that "just work".
8
+ *
9
+ * If autoTopup or the payment retry fails, the bridge surfaces the error
10
+ * as a tool-result with `isError: true` so the LLM can see what happened
11
+ * (e.g. "spend cap exceeded"). Throwing instead would crash the MCP RPC
12
+ * stream.
13
+ */
14
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
15
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
16
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
17
+ import { log } from "./log.js";
18
+ export async function startBridgeServer(opts) {
19
+ const server = new Server({ name: opts.name, version: opts.version }, { capabilities: { tools: {} } });
20
+ // ---- listTools — pass through from upstream -------------------------
21
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
22
+ const upstreamTools = await opts.upstream.base.listTools();
23
+ log.debug("listTools", { count: upstreamTools.tools.length });
24
+ return { tools: upstreamTools.tools };
25
+ });
26
+ // ---- callTool — forward through x402-aware wrapper ------------------
27
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
28
+ const { name, arguments: args } = req.params;
29
+ log.info("callTool.start", { name });
30
+ try {
31
+ const result = await opts.upstream.x402.callTool(name, (args ?? {}));
32
+ log.info("callTool.done", {
33
+ name,
34
+ paymentMade: result.paymentMade ?? false,
35
+ });
36
+ // Strip bridge-internal fields (paymentMade, paymentResponse) before
37
+ // returning to host — they're not part of the MCP CallToolResult shape.
38
+ return {
39
+ content: result.content,
40
+ isError: result.isError,
41
+ };
42
+ }
43
+ catch (err) {
44
+ const msg = err instanceof Error ? err.message : String(err);
45
+ log.error("callTool.failed", { name, err: msg });
46
+ return {
47
+ content: [{ type: "text", text: `bridge error: ${msg}` }],
48
+ isError: true,
49
+ };
50
+ }
51
+ });
52
+ const transport = new StdioServerTransport();
53
+ await server.connect(transport);
54
+ log.info("bridge.ready");
55
+ }
56
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACnE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EACL,qBAAqB,EACrB,sBAAsB,GACvB,MAAM,oCAAoC,CAAC;AAG5C,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAQ/B,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,IAAyB;IAC/D,MAAM,MAAM,GAAG,IAAI,MAAM,CACvB,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,EAC1C,EAAE,YAAY,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,CAChC,CAAC;IAEF,wEAAwE;IACxE,MAAM,CAAC,iBAAiB,CAAC,sBAAsB,EAAE,KAAK,IAAI,EAAE;QAC1D,MAAM,aAAa,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;QAC3D,GAAG,CAAC,KAAK,CAAC,WAAW,EAAE,EAAE,KAAK,EAAE,aAAa,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;QAC9D,OAAO,EAAE,KAAK,EAAE,aAAa,CAAC,KAAK,EAAE,CAAC;IACxC,CAAC,CAAC,CAAC;IAEH,wEAAwE;IACxE,MAAM,CAAC,iBAAiB,CAAC,qBAAqB,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;QAC5D,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,GAAG,GAAG,CAAC,MAAM,CAAC;QAC7C,GAAG,CAAC,IAAI,CAAC,gBAAgB,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC;QAErC,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAC9C,IAAI,EACJ,CAAC,IAAI,IAAI,EAAE,CAA4B,CACxC,CAAC;YAEF,GAAG,CAAC,IAAI,CAAC,eAAe,EAAE;gBACxB,IAAI;gBACJ,WAAW,EAAG,MAAoC,CAAC,WAAW,IAAI,KAAK;aACxE,CAAC,CAAC;YAEH,qEAAqE;YACrE,wEAAwE;YACxE,OAAO;gBACL,OAAO,EAAE,MAAM,CAAC,OAAO;gBACvB,OAAO,EAAE,MAAM,CAAC,OAAO;aACxB,CAAC;QACJ,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,GAAG,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAC7D,GAAG,CAAC,KAAK,CAAC,iBAAiB,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC;YACjD,OAAO;gBACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,iBAAiB,GAAG,EAAE,EAAE,CAAC;gBAClE,OAAO,EAAE,IAAI;aACd,CAAC;QACJ,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAChC,GAAG,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;AAC3B,CAAC"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Signer abstraction.
3
+ *
4
+ * The bridge needs two signing capabilities:
5
+ * 1. EIP-712 typed-data — for x402 EIP-3009 transferWithAuthorization
6
+ * and AllowanceModule.AllowanceTransfer.
7
+ * 2. Generic transactions — for sending the executeAllowanceTransfer
8
+ * call (the delegate is the tx sender + payer of gas).
9
+ *
10
+ * The PoC implementation is a raw-key viem account (`signers/raw-eoa.ts`).
11
+ * Future implementations swap in:
12
+ * - OS keychain wrapper (still raw key, just unsealed lazily)
13
+ * - HPP self-hosted server-wallet API (HTTP signing)
14
+ * - Coinbase CDP / Turnkey / Privy (if HPP support arrives)
15
+ * - ERC-4337 smart account session keys
16
+ *
17
+ * Importantly, x402 EIP-3009 verification on USDC.e (FiatTokenV2_2) accepts
18
+ * EIP-1271 contract signatures (verified 2026-04-30) — so a *contract*
19
+ * wallet (Safe, smart account) can also fit this interface, returning a
20
+ * Safe-format signature blob from `signTypedData`.
21
+ */
22
+ import type { Address, Hex, TypedDataDomain } from "viem";
23
+ export interface Signer {
24
+ /** Address that x402 / AllowanceModule sees as the signer / sender. */
25
+ readonly address: Address;
26
+ /**
27
+ * Sign EIP-712 typed data. The bridge uses this for:
28
+ * - EIP-3009 TransferWithAuthorization (x402 payment)
29
+ * - AllowanceModule AllowanceTransfer (autoTopup)
30
+ *
31
+ * Returns the signature in the form the verifier expects:
32
+ * - EOA: 65-byte ECDSA (r ‖ s ‖ v)
33
+ * - Contract wallet: implementation-specific blob accepted by
34
+ * EIP-1271 isValidSignature
35
+ */
36
+ signTypedData(args: {
37
+ domain: TypedDataDomain;
38
+ types: Record<string, {
39
+ name: string;
40
+ type: string;
41
+ }[]>;
42
+ primaryType: string;
43
+ message: Record<string, unknown>;
44
+ }): Promise<Hex>;
45
+ }
package/dist/signer.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=signer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signer.js","sourceRoot":"","sources":["../src/signer.ts"],"names":[],"mappings":""}
@@ -0,0 +1,12 @@
1
+ import type { Address, Hex, PrivateKeyAccount } from "viem";
2
+ import type { Signer } from "../signer.js";
3
+ export declare class RawEoaSigner implements Signer {
4
+ readonly address: Address;
5
+ private readonly account;
6
+ constructor(privateKey: Hex);
7
+ /** Expose the underlying viem account for callers that need it
8
+ * (writeContract / sendTransaction). Not part of the Signer interface;
9
+ * bridge-internal use only. */
10
+ get viemAccount(): PrivateKeyAccount;
11
+ signTypedData(args: Parameters<Signer["signTypedData"]>[0]): Promise<Hex>;
12
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * RawEoaSigner — viem PrivateKeyAccount wrapped to fit `Signer`.
3
+ *
4
+ * The most permissive signer. Accepts a 0x-prefixed 32-byte private key
5
+ * directly. Used as the PoC default; production deployments should layer
6
+ * an OS keychain or wallet service in front of this.
7
+ */
8
+ import { privateKeyToAccount } from "viem/accounts";
9
+ export class RawEoaSigner {
10
+ address;
11
+ account;
12
+ constructor(privateKey) {
13
+ this.account = privateKeyToAccount(privateKey);
14
+ this.address = this.account.address;
15
+ }
16
+ /** Expose the underlying viem account for callers that need it
17
+ * (writeContract / sendTransaction). Not part of the Signer interface;
18
+ * bridge-internal use only. */
19
+ get viemAccount() {
20
+ return this.account;
21
+ }
22
+ async signTypedData(args) {
23
+ // viem's signTypedData uses overload narrowing keyed on `primaryType`
24
+ // and the literal `types` shape. The bridge passes user-built typed
25
+ // data at runtime, so we widen via the loose overload.
26
+ return this.account.signTypedData(args);
27
+ }
28
+ }
29
+ //# sourceMappingURL=raw-eoa.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"raw-eoa.js","sourceRoot":"","sources":["../../src/signers/raw-eoa.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAKpD,MAAM,OAAO,YAAY;IACd,OAAO,CAAU;IACT,OAAO,CAAoB;IAE5C,YAAY,UAAe;QACzB,IAAI,CAAC,OAAO,GAAG,mBAAmB,CAAC,UAAU,CAAC,CAAC;QAC/C,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC;IACtC,CAAC;IAED;;oCAEgC;IAChC,IAAI,WAAW;QACb,OAAO,IAAI,CAAC,OAAO,CAAC;IACtB,CAAC;IAED,KAAK,CAAC,aAAa,CAAC,IAA4C;QAC9D,sEAAsE;QACtE,oEAAoE;QACpE,uDAAuD;QACvD,OAAO,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,IAAyD,CAAC,CAAC;IAC/F,CAAC;CACF"}
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@hpp-io/x402-mcp-bridge",
3
+ "version": "0.0.1",
4
+ "description": "MCP stdio bridge that lets Claude Desktop / OpenClaw / other MCP hosts make x402 payments on HPP — autoTopup from a Safe via AllowanceModule, signs EIP-3009 with a delegate EOA.",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "bin": {
10
+ "x402-mcp-bridge": "bin/x402-mcp-bridge.js",
11
+ "hpp-x402-safe-setup": "bin/hpp-x402-safe-setup.js",
12
+ "hpp-x402-safe-revoke": "bin/hpp-x402-safe-revoke.js"
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "bin",
17
+ "README.md",
18
+ "LICENSE"
19
+ ],
20
+ "engines": {
21
+ "node": ">=20"
22
+ },
23
+ "scripts": {
24
+ "build": "tsc",
25
+ "dev": "tsx src/index.ts",
26
+ "typecheck": "tsc --noEmit",
27
+ "prepublishOnly": "npm run typecheck && npm run build"
28
+ },
29
+ "publishConfig": {
30
+ "access": "public",
31
+ "provenance": true
32
+ },
33
+ "dependencies": {
34
+ "@modelcontextprotocol/sdk": "^1.0.0",
35
+ "@x402/core": "^2.10.0",
36
+ "@x402/evm": "^2.10.0",
37
+ "@x402/mcp": "^2.10.0",
38
+ "viem": "^2.21.54",
39
+ "zod": "^3.23.8"
40
+ },
41
+ "devDependencies": {
42
+ "@types/node": "^20.0.0",
43
+ "tsx": "^4.0.0",
44
+ "typescript": "^5.4.0"
45
+ }
46
+ }