@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 +103 -0
- package/dist/bin/suipay.d.ts +1 -0
- package/dist/bin/suipay.js +12 -0
- package/dist/chunk-A3JIIDYT.js +161 -0
- package/dist/chunk-TPW6S2K4.js +39 -0
- package/dist/chunk-XZQOOY66.js +6129 -0
- package/dist/http-BnIjiwcR.d.ts +1149 -0
- package/dist/http.d.ts +3 -0
- package/dist/http.js +7 -0
- package/dist/index.d.ts +324 -0
- package/dist/index.js +45 -0
- package/dist/personal-message-5PF4KVLF.js +284 -0
- package/package.json +36 -0
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
|
+
};
|