@mysten-incubation/suipay-mcp 0.3.0-dev.1 → 0.3.0-dev.10

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
@@ -5,7 +5,7 @@ buyer-funded spend account. The buyer owns the account. The agent holds the
5
5
  delegate key and can spend only under the on-chain grant (recipient bind,
6
6
  per-payment cap, budget).
7
7
 
8
- Pin `@mysten-incubation/suipay-mcp@0.3.0-dev.1` in MCP config for the `latest` dist-tag.
8
+ Pin `@mysten-incubation/suipay-mcp@0.3.0-dev.10` in MCP config for the `latest` dist-tag.
9
9
  `npx -y @mysten-incubation/suipay-mcp@dev` tracks the `dev` prerelease line.
10
10
  `1.0.1` remains on the registry as a historical release; do not install it
11
11
  for new agents.
@@ -13,7 +13,7 @@ for new agents.
13
13
  ## Install
14
14
 
15
15
  ```bash
16
- claude mcp add --scope user suipay -- npx -y @mysten-incubation/suipay-mcp@0.3.0-dev.1
16
+ claude mcp add --scope user suipay -- npx -y @mysten-incubation/suipay-mcp@0.3.0-dev.10
17
17
  ```
18
18
 
19
19
  `@dev` installs the Railway-dev gateway/console defaults. A workspace
@@ -155,10 +155,10 @@ Default is Railway **suipay-dev**. `--local` / `target: "local"` is laptop-only.
155
155
 
156
156
  ```bash
157
157
  # Railway suipay-dev (default)
158
- npx @mysten-incubation/suipay-mcp@0.3.0-dev.1
158
+ npx @mysten-incubation/suipay-mcp@0.3.0-dev.10
159
159
 
160
160
  # Laptop stack
161
- npx @mysten-incubation/suipay-mcp@0.3.0-dev.1 --local
161
+ npx @mysten-incubation/suipay-mcp@0.3.0-dev.10 --local
162
162
  ```
163
163
 
164
164
  `SUIPAY_MCP_TARGET=local|dev` selects the same preset. Explicit
@@ -169,12 +169,32 @@ credential is bound yet. After a successful login, `credentials.json`
169
169
  Rebind with `suipay_login({ target: "dev" | "local" })` (same key, new consent
170
170
  if that stack has no live grant).
171
171
 
172
+ ### Isolate connection environments
173
+
174
+ `SUIPAY_MCP_PROFILE` selects a separate agent-held key file at
175
+ `~/.suipay/profiles/<sha256-of-trimmed-profile>/credentials.json`. Unset or
176
+ blank keeps the existing `~/.suipay/credentials.json`. The profile is a local
177
+ storage selector, not an authorization grant or gateway override: set
178
+ `SUIPAY_GATEWAY_URL` and `SUIPAY_CONSOLE_URL` as well. Use the same stable profile
179
+ for the same environment. Login, status, discovery, pay, and logout all use
180
+ that profile; logout removes only its file. Files stay mode `0600` inside a
181
+ `0700` profile directory. Existing files are never copied, rebound, or deleted
182
+ when selecting a profile. A new profile requires fresh owner authorization.
183
+
184
+ The console's Connect Agent prompt uses its public gateway URL as the profile.
185
+ Install an MCP package that supports profiles before using this prompt, and
186
+ verify the returned login gateway matches the configured environment before
187
+ continuing. An older package may ignore the variable and reuse legacy
188
+ credentials. Stop on a mismatch; do not pay or delete the existing key.
189
+
172
190
  ## Client environment
173
191
 
