@mysten-incubation/suipay-mcp 0.0.0-stage → 0.3.0-dev.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 CHANGED
@@ -1,3 +1,225 @@
1
- # Temporary Holding Version
1
+ # @mysten-incubation/suipay-mcp
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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 `@mysten-incubation/suipay-mcp@0.3.0-dev.0` in MCP config for the `latest` dist-tag.
9
+ `npx -y @mysten-incubation/suipay-mcp@dev` tracks the `dev` prerelease line.
10
+ `1.0.1` remains on the registry as a historical release; do not install it
11
+ for new agents.
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ claude mcp add --scope user suipay -- npx -y @mysten-incubation/suipay-mcp@0.3.0-dev.0
17
+ ```
18
+
19
+ `@dev` installs the Railway-dev gateway/console defaults. A workspace
20
+ checkout still defaults to localhost. `SUIPAY_GATEWAY_URL` and
21
+ `SUIPAY_CONSOLE_URL` always override.
22
+
23
+ No wallet secret belongs in this command or MCP config. There is no
24
+ Sign-In With Sui (SIWS) step. To reuse an existing delegate seed locally,
25
+ set `SUIPAY_MCP_KEY` (64-hex or `suiprivkey1…`) in the MCP env — never in
26
+ the browser.
27
+
28
+ ## First connection
29
+
30
+ First connection is a spend-account grant plus an agent-held delegate key.
31
+
32
+ 1. In the SuiPay console, fund a spend account and create a grant that
33
+ names the agent's delegate address.
34
+ 2. Persist that delegate key locally at `~/.suipay/credentials.json`
35
+ (mode `0600`). The process that holds this key is the only process
36
+ that can settle.
37
+ 3. Point MCP at a gateway. Default is Railway **suipay-dev**. Pass
38
+ `suipay_login({ target: "local" })` or start the binary with `--local`
39
+ for the laptop stack. Env/CLI fill the process default until a
40
+ credential is bound; after login, stored `gatewayUrl` wins. Rebind
41
+ with `suipay_login({ target })`.
42
+ 4. Call `pay` with `maxAmount`. On stdio the grant cap is not a default.
43
+ An open grant returns the payee and does not sign until a later call
44
+ names that recipient. When this process holds the grant's key and the
45
+ payee is already declared, it verifies and settles. Hosted `/mcp`
46
+ returns the unsigned signing request.
47
+
48
+ The agent generates or is handed the delegate key. The key never enters
49
+ a browser URL, a SuiPay backend, logs, or a callback payload. Only the
50
+ public address leaves the local process. `suipay_login` mints or reuses
51
+ the local version-2 key at `~/.suipay/credentials.json` (mode `0600`) and
52
+ returns `{SUIPAY_CONSOLE_URL}/connect/agent?port=&delegateAddress=&connectState=&gateway=`
53
+ immediately. The owner signs in the browser; the loopback callback persists
54
+ key + gateway URL. Subsequent sessions reuse the file unless the grant is
55
+ missing, expired, or revoked.
56
+
57
+ ## Tools
58
+
59
+ | Tool | Purpose |
60
+ | --- | --- |
61
+ | `discover` | Search live gateway resources. After login: `name`, `path`, `url`, `targetHash`, catalog price, and `affordable` (can this session cover one call — not authorization). Stdio `pay` still requires `maxAmount` |
62
+ | `pay` | On stdio, `maxAmount` is required. An open grant returns the gateway payee and does not sign until `recipient` names it. Otherwise prepare `spend_account` settlement and settle when this process holds the grant key. POST `json` is the paid body. Optional stdio `file` + `fileField` fill one JSON key from a local file as canonical base64 (Walrus `contentBase64`); requires `SUIPAY_MCP_FILE_ROOT`; hosted `/mcp` refuses `file` |
63
+ | `receipts` | List settlement receipts, optionally by challenge ID |
64
+ | `access_context` | Report current session / delegate / policy state (no secret material) |
65
+ | `suipay_login` | Mint or reuse the local delegate key and return a clickable console URL (does not wait for the wallet). Default target is Railway suipay-dev; pass `target: "local"` for the laptop stack |
66
+ | `suipay_logout` | Remove local credentials |
67
+
68
+ All tools remain visible before a grant is bound. `pay` and `receipts`
69
+ return instruction to run `suipay_login` and bind a spend-account grant
70
+ when no local credentials exist. Hosted `/mcp` never holds a spendable
71
+ key.
72
+
73
+ ## Use from an agent
74
+
75
+ Install once (above). After that, paying is a prompt — paste it into Claude
76
+ (or any MCP host that has `suipay`). Do not write SDK code.
77
+
78
+ ```
79
+ Pay this SuiPay resource with the suipay MCP `pay` tool. Do not write SDK code.
80
+
81
+ url: https://gateway.example/v1/image/generate
82
+ method: POST
83
+ json: {"prompt":"a cat"}
84
+ maxAmount: 50000
85
+ ```
86
+
87
+ To store local bytes (Walrus), do not paste base64. Name the file and field.
88
+ Set `SUIPAY_MCP_FILE_ROOT` to an absolute directory; `file` must resolve inside
89
+ it (unset disables attach). Paths under `~/.suipay` are refused even if that
90
+ directory falls inside the root.
91
+
92
+ ```
93
+ Pay this SuiPay resource with the suipay MCP `pay` tool. Do not write SDK code.
94
+
95
+ url: https://gateway.example/v1/walrus/store
96
+ method: POST
97
+ json: {"contentType":"text/plain"}
98
+ file: /absolute/path/to/note.txt
99
+ fileField: contentBase64
100
+ maxAmount: 50000
101
+ ```
102
+
103
+ `pay` uses SDK `createPayer` internally. When this process settles, a binary
104
+ body is written once under `~/.suipay/deliveries/` and named first in the
105
+ JSON as `savedTo` (absolute path) and `savedToUrl` (`file://`). Hosted `/mcp`
106
+ is prepare-only and does not write a buyer file. Any MIME — image, audio,
107
+ pdf, octet-stream. JSON stays compact (no base64 wall). An MCP `image` block
108
+ is an extra display hint when the type is `image/*`; hosts such as Claude
109
+ Code's TUI may not render it. Always quote `savedTo` when present. If
110
+ `savedTo` is null, quote `saveError` and do not call `pay` again.
111
+
112
+ ## Tool errors — branch on `code`
113
+
114
+ A tool result whose `status` is `failed` or `ambiguous` sets `isError: true`
115
+ and puts the error model in `structuredContent`:
116
+
117
+ | Field | Meaning |
118
+ | --- | --- |
119
+ | `status`, `paid` | As before. `paid` is `true`, `false` or `'unknown'`. |
120
+ | `code` | Registered error code, e.g. `request.file_outside_root`. **Branch on this**, never on the text. |
121
+ | `paymentState` | `charged`, `unknown` or `not_charged`; always agrees with `paid`. |
122
+ | `retryable` | `true` only when a retry cannot charge twice. |
123
+ | `nextAction` | `retry`, `retry_later`, `fix_request`, `sign_in`, `approve_access`, `add_funds`, `reconcile`, `contact_support` or `none`. |
124
+ | `detail` | Developer text that states the fix. |
125
+ | `txDigest`, `paymentId` | When known. |
126
+
127
+ The text block holds the same object as JSON. When money moved or may have
128
+ moved it **starts** with a line for the model:
129
+
130
+ ```
131
+ You were charged. Do not pay again for this request.
132
+ { "status": "ambiguous", "paid": true, "code": "delivery.seller_unreachable", "paymentState": "charged", ... }
133
+ ```
134
+
135
+ (`This payment may have gone through. Do not pay again until it is resolved.`
136
+ when `paymentState` is `unknown`.) A result whose `status` is `ok` or
137
+ `prepared` is never an error, including a free response that needed no
138
+ payment.
139
+
140
+ Common local codes: `auth.login_required` (run `suipay_login`),
141
+ `request.file_outside_root` / `request.file_root_unset` (set
142
+ `SUIPAY_MCP_FILE_ROOT`), `client.local_credentials_invalid` (log in again),
143
+ `client.gateway_unreachable` (retry later). An unknown tool name is a JSON-RPC
144
+ error, not a tool result; an invalid argument such as a relative `url` is a
145
+ tool error with a `request.*` code and `nextAction: "fix_request"`.
146
+
147
+ ## Targets
148
+
149
+ Default is Railway **suipay-dev**. `--local` / `target: "local"` is laptop-only.
150
+
151
+ | Target | Console | Gateway | How |
152
+ | --- | --- | --- | --- |
153
+ | `dev` (default) | `https://suipay-dev.up.railway.app` | `https://gateway-dev-ba84.up.railway.app` | no flag, or `--dev` |
154
+ | `local` | `http://localhost:3000` | `http://127.0.0.1:4340` | `--local` / `-local`, or `suipay_login({ target: "local" })` |
155
+
156
+ ```bash
157
+ # Railway suipay-dev (default)
158
+ npx @mysten-incubation/suipay-mcp@0.3.0-dev.0
159
+
160
+ # Laptop stack
161
+ npx @mysten-incubation/suipay-mcp@0.3.0-dev.0 --local
162
+ ```
163
+
164
+ `SUIPAY_MCP_TARGET=local|dev` selects the same preset. Explicit
165
+ `SUIPAY_GATEWAY_URL` / `SUIPAY_CONSOLE_URL` fill the process default when no
166
+ credential is bound yet. After a successful login, `credentials.json`
167
+ `gatewayUrl` is the bound gateway: `pay` / `discover` / `suipay_login` without
168
+ `target` stay on that binding so a leftover env cannot silently retarget spend.
169
+ Rebind with `suipay_login({ target: "dev" | "local" })` (same key, new consent
170
+ if that stack has no live grant).
171
+
172
+ ## Client environment
173
+
174
+ ```bash
175
+ # Optional. Unset = Railway suipay-dev. --local uses localhost:3000 / 127.0.0.1:4340.
176
+ SUIPAY_GATEWAY_URL=https://gateway.example
177
+ SUIPAY_CONSOLE_URL=https://console.example
178
+ SUIPAY_RPC_URL=https://your-testnet-rpc.example
179
+ SUIPAY_NETWORK=testnet
180
+ SUIPAY_MCP_TARGET=dev
181
+ ```
182
+
183
+ Optional:
184
+
185
+ ```bash
186
+ # Defaults to $SUIPAY_GATEWAY_URL/api/sponsor
187
+ SUIPAY_SPONSOR_URL=https://gateway.example/api/sponsor
188
+
189
+ SUIPAY_PACKAGE_ID=0x...
190
+ SUIPAY_RECIPIENT=0x...
191
+ SUIPAY_COIN_TYPE=0x...::usdc::USDC
192
+
193
+ SUIPAY_MIN_FUNDED=5000000
194
+ SUIPAY_MIN_MAX_PER_PAYMENT=5000000
195
+ SUIPAY_LABEL="My agent"
196
+ ```
197
+
198
+ `ENOKI_PRIVATE_API_KEY` is backend-only. Never set it in MCP config or
199
+ local SuiPay credentials.
200
+
201
+ If `SUI_SECRET_KEY` is present, it must derive to the delegate address
202
+ stored in credentials. Mismatch or malformed credentials stop signing.
203
+ No mnemonic or ephemeral payment fallback exists.
204
+
205
+ ## Logout safety
206
+
207
+ `suipay_logout` removes the local credentials file. Revoke the grant in
208
+ the console if the spend account should stop honouring this key.
209
+ `force=true` removes local credentials even when the file is unreadable.
210
+
211
+ ## Build and run locally
212
+
213
+ ```bash
214
+ pnpm --filter @mysten-incubation/suipay-mcp build
215
+ node packages/mcp/dist/bin/suipay.js
216
+ ```
217
+
218
+ Binary writes status banner to stderr only. Stdout stays reserved for
219
+ MCP protocol.
220
+
221
+ Publishing is the `release-npm` workflow: merge to `dev` publishes
222
+ `@mysten-incubation/suipay-mcp@<version>-dev.N` with dist-tag `dev` (OIDC, pack gate,
223
+ provenance). `latest` is a gated dispatch from `main` (`dry_run` default
224
+ true). Do not `npm publish` from a laptop. See
225
+ [`docs/guide/npm-release.md`](../../docs/guide/npm-release.md).
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ main
4
+ } from "../chunk-47YNZDCC.js";
5
+ import {
6
+ logError
7
+ } from "../chunk-CXM7PELK.js";
8
+
9
+ // src/bin/suipay.ts
10
+ main().catch((err) => {
11
+ logError({ scope: "suipay-mcp", code: "client.unexpected", withStack: true }, err);
12
+ process.exit(1);
13
+ });
@@ -0,0 +1,53 @@
1
+ import {
2
+ createServer,
3
+ loadLocalSigner,
4
+ loadLocalSigningKeyRecord,
5
+ loadMcpConfig,
6
+ loadPaymentProfile,
7
+ logError
8
+ } from "./chunk-CXM7PELK.js";
9
+
10
+ // src/index.ts
11
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
12
+ function loadBootProfile(load = loadPaymentProfile) {
13
+ try {
14
+ return { profile: load(), unreadable: false };
15
+ } catch {
16
+ return { profile: null, unreadable: true };
17
+ }
18
+ }
19
+ async function main() {
20
+ const argv = process.argv.slice(2);
21
+ const cfg = loadMcpConfig(process.env, argv);
22
+ const target = cfg.target ?? "dev";
23
+ const server = createServer(cfg, void 0, void 0, { transport: "stdio" });
24
+ await server.connect(new StdioServerTransport());
25
+ try {
26
+ const boundGateway = loadLocalSigningKeyRecord()?.gatewayUrl ?? cfg.gatewayUrl;
27
+ const local = loadLocalSigner();
28
+ if (local) {
29
+ process.stderr.write(`[suipay-mcp] ${target} ${boundGateway} \xB7 delegate ${local.address} \xB7 stdio ready
30
+ `);
31
+ } else {
32
+ process.stderr.write(`[suipay-mcp] ${target} ${boundGateway} \xB7 not connected \xB7 run suipay_login \xB7 stdio ready
33
+ `);
34
+ }
35
+ } catch (error) {
36
+ logError(
37
+ {
38
+ scope: "suipay-mcp",
39
+ code: "client.local_credentials_invalid",
40
+ level: "warn",
41
+ msg: "local credentials are unreadable; run suipay_login or suipay_logout force=true"
42
+ },
43
+ error
44
+ );
45
+ process.stderr.write(`[suipay-mcp] ${target} ${cfg.gatewayUrl} \xB7 credentials unreadable \xB7 stdio ready
46
+ `);
47
+ }
48
+ }
49
+
50
+ export {
51
+ loadBootProfile,
52
+ main
53
+ };