@cmdoss/suipay-mcp 0.2.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/README.md ADDED
@@ -0,0 +1,103 @@
1
+ # @cmdoss/suipay-mcp
2
+
3
+ SuiPay MCP lets an MCP-capable agent pay MPP-gated HTTP resources from a
4
+ buyer-funded spend account. The buyer owns the account. The agent holds the
5
+ delegate key and can spend only under the on-chain grant (recipient bind,
6
+ per-payment cap, budget).
7
+
8
+ Pin `@cmdoss/suipay-mcp@0.2.0` in MCP config. `1.0.1` remains on the
9
+ registry as a historical release; do not install it for new agents.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ claude mcp add --scope user suipay -- npx -y @cmdoss/suipay-mcp@0.2.0
15
+ ```
16
+
17
+ No wallet secret belongs in this command or MCP config. There is no
18
+ Sign-In With Sui (SIWS) step.
19
+
20
+ ## First connection
21
+
22
+ First connection is a spend-account grant plus an agent-held delegate key.
23
+
24
+ 1. In the SuiPay console, fund a spend account and create a grant that
25
+ names the agent's delegate address.
26
+ 2. Persist that delegate key locally at `~/.suipay/credentials.json`
27
+ (mode `0600`). The process that holds this key is the only process
28
+ that can settle.
29
+ 3. Point MCP at the gateway (`SUIPAY_GATEWAY_URL`) and the same network
30
+ the grant was created on.
31
+ 4. Call `pay`. The tool prepares unsigned `spend_account` settlement
32
+ bytes. If this process holds the grant's key it verifies and settles;
33
+ otherwise it returns the signing request.
34
+
35
+ The agent generates or is handed the delegate key. The key never enters
36
+ a browser URL, a SuiPay backend, logs, or a callback payload. Only the
37
+ public address leaves the local process. `suipay_login` mints or reuses
38
+ the local version-2 key at `~/.suipay/credentials.json` (mode `0600`) and
39
+ opens `{SUIPAY_CONSOLE_URL}/connect/agent?delegateAddress=0x…`.
40
+
41
+ ## Tools
42
+
43
+ | Tool | Purpose |
44
+ | --- | --- |
45
+ | `discover` | Search live managed-gateway resources |
46
+ | `pay` | Fetch resource, prepare `spend_account` settlement, settle when this process holds the grant key |
47
+ | `receipts` | List settlement receipts, optionally by challenge ID |
48
+ | `suipay_login` | Mint or reuse the local delegate key and open the console with the public address only |
49
+ | `suipay_logout` | Remove local credentials |
50
+
51
+ All tools remain visible before a grant is bound. `pay` and `receipts`
52
+ return instruction to run `suipay_login` and bind a spend-account grant
53
+ when no local credentials exist. Hosted `/mcp` never holds a spendable
54
+ key.
55
+
56
+ ## Client environment
57
+
58
+ ```bash
59
+ SUIPAY_GATEWAY_URL=https://gateway.example
60
+ SUIPAY_CONSOLE_URL=https://console.example
61
+ SUIPAY_RPC_URL=https://your-testnet-rpc.example
62
+ SUIPAY_NETWORK=testnet
63
+ ```
64
+
65
+ Optional:
66
+
67
+ ```bash
68
+ # Defaults to $SUIPAY_GATEWAY_URL/api/sponsor
69
+ SUIPAY_SPONSOR_URL=https://gateway.example/api/sponsor
70
+
71
+ SUIPAY_PACKAGE_ID=0x...
72
+ SUIPAY_RECIPIENT=0x...
73
+ SUIPAY_COIN_TYPE=0x...::usdc::USDC
74
+
75
+ SUIPAY_MIN_FUNDED=5000000
76
+ SUIPAY_MIN_MAX_PER_PAYMENT=5000000
77
+ SUIPAY_LABEL="My agent"
78
+ ```
79
+
80
+ `ENOKI_PRIVATE_API_KEY` is backend-only. Never set it in MCP config or
81
+ local SuiPay credentials.
82
+
83
+ If `SUI_SECRET_KEY` is present, it must derive to the delegate address
84
+ stored in credentials. Mismatch or malformed credentials stop signing.
85
+ No mnemonic or ephemeral payment fallback exists.
86
+
87
+ ## Logout safety
88
+
89
+ `suipay_logout` removes the local credentials file. Revoke the grant in
90
+ the console if the spend account should stop honouring this key.
91
+ `force=true` removes local credentials even when the file is unreadable.
92
+
93
+ ## Build and run locally
94
+
95
+ ```bash
96
+ pnpm --filter @cmdoss/suipay-mcp build
97
+ node packages/mcp/dist/bin/suipay.js
98
+ ```
99
+
100
+ Binary writes status banner to stderr only. Stdout stays reserved for
101
+ MCP protocol.
102
+
103
+ Do not publish `@cmdoss/suipay-mcp` from this change.
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ main
4
+ } from "../chunk-TPW6S2K4.js";
5
+ import "../chunk-XZQOOY66.js";
6
+ import "../chunk-A3JIIDYT.js";
7
+
8
+ // src/bin/suipay.ts
9
+ main().catch((err) => {
10
+ console.error(err instanceof Error ? err.message : String(err));
11
+ process.exit(1);
12
+ });
@@ -0,0 +1,161 @@
1
+ // ../../src/protocol/rail.ts
2
+ var SETTLEMENT_RAILS = [
3
+ "direct",
4
+ "allowance",
5
+ "shared_pool",
6
+ "spend_account",
7
+ "auth_capture",
8
+ "subscription"
9
+ ];
10
+ function isSpendAccountRail(rail) {
11
+ return rail === "shared_pool" || rail === "spend_account";
12
+ }
13
+ var DIALECT_ORDER = ["mpp", "x402"];
14
+ function isSettlementRail(value) {
15
+ return typeof value === "string" && SETTLEMENT_RAILS.includes(value);
16
+ }
17
+ function assertSettlementRail(value) {
18
+ if (!isSettlementRail(value)) {
19
+ throw new Error(
20
+ `invalid settlement rail: ${String(value)} (expected ${SETTLEMENT_RAILS.join(" | ")})`
21
+ );
22
+ }
23
+ return value;
24
+ }
25
+ function isDialect(value) {
26
+ return value === "mpp" || value === "x402";
27
+ }
28
+ function isDialectMode(value) {
29
+ return value === "transaction" || value === "finalized-proof";
30
+ }
31
+ function dialectsFromModes(modes) {
32
+ return DIALECT_ORDER.filter((d) => modes[d] != null);
33
+ }
34
+ function dialectMode(authority, dialect) {
35
+ const mode = authority.dialectModes[dialect];
36
+ if (!mode) {
37
+ throw new Error(
38
+ `${authority.rail} has no dialectMode for ${dialect} (ceiling: ${authority.supportedDialects.join(",")})`
39
+ );
40
+ }
41
+ return mode;
42
+ }
43
+ function createSettlementRailAuthority(input) {
44
+ assertSettlementRail(input.rail);
45
+ if (typeof input.packageId !== "string" || !input.packageId.trim()) {
46
+ throw new Error("packageId is required");
47
+ }
48
+ if (typeof input.module !== "string" || !input.module.trim()) {
49
+ throw new Error("module is required");
50
+ }
51
+ if (typeof input.payFunction !== "string" || !input.payFunction.trim()) {
52
+ throw new Error("payFunction is required");
53
+ }
54
+ const keys = Object.keys(input.dialectModes ?? {});
55
+ if (!keys.length) {
56
+ throw new Error("dialectModes must be non-empty");
57
+ }
58
+ for (const d of keys) {
59
+ if (!isDialect(d)) {
60
+ throw new Error(`unknown dialect in dialectModes: ${String(d)}`);
61
+ }
62
+ const mode = input.dialectModes[d];
63
+ if (!isDialectMode(mode)) {
64
+ throw new Error(
65
+ `invalid dialectMode for ${d}: ${String(mode)}`
66
+ );
67
+ }
68
+ }
69
+ const dialectModes = Object.freeze({ ...input.dialectModes });
70
+ const supportedDialects = Object.freeze(
71
+ dialectsFromModes(dialectModes)
72
+ );
73
+ const proofMode = dialectModes.mpp ?? dialectModes.x402 ?? "transaction";
74
+ return Object.freeze({
75
+ rail: input.rail,
76
+ packageId: input.packageId.trim(),
77
+ module: input.module.trim(),
78
+ payFunction: input.payFunction.trim(),
79
+ dialectModes,
80
+ supportedDialects,
81
+ proofMode
82
+ });
83
+ }
84
+ var SPEND_ACCOUNT_DEBIT_FUNCTIONS = [
85
+ "settle_policy_payment",
86
+ "settle_policy_payment_to_refund_vault",
87
+ "settle_open_recipient_payment",
88
+ "settle_open_recipient_payment_to_refund_vault"
89
+ ];
90
+ function spendAccountDebitFunction(input) {
91
+ const open = input?.accessMode === "open";
92
+ const refundable = input?.refundable === true;
93
+ if (open && refundable) return "settle_open_recipient_payment_to_refund_vault";
94
+ if (open) return "settle_open_recipient_payment";
95
+ if (refundable) return "settle_policy_payment_to_refund_vault";
96
+ return "settle_policy_payment";
97
+ }
98
+ function defaultAuthorityForRail(rail, packageId, opts) {
99
+ switch (rail) {
100
+ case "spend_account":
101
+ case "shared_pool": {
102
+ const dual = opts?.x402DialectEnabled === true;
103
+ return createSettlementRailAuthority({
104
+ rail,
105
+ packageId,
106
+ module: "spend_account",
107
+ payFunction: spendAccountDebitFunction(),
108
+ dialectModes: dual ? { mpp: "finalized-proof", x402: "transaction" } : { mpp: "finalized-proof" }
109
+ });
110
+ }
111
+ case "allowance":
112
+ throw new Error(
113
+ "settlement rail 'allowance' is retired (ADR-0010); it is decodable but has no settlement authority"
114
+ );
115
+ case "subscription":
116
+ return createSettlementRailAuthority({
117
+ rail: "subscription",
118
+ packageId,
119
+ module: "subscription",
120
+ payFunction: "charge",
121
+ dialectModes: {
122
+ mpp: "transaction",
123
+ x402: "transaction"
124
+ }
125
+ });
126
+ case "direct":
127
+ return createSettlementRailAuthority({
128
+ rail: "direct",
129
+ packageId,
130
+ module: "payment",
131
+ payFunction: "pay",
132
+ dialectModes: {
133
+ mpp: "transaction",
134
+ x402: "transaction"
135
+ }
136
+ });
137
+ case "auth_capture":
138
+ return createSettlementRailAuthority({
139
+ rail: "auth_capture",
140
+ packageId,
141
+ module: "auth_capture",
142
+ payFunction: "authorize",
143
+ dialectModes: {
144
+ x402: "transaction"
145
+ }
146
+ });
147
+ default: {
148
+ const _exhaustive = rail;
149
+ throw new Error(`unhandled rail: ${String(_exhaustive)}`);
150
+ }
151
+ }
152
+ }
153
+
154
+ export {
155
+ isSpendAccountRail,
156
+ isSettlementRail,
157
+ dialectMode,
158
+ SPEND_ACCOUNT_DEBIT_FUNCTIONS,
159
+ spendAccountDebitFunction,
160
+ defaultAuthorityForRail
161
+ };
@@ -0,0 +1,39 @@
1
+ import {
2
+ createServer,
3
+ loadMcpConfig,
4
+ loadPaymentProfile
5
+ } from "./chunk-XZQOOY66.js";
6
+
7
+ // src/index.ts
8
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
9
+ function loadBootProfile(load = loadPaymentProfile) {
10
+ try {
11
+ return { profile: load(), unreadable: false };
12
+ } catch {
13
+ return { profile: null, unreadable: true };
14
+ }
15
+ }
16
+ async function main() {
17
+ const cfg = loadMcpConfig();
18
+ const { profile: bootProfile, unreadable: bootProfileUnreadable } = loadBootProfile();
19
+ const server = createServer(cfg);
20
+ await server.connect(new StdioServerTransport());
21
+ if (bootProfileUnreadable) {
22
+ console.error(
23
+ "[suipay-mcp] credentials unreadable: run suipay_login or suipay_logout force=true \xB7 stdio ready"
24
+ );
25
+ } else if (bootProfile) {
26
+ console.error(
27
+ `[suipay-mcp] delegate ${bootProfile.creds.delegateAddress} (${bootProfile.source}) \xB7 stdio ready`
28
+ );
29
+ } else {
30
+ console.error(
31
+ "[suipay-mcp] not connected \xB7 run suipay_login \xB7 stdio ready"
32
+ );
33
+ }
34
+ }
35
+
36
+ export {
37
+ loadBootProfile,
38
+ main
39
+ };