174
192
  ```bash
175
193
  # Optional. Unset = Railway suipay-dev. --local uses localhost:3000 / 127.0.0.1:4340.
176
194
  SUIPAY_GATEWAY_URL=https://gateway.example
177
195
  SUIPAY_CONSOLE_URL=https://console.example
196
+ # Optional. Unset/blank retains the legacy credentials file.
197
+ SUIPAY_MCP_PROFILE=https://gateway.example
178
198
  SUIPAY_RPC_URL=https://your-testnet-rpc.example
179
199
  SUIPAY_NETWORK=testnet
180
200
  SUIPAY_MCP_TARGET=dev
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  main
4
- } from "../chunk-L7OTT6U5.js";
4
+ } from "../chunk-D6LFU4VE.js";
5
5
  import {
6
6
  logError
7
- } from "../chunk-WPRFEMBU.js";
7
+ } from "../chunk-RD34GGSA.js";
8
8
 
9
9
  // src/bin/suipay.ts
10
10
  main().catch((err) => {
@@ -5,7 +5,7 @@ import {
5
5
  loadMcpConfig,
6
6
  loadPaymentProfile,
7
7
  logError
8
- } from "./chunk-WPRFEMBU.js";
8
+ } from "./chunk-RD34GGSA.js";
9
9
 
10
10
  // src/index.ts
11
11
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -448,6 +448,14 @@ var SERVICE_STATE_TRANSITIONS = Object.freeze([
448
448
  to: "payout_change",
449
449
  actors: ["system"],
450
450
  requires: ["reason_code"]
451
+ },
452
+ {
453
+ id: "T25",
454
+ dimension: "suspension",
455
+ from: ["binding_changed"],
456
+ to: "none",
457
+ actors: ["system"],
458
+ requires: ["reason_code"]
451
459
  }
452
460
  ]);
453
461
 
@@ -949,6 +957,70 @@ function createSpendAccountChain(options) {
949
957
  });
950
958
  return kindBytes(tx);
951
959
  },
960
+ async buildCreatePolicyAndGrantKind(input) {
961
+ if (!input.policy.targets?.length) {
962
+ throw new Error("create_spend_policy requires at least one target");
963
+ }
964
+ if (!input.policy.totalCap || BigInt(input.policy.totalCap) <= 0n) {
965
+ throw new Error("create_spend_policy requires a non-zero total_cap");
966
+ }
967
+ if (!input.policy.maxPerPayment || BigInt(input.policy.maxPerPayment) <= 0n) {
968
+ throw new Error("create_spend_policy requires a non-zero max_per_payment");
969
+ }
970
+ if (!input.grant.sessionCap || BigInt(input.grant.sessionCap) <= 0n) {
971
+ throw new Error("create_grant requires a non-zero session_cap");
972
+ }
973
+ if (!input.grant.maxPerPayment || BigInt(input.grant.maxPerPayment) <= 0n) {
974
+ throw new Error("create_grant requires a non-zero max_per_payment");
975
+ }
976
+ const tx = new Transaction2();
977
+ tx.setSender(input.owner);
978
+ const targetResults = input.policy.targets.map((t) => {
979
+ const hash = targetHashBytes(t.targetHash);
980
+ return tx.moveCall({
981
+ target: target("new_policy_target"),
982
+ arguments: [
983
+ tx.pure.vector("u8", Array.from(hash)),
984
+ tx.pure.address(t.recipient),
985
+ tx.pure.u64(BigInt(t.maxPerPayment))
986
+ ]
987
+ });
988
+ });
989
+ const targetsVec = tx.makeMoveVec({
990
+ type: `${packageId}::${MODULE}::PolicyTarget`,
991
+ elements: targetResults
992
+ });
993
+ const policyExpires = input.policy.expiresAtMs === void 0 || input.policy.expiresAtMs === null ? 0n : BigInt(input.policy.expiresAtMs);
994
+ const policyIdResult = tx.moveCall({
995
+ target: target("create_spend_policy"),
996
+ typeArguments: [input.coinType],
997
+ arguments: [
998
+ tx.object(input.poolObjectId),
999
+ targetsVec,
1000
+ tx.pure.u64(BigInt(input.policy.totalCap)),
1001
+ tx.pure.u64(BigInt(input.policy.maxPerPayment)),
1002
+ tx.pure.u64(policyExpires)
1003
+ ]
1004
+ });
1005
+ const policyIdsVec = tx.makeMoveVec({
1006
+ type: "u64",
1007
+ elements: [policyIdResult]
1008
+ });
1009
+ const grantExpires = input.grant.expiresAtMs === void 0 || input.grant.expiresAtMs === null ? 0n : BigInt(input.grant.expiresAtMs);
1010
+ tx.moveCall({
1011
+ target: target("create_policy_grant"),
1012
+ typeArguments: [input.coinType],
1013
+ arguments: [
1014
+ tx.object(input.poolObjectId),
1015
+ tx.pure.address(input.delegate),
1016
+ tx.pure.option("u64", BigInt(input.grant.sessionCap)),
1017
+ tx.pure.option("u64", BigInt(input.grant.maxPerPayment)),
1018
+ tx.pure.u64(grantExpires),
1019
+ policyIdsVec
1020
+ ]
1021
+ });
1022
+ return kindBytes(tx);
1023
+ },
952
1024
  async buildPausePolicyKind(input) {
953
1025
  const tx = new Transaction2();
954
1026
  tx.setSender(input.owner);
@@ -3846,6 +3918,17 @@ var INDEXER_CODES = {
3846
3918
  user: null,
3847
3919
  logOnly: true
3848
3920
  },
3921
+ "indexer.native_settlement_deferred": {
3922
+ surfaces: ["indexer"],
3923
+ status: null,
3924
+ title: "Native settlement is deferred; holding the checkpoint",
3925
+ paymentState: "not_charged",
3926
+ retry: "after_delay",
3927
+ retryAfterSeconds: 5,
3928
+ nextAction: "retry_later",
3929
+ user: null,
3930
+ logOnly: true
3931
+ },
3849
3932
  "indexer.config_invalid": {
3850
3933
  surfaces: ["indexer"],
3851
3934
  status: null,
@@ -4460,6 +4543,55 @@ var REQUEST_CODES = {
4460
4543
  nextAction: "fix_request",
4461
4544
  user: { title: "Address not found", message: "That web address doesn't exist. Check the spelling and try again." }
4462
4545
  },
4546
+ "request.recipient_changed": {
4547
+ surfaces: ["gateway"],
4548
+ status: 409,
4549
+ title: "Pay-to address changed while its registry was being checked",
4550
+ paymentState: "not_charged",
4551
+ retry: "never",
4552
+ nextAction: "fix_request",
4553
+ user: {
4554
+ title: "Retry this publish",
4555
+ message: "The payout address changed while its payment registry was being checked. Retry so the current address is checked."
4556
+ }
4557
+ },
4558
+ "request.recipient_registry_invalid": {
4559
+ surfaces: ["gateway"],
4560
+ status: 409,
4561
+ title: "Payment registry does not verify for this deployment",
4562
+ paymentState: "not_charged",
4563
+ retry: "never",
4564
+ nextAction: "fix_request",
4565
+ user: {
4566
+ title: "Registry does not match",
4567
+ message: "The payout address's payment registry does not match this deployment. Fix it, then publish again."
4568
+ }
4569
+ },
4570
+ "request.recipient_registry_missing": {
4571
+ surfaces: ["gateway"],
4572
+ status: 409,
4573
+ title: "Pay-to address has no payment registry",
4574
+ paymentState: "not_charged",
4575
+ retry: "never",
4576
+ nextAction: "fix_request",
4577
+ user: {
4578
+ title: "Create a payment registry",
4579
+ message: "This payout address has no payment registry on this network yet. Create it in the seller console, then publish again."
4580
+ }
4581
+ },
4582
+ "request.recipient_registry_unreadable": {
4583
+ surfaces: ["gateway"],
4584
+ status: 503,
4585
+ title: "Payment registry could not be read",
4586
+ paymentState: "not_charged",
4587
+ retry: "after_delay",
4588
+ retryAfterSeconds: 5,
4589
+ nextAction: "retry_later",
4590
+ user: {
4591
+ title: "Registry unread",
4592
+ message: "The payment registry could not be read just now. Wait a moment and try again."
4593
+ }
4594
+ },
4463
4595
  "request.redirect_refused": {
4464
4596
  surfaces: ["console_server", "sdk"],
4465
4597
  status: 422,
@@ -4924,6 +5056,19 @@ var SERVICE_CODES = {
4924
5056
  user: null,
4925
5057
  logOnly: true
4926
5058
  },
5059
+ // A readiness gauge moved while the process is still serving. Not a degraded
5060
+ // boot: `service.config_degraded` is reserved for that, and for the sample
5061
+ // that actually fails closed.
5062
+ "service.readiness": {
5063
+ surfaces: ["gateway"],
5064
+ status: null,
5065
+ title: "Readiness gauge updated",
5066
+ paymentState: "not_charged",
5067
+ retry: "never",
5068
+ nextAction: "none",
5069
+ user: null,
5070
+ logOnly: true
5071
+ },
4927
5072
  // A best-effort write (a trace event, a receipt, a delivery record) failed.
4928
5073
  // The request it belongs to keeps its answer.
4929
5074
  "service.bookkeeping_failed": {
@@ -5751,6 +5896,11 @@ var CONTEXT_ALLOWLIST = /* @__PURE__ */ new Set([
5751
5896
  "rails",
5752
5897
  "shard",
5753
5898
  "metric",
5899
+ // Gateway readiness gauge. Fixed marks only: 0 or 1, and up, down, skipped, unknown.
5900
+ "ready",
5901
+ "grpc",
5902
+ "enoki",
5903
+ "postgres",
5754
5904
  "alarm",
5755
5905
  "alert",
5756
5906
  "from",
@@ -9849,6 +9999,7 @@ import { bytesToHex as bytesToHex4 } from "@noble/hashes/utils.js";
9849
9999
 
9850
10000
  // src/config.ts
9851
10001
  import { homedir } from "os";
10002
+ import { createHash as createHash4 } from "crypto";
9852
10003
  import { join as join2 } from "path";
9853
10004
  import { SuiGrpcClient as SuiGrpcClient2 } from "@mysten/sui/grpc";
9854
10005
  import { normalizeSuiAddress as normalizeSuiAddress7 } from "@mysten/sui/utils";
@@ -9915,6 +10066,11 @@ function configError(detail) {
9915
10066
  }
9916
10067
  function mcpCredentialsPath() {
9917
10068
  const home = process.env.HOME ?? process.env.USERPROFILE ?? homedir();
10069
+ const profile2 = process.env.SUIPAY_MCP_PROFILE?.trim();
10070
+ if (profile2) {
10071
+ const id = createHash4("sha256").update(profile2).digest("hex");
10072
+ return join2(home, ".suipay", "profiles", id, "credentials.json");
10073
+ }
9918
10074
  return join2(home, ".suipay", "credentials.json");
9919
10075
  }
9920
10076
  var MCP_TARGET_PRESETS = {
@@ -13043,6 +13199,13 @@ function receiptChallengeId(receipt) {
13043
13199
  return typeof id === "string" && id.length > 0 ? id : void 0;
13044
13200
  }
13045
13201
  var MCP_SAVE_ERROR = "delivery file was not written; do not call pay again";
13202
+ var MCP_EMPTY_DELIVERY = "the seller returned an empty delivery body; no file was written; do not call pay again";
13203
+ function deliverySaveError(cause, result) {
13204
+ const code = cause?.code;
13205
+ const reason = typeof code === "string" && /^E[A-Z0-9_]+$/.test(code) ? code : "file-system operation failed";
13206
+ const recovery = result.paid === true && result.txDigest ? `the body was not saved locally and cannot be re-fetched here; transaction digest ${result.txDigest} identifies the settlement for support` : "no confirmed transaction digest is available";
13207
+ return `${MCP_SAVE_ERROR}; cause: ${reason}; ${recovery}`;
13208
+ }
13046
13209
  function paidResult(result) {
13047
13210
  if (!isBinaryBody(result.body)) return text(result);
13048
13211
  const body = result.body;
@@ -13052,6 +13215,7 @@ function paidResult(result) {
13052
13215
  byteLength: body.byteLength
13053
13216
  };
13054
13217
  let savedTo;
13218
+ let saveError = MCP_EMPTY_DELIVERY;
13055
13219
  try {
13056
13220
  savedTo = saveDeliveredBody({
13057
13221
  bytes: body.bytes,
@@ -13059,8 +13223,8 @@ function paidResult(result) {
13059
13223
  digest: result.txDigest,
13060
13224
  challengeId: receiptChallengeId(result.receipt)
13061
13225
  });
13062
- } catch {
13063
- savedTo = void 0;
13226
+ } catch (cause) {
13227
+ saveError = deliverySaveError(cause, result);
13064
13228
  }
13065
13229
  const payload = savedTo ? {
13066
13230
  savedTo,
@@ -13069,7 +13233,7 @@ function paidResult(result) {
13069
13233
  body: shape
13070
13234
  } : {
13071
13235
  savedTo: null,
13072
- saveError: MCP_SAVE_ERROR,
13236
+ saveError,
13073
13237
  saveErrorCode: "client.delivery_file_not_saved",
13074
13238
  ...result,
13075
13239
  body: shape
@@ -13819,7 +13983,7 @@ function grantObjectIdFromBootstrap(grants, nowMs) {
13819
13983
  }
13820
13984
  if (live.length === 1) return { kind: "live", grantObjectId: live[0] };
13821
13985
  if (live.length > 1) return { kind: "ambiguous" };
13822
- if (named > 0) return { kind: "absent" };
13986
+ if (named > 0) return { kind: "expired" };
13823
13987
  if (grants.length > 0) return { kind: "unknown", detail: "delegate grants list was not usable" };
13824
13988
  return { kind: "absent" };
13825
13989
  }
@@ -14266,7 +14430,8 @@ async function suipayLogin(args, cfg, deps = {}) {
14266
14430
  target: target ?? cfg.target,
14267
14431
  delegateAddress: identity.delegateAddress,
14268
14432
  gatewayUrl: endpoints.gatewayUrl,
14269
- message: "Credentials are already saved for this agent. The existing grant could not be checked just now, so no new sign-in was started and the key file was left unchanged. Retry suipay_login in a moment."
14433
+ nextAction: "poll suipay_login_status to verify authorization before retrying payment",
14434
+ message: "Credentials are already saved for this agent. The existing grant could not be checked just now, so no new sign-in was started and the key file was left unchanged. Poll suipay_login_status in a moment."
14270
14435
  });
14271
14436
  }
14272
14437
  }
@@ -14301,7 +14466,7 @@ async function suipayLogin(args, cfg, deps = {}) {
14301
14466
  ``,
14302
14467
  `1. Use the browser window that just opened, or open the URL above`,
14303
14468
  `2. Connect the owner wallet, create a key, then Authorize on the review step`,
14304
- `3. Once Connected appears, retry the original request \u2014 credentials at \`~/.suipay/credentials.json\` are picked up on the next tool call`,
14469
+ `3. Poll \`suipay_login_status\` to confirm authorization \u2014 credentials at \`~/.suipay/credentials.json\` are active once approved`,
14305
14470
  ``,
14306
14471
  `_The login link stays valid for 5 minutes. If it expires, call \`suipay_login\` again._`,
14307
14472
  ``,
@@ -14311,11 +14476,113 @@ async function suipayLogin(args, cfg, deps = {}) {
14311
14476
  delegateAddress: identity.delegateAddress,
14312
14477
  connectUrl: url,
14313
14478
  consoleUrl: endpoints.consoleUrl,
14314
- gatewayUrl: endpoints.gatewayUrl
14479
+ gatewayUrl: endpoints.gatewayUrl,
14480
+ nextAction: "poll suipay_login_status to verify authorization before retrying payment"
14315
14481
  })
14316
14482
  ].join("\n")
14317
14483
  );
14318
14484
  }
14485
+ function defaultKeyName(delegateAddress) {
14486
+ const clean = delegateAddress.trim().toLowerCase();
14487
+ const hex = clean.startsWith("0x") ? clean.slice(2) : clean;
14488
+ const prefix = hex.slice(0, 8).padEnd(8, "0");
14489
+ return `agent-${prefix}`;
14490
+ }
14491
+ function safeRetryDetail(rawDetail) {
14492
+ if (!rawDetail) {
14493
+ return "Grant status could not be verified on chain. Retry shortly.";
14494
+ }
14495
+ let stripped = rawDetail.replace(/https?:\/\/[^\s"'<>]+/gi, "[redacted]");
14496
+ if (stripped.includes("ECONNREFUSED") || stripped.includes("ETIMEDOUT") || stripped.includes("fetch failed")) {
14497
+ return "Network connection to gateway or chain node failed. Retry shortly.";
14498
+ }
14499
+ return stripped;
14500
+ }
14501
+ var loginStatusCache = /* @__PURE__ */ new Map();
14502
+ var DEFAULT_LOGIN_STATUS_RETRY_AFTER_MS = 2e3;
14503
+ var DEFAULT_LOGIN_STATUS_CACHE_TTL_MS = 1500;
14504
+ async function suipayLoginStatus(_args = {}, cfg, deps = {}) {
14505
+ if (deps.auth) {
14506
+ return mcpFailure(
14507
+ "request.local_only",
14508
+ "suipay_login_status is for local stdio only; hosted /mcp never holds a spendable key"
14509
+ );
14510
+ }
14511
+ const loadKeypair = deps.loadLocalKeypair ?? loadLocalKeypair;
14512
+ const keypair = loadKeypair();
14513
+ if (!keypair) {
14514
+ return text({
14515
+ status: "not_started",
14516
+ nextAction: "run suipay_login"
14517
+ });
14518
+ }
14519
+ const delegateAddress = keypair.toSuiAddress();
14520
+ const stored = loadLocalSigningKeyRecord();
14521
+ const gatewayUrl = stored?.gatewayUrl ?? cfg.gatewayUrl;
14522
+ const cacheKey = `${gatewayUrl}:${delegateAddress}`;
14523
+ const now = Date.now();
14524
+ const cacheTtlMs = deps.cacheTtlMs ?? DEFAULT_LOGIN_STATUS_CACHE_TTL_MS;
14525
+ if (!deps.bypassCache) {
14526
+ const cached = loginStatusCache.get(cacheKey);
14527
+ if (cached && now - cached.timestamp < cacheTtlMs) {
14528
+ return cached.result;
14529
+ }
14530
+ }
14531
+ const checkFn = deps.checkLiveDelegateGrant ?? checkLiveDelegateGrant;
14532
+ let live;
14533
+ try {
14534
+ live = await checkFn({
14535
+ gatewayUrl,
14536
+ delegateAddress,
14537
+ signPersonalMessage: async (bytes) => {
14538
+ const { signature } = await keypair.signPersonalMessage(bytes);
14539
+ return signature;
14540
+ },
14541
+ ...deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}
14542
+ });
14543
+ } catch (err) {
14544
+ const rawMessage = err instanceof Error ? err.message : String(err);
14545
+ live = {
14546
+ kind: "unknown",
14547
+ detail: rawMessage
14548
+ };
14549
+ }
14550
+ let result;
14551
+ if (live.kind === "live") {
14552
+ result = text({
14553
+ status: "connected",
14554
+ grantObjectId: live.grantObjectId,
14555
+ delegateAddress
14556
+ });
14557
+ } else if (live.kind === "expired") {
14558
+ result = text({
14559
+ status: "expired",
14560
+ delegateAddress,
14561
+ detail: "Grant has expired. Run suipay_login to reconnect.",
14562
+ nextAction: "run suipay_login"
14563
+ });
14564
+ } else if (live.kind === "absent") {
14565
+ result = text({
14566
+ status: "pending",
14567
+ delegateAddress,
14568
+ defaultKeyName: defaultKeyName(delegateAddress)
14569
+ });
14570
+ } else if (live.kind === "ambiguous") {
14571
+ result = text({
14572
+ status: "ambiguous",
14573
+ delegateAddress,
14574
+ detail: "Multiple live grants found for this delegate key. Revoke the duplicate grants and reconnect."
14575
+ });
14576
+ } else {
14577
+ result = text({
14578
+ status: "retry",
14579
+ retryAfterMs: DEFAULT_LOGIN_STATUS_RETRY_AFTER_MS,
14580
+ detail: safeRetryDetail(live.detail)
14581
+ });
14582
+ }
14583
+ loginStatusCache.set(cacheKey, { timestamp: Date.now(), result });
14584
+ return result;
14585
+ }
14319
14586
  async function logout(args, _cfg, deps = {}) {
14320
14587
  if (args.force) {
14321
14588
  clearCreds();
@@ -14419,6 +14686,14 @@ var TOOLS = [
14419
14686
  }
14420
14687
  }
14421
14688
  },
14689
+ {
14690
+ name: "suipay_login_status",
14691
+ description: `Check whether the local delegate key has an approved, active spend-account grant, read from the gateway's delegate-grant state. Read-only and pollable (does not mint keys, write credentials, or sign transactions). Returns one of five states: (1) "not_started" if no local key exists yet (call suipay_login first); (2) "connected" if a live grant exists for this local delegate key (note: "connected" means a live grant exists for this delegate key, which may be pre-existing from a prior session); (3) "pending" if the key exists but no grant has been approved yet (includes delegateAddress and a defaultKeyName of format "agent-<first-8-hex-chars-of-address-without-0x>"; note that if the owner already has a live key with this normalized name, creating it may yield 409 CONFLICT); (4) "ambiguous" (terminal) if several live grants exist for this delegate key \u2014 stop polling and revoke the duplicates; (5) "retry" if the status could not be verified (includes retryAfterMs floor and safe detail). Stdio only; hosted /mcp never holds a spendable key.`,
14692
+ inputSchema: {
14693
+ type: "object",
14694
+ properties: {}
14695
+ }
14696
+ },
14422
14697
  {
14423
14698
  name: "suipay_logout",
14424
14699
  description: "Remove the local delegate-key credentials file. Missing file is success. Force clears a corrupt file without printing the seed.",
@@ -14566,6 +14841,14 @@ function createServer2(cfg, auth, trace, options = {}) {
14566
14841
  }
14567
14842
  );
14568
14843
  }
14844
+ case "suipay_login_status":
14845
+ if (auth) {
14846
+ return mcpFailure(
14847
+ "request.local_only",
14848
+ "suipay_login_status is for local stdio only; hosted /mcp never holds a spendable key"
14849
+ );
14850
+ }
14851
+ return await suipayLoginStatus(args, cfg, { auth });
14569
14852
  case "suipay_logout":
14570
14853
  if (auth) {
14571
14854
  return mcpFailure(
@@ -865,6 +865,17 @@ declare const ERRORS: {
865
865
  readonly user: null;
866
866
  readonly logOnly: true;
867
867
  };
868
+ readonly 'indexer.native_settlement_deferred': {
869
+ readonly surfaces: readonly ["indexer"];
870
+ readonly status: null;
871
+ readonly title: "Native settlement is deferred; holding the checkpoint";
872
+ readonly paymentState: "not_charged";
873
+ readonly retry: "after_delay";
874
+ readonly retryAfterSeconds: 5;
875
+ readonly nextAction: "retry_later";
876
+ readonly user: null;
877
+ readonly logOnly: true;
878
+ };
868
879
  readonly 'indexer.config_invalid': {
869
880
  readonly surfaces: readonly ["indexer"];
870
881
  readonly status: null;
@@ -1267,6 +1278,16 @@ declare const ERRORS: {
1267
1278
  readonly user: null;
1268
1279
  readonly logOnly: true;
1269
1280
  };
1281
+ readonly 'service.readiness': {
1282
+ readonly surfaces: readonly ["gateway"];
1283
+ readonly status: null;
1284
+ readonly title: "Readiness gauge updated";
1285
+ readonly paymentState: "not_charged";
1286
+ readonly retry: "never";
1287
+ readonly nextAction: "none";
1288
+ readonly user: null;
1289
+ readonly logOnly: true;
1290
+ };
1270
1291
  readonly 'service.bookkeeping_failed': {
1271
1292
  readonly surfaces: readonly ["gateway"];
1272
1293
  readonly status: null;
@@ -3504,6 +3525,55 @@ declare const ERRORS: {
3504
3525
  readonly message: "That web address doesn't exist. Check the spelling and try again.";
3505
3526
  };
3506
3527
  };
3528
+ readonly 'request.recipient_changed': {
3529
+ readonly surfaces: readonly ["gateway"];
3530
+ readonly status: 409;
3531
+ readonly title: "Pay-to address changed while its registry was being checked";
3532
+ readonly paymentState: "not_charged";
3533
+ readonly retry: "never";
3534
+ readonly nextAction: "fix_request";
3535
+ readonly user: {
3536
+ readonly title: "Retry this publish";
3537
+ readonly message: "The payout address changed while its payment registry was being checked. Retry so the current address is checked.";
3538
+ };
3539
+ };
3540
+ readonly 'request.recipient_registry_invalid': {
3541
+ readonly surfaces: readonly ["gateway"];
3542
+ readonly status: 409;
3543
+ readonly title: "Payment registry does not verify for this deployment";
3544
+ readonly paymentState: "not_charged";
3545
+ readonly retry: "never";
3546
+ readonly nextAction: "fix_request";
3547
+ readonly user: {
3548
+ readonly title: "Registry does not match";
3549
+ readonly message: "The payout address's payment registry does not match this deployment. Fix it, then publish again.";
3550
+ };
3551
+ };
3552
+ readonly 'request.recipient_registry_missing': {
3553
+ readonly surfaces: readonly ["gateway"];
3554
+ readonly status: 409;
3555
+ readonly title: "Pay-to address has no payment registry";
3556
+ readonly paymentState: "not_charged";
3557
+ readonly retry: "never";
3558
+ readonly nextAction: "fix_request";
3559
+ readonly user: {
3560
+ readonly title: "Create a payment registry";
3561
+ readonly message: "This payout address has no payment registry on this network yet. Create it in the seller console, then publish again.";
3562
+ };
3563
+ };
3564
+ readonly 'request.recipient_registry_unreadable': {
3565
+ readonly surfaces: readonly ["gateway"];
3566
+ readonly status: 503;
3567
+ readonly title: "Payment registry could not be read";
3568
+ readonly paymentState: "not_charged";
3569
+ readonly retry: "after_delay";
3570
+ readonly retryAfterSeconds: 5;
3571
+ readonly nextAction: "retry_later";
3572
+ readonly user: {
3573
+ readonly title: "Registry unread";
3574
+ readonly message: "The payment registry could not be read just now. Wait a moment and try again.";
3575
+ };
3576
+ };
3507
3577
  readonly 'request.redirect_refused': {
3508
3578
  readonly surfaces: readonly ["console_server", "sdk"];
3509
3579
  readonly status: 422;
@@ -4912,6 +4982,13 @@ declare const TOOLS: readonly [{
4912
4982
  };
4913
4983
  };
4914
4984
  };
4985
+ }, {
4986
+ readonly name: "suipay_login_status";
4987
+ readonly description: string;
4988
+ readonly inputSchema: {
4989
+ readonly type: "object";
4990
+ readonly properties: {};
4991
+ };
4915
4992
  }, {
4916
4993
  readonly name: "suipay_logout";
4917
4994
  readonly description: "Remove the local delegate-key credentials file. Missing file is success. Force clears a corrupt file without printing the seed.";
package/dist/http.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { I as handleSuipayMcpHttpRequest } from './http-BFEAurkF.js';
1
+ export { I as handleSuipayMcpHttpRequest } from './http-ZG_iISm1.js';
2
2
  import '@modelcontextprotocol/sdk/server/index.js';
3
3
  import '@mysten/sui/keypairs/ed25519';
4
4
  import 'node:crypto';
package/dist/http.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  handleSuipayMcpHttpRequest
3
- } from "./chunk-WPRFEMBU.js";
3
+ } from "./chunk-RD34GGSA.js";
4
4
  export {
5
5
  handleSuipayMcpHttpRequest
6
6
  };
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { S as SpendAccountSigningRequest, P as PaidHttpRequest, D as DelegatedKeySigner, G as GetObjectJson, a as DelegatedPayerSession, b as DelegatedPayerConfig, c as DelegatedPayerDeps, d as SpendIntent, e as DelegatedPayResult, f as DelegationScope, C as CodeFor, N as NextAction, g as GrantPolicyBinding, h as PaymentAttemptLedger, O as OfferPublicKeySource, i as DelegatedPayerLog, j as PreparedSettlementTerms, X as X402PayProgress, k as Offer, M as McpConfig, l as McpAuthContext, m as McpTraceHooks, n as SpendAccountPayDeps, o as createReader } from './http-BFEAurkF.js';
2
- export { A as AgentHeldResolver, p as AgentHeldSettlement, q as MCP_TARGET_PRESETS, r as McpEnvelope, s as McpEnvelopeEntry, t as McpServerOptions, u as McpTarget, v as McpTraceContext, w as McpTraceRuntime, x as PACKAGE_NAME, y as PreparedPayment, z as SpendAccountPrepareResult, B as SpendAccountPrepareSession, T as TOOLS, E as buildSpendAccountPayKind, F as canonicalMcpRequestId, H as createServer, I as handleSuipayMcpHttpRequest, J as inspectMcpEnvelope, K as loadMcpConfig, L as parseMcpTargetFlag, Q as prepareSpendAccountPayment, R as resetAgentHeldProcessLedger, U as resolveAgentHeldSettlement, V as resolveGrantTarget, W as resolveMcpTarget, Y as sameMcpGateway, Z as traceMcpHttpRequest, _ as verifySpendAccountPayReconstructs } from './http-BFEAurkF.js';
3
- import { SuiGrpcClient } from '@mysten/sui/grpc';
4
1
  import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
2
+ import { S as SpendAccountSigningRequest, P as PaidHttpRequest, D as DelegatedKeySigner, G as GetObjectJson, a as DelegatedPayerSession, b as DelegatedPayerConfig, c as DelegatedPayerDeps, d as SpendIntent, e as DelegatedPayResult, f as DelegationScope, C as CodeFor, N as NextAction, g as GrantPolicyBinding, h as PaymentAttemptLedger, O as OfferPublicKeySource, i as DelegatedPayerLog, j as PreparedSettlementTerms, X as X402PayProgress, k as Offer, M as McpConfig, l as McpAuthContext, m as McpTraceHooks, n as SpendAccountPayDeps, o as createReader } from './http-ZG_iISm1.js';
3
+ export { A as AgentHeldResolver, p as AgentHeldSettlement, q as MCP_TARGET_PRESETS, r as McpEnvelope, s as McpEnvelopeEntry, t as McpServerOptions, u as McpTarget, v as McpTraceContext, w as McpTraceRuntime, x as PACKAGE_NAME, y as PreparedPayment, z as SpendAccountPrepareResult, B as SpendAccountPrepareSession, T as TOOLS, E as buildSpendAccountPayKind, F as canonicalMcpRequestId, H as createServer, I as handleSuipayMcpHttpRequest, J as inspectMcpEnvelope, K as loadMcpConfig, L as parseMcpTargetFlag, Q as prepareSpendAccountPayment, R as resetAgentHeldProcessLedger, U as resolveAgentHeldSettlement, V as resolveGrantTarget, W as resolveMcpTarget, Y as sameMcpGateway, Z as traceMcpHttpRequest, _ as verifySpendAccountPayReconstructs } from './http-ZG_iISm1.js';
4
+ import { SuiGrpcClient } from '@mysten/sui/grpc';
5
5
  import { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
6
6
  import '@modelcontextprotocol/sdk/server/index.js';
7
7
  import 'node:crypto';
@@ -65,6 +65,8 @@ declare function loadLocalSigner(): {
65
65
  signTransaction: (txBytes: string) => Promise<string>;
66
66
  signPersonalMessage: (message: Uint8Array) => Promise<string>;
67
67
  } | null;
68
+ /** Ed25519 keypair from the persisted seed. Null when this process holds no key. */
69
+ declare function loadLocalKeypair(): Ed25519Keypair | null;
68
70
 
69
71
  /**
70
72
  * Client-side completion of an MCP prepared payment.
@@ -581,6 +583,10 @@ interface ToolDeps {
581
583
  createPayer?: typeof createPayer;
582
584
  createReader?: typeof createReader;
583
585
  startStdioLogin?: typeof startStdioLogin;
586
+ loadLocalKeypair?: typeof loadLocalKeypair;
587
+ checkLiveDelegateGrant?: typeof checkLiveDelegateGrant;
588
+ bypassCache?: boolean;
589
+ cacheTtlMs?: number;
584
590
  }
585
591
  declare function pay(args: {
586
592
  url: string;
@@ -591,6 +597,25 @@ declare function pay(args: {
591
597
  recipient?: unknown;
592
598
  maxAmount?: unknown;
593
599
  }, cfg: McpConfig, deps?: ToolDeps): Promise<ToolResult>;
600
+ type LiveGrantCheck = {
601
+ kind: 'live';
602
+ grantObjectId: string;
603
+ } | {
604
+ kind: 'ambiguous';
605
+ } | {
606
+ kind: 'absent';
607
+ } | {
608
+ kind: 'expired';
609
+ } | {
610
+ kind: 'unknown';
611
+ detail: string;
612
+ };
613
+ declare function checkLiveDelegateGrant(args: {
614
+ gatewayUrl: string;
615
+ delegateAddress: string;
616
+ signPersonalMessage: (message: Uint8Array) => Promise<string>;
617
+ fetchImpl?: typeof fetch;
618
+ }): Promise<LiveGrantCheck>;
594
619
  declare function receipts(args: {
595
620
  challengeId?: string;
596
621
  }, cfg: McpConfig, deps?: ToolDeps): Promise<ToolResult>;
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  loadBootProfile,
3
3
  main
4
- } from "./chunk-L7OTT6U5.js";
4
+ } from "./chunk-D6LFU4VE.js";
5
5
  import {
6
6
  MCP_TARGET_PRESETS,
7
7
  PACKAGE_NAME,
@@ -26,7 +26,7 @@ import {
26
26
  settlePreparedMcpPayment,
27
27
  traceMcpHttpRequest,
28
28
  verifySpendAccountPayReconstructs
29
- } from "./chunk-WPRFEMBU.js";
29
+ } from "./chunk-RD34GGSA.js";
30
30
  export {
31
31
  MCP_TARGET_PRESETS,
32
32
  PACKAGE_NAME,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mysten-incubation/suipay-mcp",
3
- "version": "0.3.0-dev.1",
3
+ "version": "0.3.0-dev.10",
4
4
  "type": "module",
5
5
  "description": "SuiPay MCP — pay MPP-gated APIs from a spend-account grant with an agent-held delegate key.",
6
6
  "repository": {