@cmdoss/suipay-mcp 0.2.2-dev.7 → 0.2.2-dev.9

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,13 +5,13 @@ 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 `@cmdoss/suipay-mcp@0.2.2` in MCP config. `1.0.1` remains on the
8
+ Pin `@cmdoss/suipay-mcp@0.2.2-dev.9` in MCP config. `1.0.1` remains on the
9
9
  registry as a historical release; do not install it for new agents.
10
10
 
11
11
  ## Install
12
12
 
13
13
  ```bash
14
- claude mcp add --scope user suipay -- npx -y @cmdoss/suipay-mcp@0.2.2
14
+ claude mcp add --scope user suipay -- npx -y @cmdoss/suipay-mcp@0.2.2-dev.9
15
15
  ```
16
16
 
17
17
  No wallet secret belongs in this command or MCP config. There is no
@@ -112,10 +112,10 @@ Default is Railway **suipay-dev**. `--local` / `target: "local"` is laptop-only.
112
112
 
113
113
  ```bash
114
114
  # Railway suipay-dev (default)
115
- npx @cmdoss/suipay-mcp@0.2.2
115
+ npx @cmdoss/suipay-mcp@0.2.2-dev.9
116
116
 
117
117
  # Laptop stack
118
- npx @cmdoss/suipay-mcp@0.2.2 --local
118
+ npx @cmdoss/suipay-mcp@0.2.2-dev.9 --local
119
119
  ```
120
120
 
121
121
  `SUIPAY_MCP_TARGET=local|dev` selects the same preset. Explicit
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  main
4
- } from "../chunk-KR6WRUDY.js";
5
- import "../chunk-6ZSOXII5.js";
4
+ } from "../chunk-LQOZBRH6.js";
5
+ import "../chunk-NDJ7BFI4.js";
6
6
 
7
7
  // src/bin/suipay.ts
8
8
  main().catch((err) => {
@@ -4,7 +4,7 @@ import {
4
4
  loadLocalSigningKeyRecord,
5
5
  loadMcpConfig,
6
6
  loadPaymentProfile
7
- } from "./chunk-6ZSOXII5.js";
7
+ } from "./chunk-NDJ7BFI4.js";
8
8
 
9
9
  // src/index.ts
10
10
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -538,6 +538,32 @@ function createSpendAccountChain(options) {
538
538
  kindBytes: toBase64(await tx.build({ client, onlyTransactionKind: true }))
539
539
  };
540
540
  }
541
+ function appendCreatePolicyGrant(tx, input) {
542
+ if (!input.onChainPolicyIds.length) {
543
+ throw new Error("create_grant requires at least one on-chain policy id");
544
+ }
545
+ if (!input.maxPerPayment || BigInt(input.maxPerPayment) <= 0n) {
546
+ throw new Error("create_grant requires a non-zero max_per_payment");
547
+ }
548
+ if (!input.sessionCap || BigInt(input.sessionCap) <= 0n) {
549
+ throw new Error("create_grant requires a non-zero session_cap");
550
+ }
551
+ const expires = input.expiresAtMs === void 0 || input.expiresAtMs === null ? 0n : BigInt(input.expiresAtMs);
552
+ tx.moveCall({
553
+ target: target("create_policy_grant"),
554
+ typeArguments: [input.coinType],
555
+ arguments: [
556
+ tx.object(input.poolObjectId),
557
+ tx.pure.address(input.delegate),
558
+ // The unified Grant<T> takes Option<u64> caps. This builder rejects
559
+ // absent or zero caps above, so both are always Some here.
560
+ tx.pure.option("u64", BigInt(input.sessionCap)),
561
+ tx.pure.option("u64", BigInt(input.maxPerPayment)),
562
+ tx.pure.u64(expires),
563
+ tx.pure.vector("u64", input.onChainPolicyIds.map((id) => BigInt(id)))
564
+ ]
565
+ });
566
+ }
541
567
  async function fundingCoin(tx, owner, coinType, amount) {
542
568
  if (!isSuiCoinType(coinType)) {
543
569
  return coinWithBalance({ type: coinType, balance: amount });
@@ -737,32 +763,23 @@ function createSpendAccountChain(options) {
737
763
  return kindBytes(tx);
738
764
  },
739
765
  async buildCreateGrantKind(input) {
740
- if (!input.onChainPolicyIds.length) {
741
- throw new Error("create_grant requires at least one on-chain policy id");
742
- }
743
- if (!input.maxPerPayment || BigInt(input.maxPerPayment) <= 0n) {
744
- throw new Error("create_grant requires a non-zero max_per_payment");
766
+ const tx = new Transaction2();
767
+ tx.setSender(input.owner);
768
+ appendCreatePolicyGrant(tx, input);
769
+ return kindBytes(tx);
770
+ },
771
+ async buildCreateGrantBatchKind(inputs) {
772
+ if (!inputs.length) {
773
+ throw new Error("create_grant batch requires at least one grant");
745
774
  }
746
- if (!input.sessionCap || BigInt(input.sessionCap) <= 0n) {
747
- throw new Error("create_grant requires a non-zero session_cap");
775
+ const owner = inputs[0].owner;
776
+ if (inputs.some((input) => input.owner !== owner)) {
777
+ throw new Error("create_grant batch requires one owner");
748
778
  }
749
779
  const tx = new Transaction2();
750
- tx.setSender(input.owner);
751
- const expires = input.expiresAtMs === void 0 || input.expiresAtMs === null ? 0n : BigInt(input.expiresAtMs);
752
- tx.moveCall({
753
- target: target("create_policy_grant"),
754
- typeArguments: [input.coinType],
755
- arguments: [
756
- tx.object(input.poolObjectId),
757
- tx.pure.address(input.delegate),
758
- // The unified Grant<T> takes Option<u64> caps. This builder rejects
759
- // absent or zero caps above, so both are always Some here.
760
- tx.pure.option("u64", BigInt(input.sessionCap)),
761
- tx.pure.option("u64", BigInt(input.maxPerPayment)),
762
- tx.pure.u64(expires),
763
- tx.pure.vector("u64", input.onChainPolicyIds.map((id) => BigInt(id)))
764
- ]
765
- });
780
+ tx.setSender(owner);
781
+ for (const input of inputs)
782
+ appendCreatePolicyGrant(tx, input);
766
783
  return kindBytes(tx);
767
784
  },
768
785
  async buildCreateSpendGrantKind(input) {
@@ -5563,7 +5580,7 @@ var MCP_TARGET_PRESETS = {
5563
5580
  gatewayUrl: "https://gateway-dev-ba84.up.railway.app"
5564
5581
  },
5565
5582
  local: {
5566
- consoleUrl: "http://localhost:3000",
5583
+ consoleUrl: "https://suipay-dev.up.railway.app",
5567
5584
  gatewayUrl: "http://127.0.0.1:4340"
5568
5585
  }
5569
5586
  };
@@ -5675,6 +5692,35 @@ function loadMcpConfig(env = process.env, argv = []) {
5675
5692
  exclusionDirectoryObjectId: rawDir || null
5676
5693
  };
5677
5694
  }
5695
+ var OBJECT_ID = /^0x(?:[0-9a-fA-F]{1,64})$/;
5696
+ async function publishedExclusionDirectoryId(gatewayUrl, fetchImpl = fetch) {
5697
+ let url;
5698
+ try {
5699
+ url = new URL("/v1/delegate/deployment", gatewayUrl).toString();
5700
+ } catch {
5701
+ return null;
5702
+ }
5703
+ let response;
5704
+ try {
5705
+ response = await fetchImpl(url, {
5706
+ method: "GET",
5707
+ redirect: "manual",
5708
+ signal: AbortSignal.timeout(1500)
5709
+ });
5710
+ } catch {
5711
+ return null;
5712
+ }
5713
+ if (response.status !== 200) return null;
5714
+ let body;
5715
+ try {
5716
+ body = await response.json();
5717
+ } catch {
5718
+ return null;
5719
+ }
5720
+ const id = body && typeof body === "object" && "exclusionDirectoryObjectId" in body ? body.exclusionDirectoryObjectId : void 0;
5721
+ if (typeof id !== "string" || !OBJECT_ID.test(id.trim())) return null;
5722
+ return id.trim();
5723
+ }
5678
5724
  function rpcClient(cfg) {
5679
5725
  return new SuiGrpcClient2({ network: cfg.network, baseUrl: cfg.rpcUrl });
5680
5726
  }
@@ -5967,9 +6013,29 @@ function recordFromKeypair(kp, opts = {}, existing, operatorSecret) {
5967
6013
  };
5968
6014
  }
5969
6015
  var ENV_KEY_MISMATCH = "SUIPAY_MCP_KEY does not match the stored delegate key - refusing to overwrite";
6016
+ function signingMaterial(parsed) {
6017
+ if (!parsed || typeof parsed !== "object") return null;
6018
+ if (isLocalSigningKeyRecord(parsed)) return parsed;
6019
+ const c = parsed;
6020
+ if (typeof c.delegatePrivateKey !== "string" || !HEX64.test(c.delegatePrivateKey) || typeof c.delegatePublicKeyHex !== "string" || !HEX64.test(c.delegatePublicKeyHex) || typeof c.delegateAddress !== "string" || !ADDR.test(c.delegateAddress)) {
6021
+ return null;
6022
+ }
6023
+ const label = typeof c.label === "string" ? c.label.trim() : "";
6024
+ return {
6025
+ version: 2,
6026
+ delegatePrivateKey: c.delegatePrivateKey,
6027
+ delegatePublicKeyHex: c.delegatePublicKeyHex,
6028
+ delegateAddress: c.delegateAddress,
6029
+ createdAt: typeof c.createdAt === "string" && c.createdAt ? c.createdAt : (/* @__PURE__ */ new Date(0)).toISOString(),
6030
+ ...isHttpUrl(c.gatewayUrl) ? { gatewayUrl: c.gatewayUrl } : {},
6031
+ ...label ? { label } : {}
6032
+ };
6033
+ }
5970
6034
  function existingRecordForEnv(parsed) {
5971
6035
  if (parsed === null) return null;
5972
- if (isLocalSigningKeyRecord(parsed) || isValid(parsed)) return parsed;
6036
+ if (isValid(parsed)) return parsed;
6037
+ const material = signingMaterial(parsed);
6038
+ if (material) return material;
5973
6039
  throw new Error(
5974
6040
  `credentials file at ${credsPath()} failed schema validation - refusing to continue`
5975
6041
  );
@@ -6007,16 +6073,25 @@ async function ensureLocalSigningKey(opts = {}) {
6007
6073
  }
6008
6074
  const parsed = readCredentialsJson();
6009
6075
  if (parsed !== null) {
6010
- if (isLocalSigningKeyRecord(parsed)) {
6011
- return identityFromStored(
6012
- parsed.delegatePrivateKey,
6013
- parsed.delegatePublicKeyHex,
6014
- parsed.delegateAddress
6015
- );
6016
- }
6017
- if (isValid(parsed)) {
6076
+ if (isValid(parsed) && !isLocalSigningKeyRecord(parsed)) {
6018
6077
  return rewriteV1ToLocalSigningKey(parsed, opts);
6019
6078
  }
6079
+ const material = signingMaterial(parsed);
6080
+ if (material) {
6081
+ const identity = identityFromStored(
6082
+ material.delegatePrivateKey,
6083
+ material.delegatePublicKeyHex,
6084
+ material.delegateAddress
6085
+ );
6086
+ if (!isLocalSigningKeyRecord(parsed)) {
6087
+ const label = opts.label?.trim() || material.label;
6088
+ writeCredentialsFile({
6089
+ ...material,
6090
+ ...label ? { label } : {}
6091
+ });
6092
+ }
6093
+ return identity;
6094
+ }
6020
6095
  throw new Error(`credentials file at ${credsPath()} failed schema validation - refusing to continue`);
6021
6096
  }
6022
6097
  const kp = await generateKeypair();
@@ -6107,6 +6182,32 @@ function loadLocalKeypair() {
6107
6182
  Uint8Array.from(Buffer.from(parsed.delegatePrivateKey, "hex"))
6108
6183
  );
6109
6184
  }
6185
+ function replaceLocalSigningKey(input) {
6186
+ identityFromStored(
6187
+ input.delegatePrivateKey,
6188
+ input.delegatePublicKeyHex,
6189
+ input.delegateAddress
6190
+ );
6191
+ const parsed = readCredentialsJson();
6192
+ let gatewayUrl;
6193
+ let label;
6194
+ if (parsed && typeof parsed === "object") {
6195
+ const record = parsed;
6196
+ if (isHttpUrl(record.gatewayUrl)) gatewayUrl = record.gatewayUrl;
6197
+ if (typeof record.label === "string" && record.label.trim()) {
6198
+ label = record.label.trim();
6199
+ }
6200
+ }
6201
+ writeCredentialsFile({
6202
+ version: 2,
6203
+ delegatePrivateKey: input.delegatePrivateKey,
6204
+ delegatePublicKeyHex: input.delegatePublicKeyHex,
6205
+ delegateAddress: input.delegateAddress,
6206
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
6207
+ ...gatewayUrl ? { gatewayUrl } : {},
6208
+ ...label ? { label } : {}
6209
+ });
6210
+ }
6110
6211
  function persistGatewayUrl(gatewayUrl) {
6111
6212
  if (!isHttpUrl(gatewayUrl)) {
6112
6213
  throw new Error("gatewayUrl must be an http(s) origin");
@@ -6938,7 +7039,23 @@ async function fetchGatewayDeployment(options) {
6938
7039
  }
6939
7040
  };
6940
7041
  }
6941
- return { ok: true, value: { network: d.network, packageId: d.packageId } };
7042
+ if (d.exclusionDirectoryObjectId !== void 0 && !isNonEmptyString2(d.exclusionDirectoryObjectId)) {
7043
+ return {
7044
+ ok: false,
7045
+ error: {
7046
+ code: MANIFEST_ERROR_CODES.MALFORMED_MANIFEST,
7047
+ detail: "gateway deployment identity carries an unusable exclusionDirectoryObjectId"
7048
+ }
7049
+ };
7050
+ }
7051
+ return {
7052
+ ok: true,
7053
+ value: {
7054
+ network: d.network,
7055
+ packageId: d.packageId,
7056
+ ...isNonEmptyString2(d.exclusionDirectoryObjectId) ? { exclusionDirectoryObjectId: d.exclusionDirectoryObjectId } : {}
7057
+ }
7058
+ };
6942
7059
  }
6943
7060
  function explainBlockedGrant(raw, error2) {
6944
7061
  if (error2.code !== MANIFEST_ERROR_CODES.MISSING_POLICY_BINDING)
@@ -7030,7 +7147,7 @@ async function resolvePayerSession(options) {
7030
7147
  ...options.select ? { select: options.select } : {}
7031
7148
  });
7032
7149
  if (whole.ok)
7033
- return whole;
7150
+ return { ok: true, value: { ...whole.value, deployment } };
7034
7151
  const data = bootstrap.data;
7035
7152
  const rawGrants = Array.isArray(data.grants) ? data.grants : [];
7036
7153
  const usable = [];
@@ -7065,11 +7182,12 @@ async function resolvePayerSession(options) {
7065
7182
  const pick = scoped ?? blocked[0];
7066
7183
  return pick ? { ok: false, error: explainBlockedGrant(pick.raw, pick.error) } : whole;
7067
7184
  }
7068
- return manifestToSession({
7185
+ const resolved = manifestToSession({
7069
7186
  manifest: { ...data, grants: usable },
7070
7187
  signer: options.signer,
7071
7188
  ...options.select ? { select: options.select } : {}
7072
7189
  });
7190
+ return resolved.ok ? { ok: true, value: { ...resolved.value, deployment } } : resolved;
7073
7191
  }
7074
7192
 
7075
7193
  // ../sdk/dist/buyer/transport.js
@@ -7099,6 +7217,9 @@ function mergeObjectEnvelope(object) {
7099
7217
  var SECRET_KEY_SET = new Set(FORBIDDEN_SECRET_KEYS.map((k) => k.toLowerCase()));
7100
7218
 
7101
7219
  // ../sdk/dist/agent-sdk/delegate-agent.js
7220
+ function resolveExclusionDirectoryId(explicit, discovered) {
7221
+ return explicit?.trim() ? explicit : discovered;
7222
+ }
7102
7223
  function createExclusionRegistryResolver(client, exclusionDirectoryObjectId) {
7103
7224
  if (!exclusionDirectoryObjectId || !exclusionDirectoryObjectId.trim())
7104
7225
  return void 0;
@@ -7167,8 +7288,17 @@ async function createPayer(options) {
7167
7288
  const { session, binding } = resolved.value;
7168
7289
  const deployment = {
7169
7290
  network: session.network,
7170
- packageId: session.packageId
7291
+ packageId: session.packageId,
7292
+ // What the GATEWAY published, not what this payer settled on: a caller
7293
+ // override belongs to the payer's configuration, not to the deployment it
7294
+ // is describing. Dropping this left the field permanently undefined on the
7295
+ // one instance a consumer can reach, so anything asking the payer whether
7296
+ // guarded settles are available got "no" while it was doing them.
7297
+ ...resolved.value.deployment.exclusionDirectoryObjectId ? {
7298
+ exclusionDirectoryObjectId: resolved.value.deployment.exclusionDirectoryObjectId
7299
+ } : {}
7171
7300
  };
7301
+ const exclusionDirectoryObjectId = resolveExclusionDirectoryId(options.exclusionDirectoryObjectId, resolved.value.deployment.exclusionDirectoryObjectId);
7172
7302
  let network;
7173
7303
  try {
7174
7304
  network = toGrpcNetwork(deployment.network);
@@ -7227,7 +7357,7 @@ async function createPayer(options) {
7227
7357
  // directory id was configured, so such an offer fails closed (BUILD_FAILED)
7228
7358
  // rather than building an unguarded settle. Unguarded offers never call it.
7229
7359
  ...(() => {
7230
- const resolve = createExclusionRegistryResolver(chain.client, options.exclusionDirectoryObjectId);
7360
+ const resolve = createExclusionRegistryResolver(chain.client, exclusionDirectoryObjectId);
7231
7361
  return resolve ? { resolveExclusionRegistry: resolve } : {};
7232
7362
  })(),
7233
7363
  // x402 bounds the transaction's expiration by the current epoch, read from
@@ -7286,6 +7416,7 @@ async function createPayer(options) {
7286
7416
  import { createServer } from "http";
7287
7417
  import { randomBytes as randomBytes3, timingSafeEqual as timingSafeEqual2 } from "crypto";
7288
7418
  import open from "open";
7419
+ import { Ed25519Keypair as Ed25519Keypair3 } from "@mysten/sui/keypairs/ed25519";
7289
7420
  var HEX642 = /^[0-9a-f]{64}$/;
7290
7421
  var PUBHEX = /^[0-9a-fA-F]{64}$/;
7291
7422
  function isPreflight(obj) {
@@ -7293,6 +7424,11 @@ function isPreflight(obj) {
7293
7424
  const o = obj;
7294
7425
  return typeof o.state === "string" && HEX642.test(o.state) && typeof o.publicKey === "string" && PUBHEX.test(o.publicKey) && typeof o.delegateAddress === "string" && /^0x[0-9a-fA-F]{64}$/.test(o.delegateAddress) && typeof o.gateway === "string" && o.gateway.length > 0;
7295
7426
  }
7427
+ function isAdoptKey(obj) {
7428
+ if (!obj || typeof obj !== "object") return false;
7429
+ const o = obj;
7430
+ return typeof o.state === "string" && HEX642.test(o.state) && typeof o.delegatePrivateKey === "string" && HEX642.test(o.delegatePrivateKey) && typeof o.delegatePublicKeyHex === "string" && PUBHEX.test(o.delegatePublicKeyHex) && typeof o.delegateAddress === "string" && /^0x[0-9a-fA-F]{64}$/.test(o.delegateAddress);
7431
+ }
7296
7432
  function isCallback(obj) {
7297
7433
  if (!obj || typeof obj !== "object") return false;
7298
7434
  const o = obj;
@@ -7417,7 +7553,7 @@ async function startStdioLogin(opts) {
7417
7553
  res.writeHead(code, { "content-type": type, ...SECURITY_HEADERS, ...extra });
7418
7554
  res.end(body);
7419
7555
  };
7420
- const okPath = url.pathname === "/preflight" || url.pathname === "/callback";
7556
+ const okPath = url.pathname === "/preflight" || url.pathname === "/callback" || url.pathname === "/key";
7421
7557
  if (req.method === "OPTIONS") {
7422
7558
  const extra = { ...cors };
7423
7559
  if (okHost && okOrigin && okPath && req.headers["access-control-request-private-network"] === "true") {
@@ -7488,6 +7624,55 @@ async function startStdioLogin(opts) {
7488
7624
  );
7489
7625
  return;
7490
7626
  }
7627
+ if (url.pathname === "/key") {
7628
+ if (!preflightVerified) {
7629
+ res.writeHead(428, {
7630
+ "content-type": "application/json",
7631
+ ...cors,
7632
+ ...SECURITY_HEADERS
7633
+ });
7634
+ res.end(JSON.stringify({ ok: false, error: "preflight required" }));
7635
+ return;
7636
+ }
7637
+ if (!isAdoptKey(parsed) || !stateEquals(parsed.state, connectState)) {
7638
+ res.writeHead(400, {
7639
+ "content-type": "application/json",
7640
+ ...cors,
7641
+ ...SECURITY_HEADERS
7642
+ });
7643
+ res.end(JSON.stringify({ ok: false, error: "invalid key payload" }));
7644
+ return;
7645
+ }
7646
+ try {
7647
+ const kp = Ed25519Keypair3.fromSecretKey(
7648
+ Uint8Array.from(Buffer.from(parsed.delegatePrivateKey, "hex"))
7649
+ );
7650
+ const derived = kp.getPublicKey().toSuiAddress();
7651
+ if (derived.toLowerCase() !== parsed.delegateAddress.toLowerCase()) {
7652
+ throw new Error("address mismatch");
7653
+ }
7654
+ replaceLocalSigningKey({
7655
+ delegatePrivateKey: parsed.delegatePrivateKey,
7656
+ delegatePublicKeyHex: parsed.delegatePublicKeyHex,
7657
+ delegateAddress: derived
7658
+ });
7659
+ } catch {
7660
+ res.writeHead(400, {
7661
+ "content-type": "application/json",
7662
+ ...cors,
7663
+ ...SECURITY_HEADERS
7664
+ });
7665
+ res.end(JSON.stringify({ ok: false, error: "key rejected" }));
7666
+ return;
7667
+ }
7668
+ res.writeHead(200, {
7669
+ "content-type": "application/json",
7670
+ ...cors,
7671
+ ...SECURITY_HEADERS
7672
+ });
7673
+ res.end(JSON.stringify({ ok: true }));
7674
+ return;
7675
+ }
7491
7676
  if (!preflightVerified) {
7492
7677
  res.writeHead(428, {
7493
7678
  "content-type": "text/html",
@@ -7591,6 +7776,22 @@ import { fromBase64 as fromBase6410, toBase64 as toBase646 } from "@mysten/sui/u
7591
7776
 
7592
7777
  // src/spend-account-pay.ts
7593
7778
  var SUI_CLOCK3 = "0x6";
7779
+ var sharedObjectReaderForTests = null;
7780
+ function chainSharedObjectReader(cfg, packageId) {
7781
+ const chain = createSpendAccountChain({
7782
+ packageId,
7783
+ network: cfg.network,
7784
+ url: cfg.rpcUrl
7785
+ });
7786
+ return async (objectId) => {
7787
+ const { object } = await chain.client.core.getObject({
7788
+ objectId,
7789
+ include: { json: true }
7790
+ });
7791
+ if (!object?.owner) return null;
7792
+ return { owner: object.owner };
7793
+ };
7794
+ }
7594
7795
  function traceEmitter(trace) {
7595
7796
  if (!trace) return () => void 0;
7596
7797
  const now = trace.now ?? Date.now;
@@ -7650,12 +7851,12 @@ async function buildSpendAccountPayKind2(input) {
7650
7851
  }),
7651
7852
  tx.sharedObjectRef({
7652
7853
  objectId: input.poolObjectId,
7653
- initialSharedVersion: "1",
7854
+ initialSharedVersion: input.poolInitialSharedVersion?.trim() || "1",
7654
7855
  mutable: true
7655
7856
  }),
7656
7857
  tx.sharedObjectRef({
7657
7858
  objectId: input.grantObjectId,
7658
- initialSharedVersion: "1",
7859
+ initialSharedVersion: input.grantInitialSharedVersion?.trim() || "1",
7659
7860
  mutable: true
7660
7861
  }),
7661
7862
  tx.pure.u64(BigInt(input.policyId)),
@@ -7666,7 +7867,7 @@ async function buildSpendAccountPayKind2(input) {
7666
7867
  tx.pure.u64(BigInt(guarded.expiresAtMs)),
7667
7868
  tx.sharedObjectRef({
7668
7869
  objectId: SUI_CLOCK3,
7669
- initialSharedVersion: "1",
7870
+ initialSharedVersion: input.clockInitialSharedVersion?.trim() || "1",
7670
7871
  mutable: false
7671
7872
  })
7672
7873
  ]
@@ -7754,6 +7955,12 @@ async function prepareSpendAccountPayment(args) {
7754
7955
  ...guardedFields
7755
7956
  });
7756
7957
  }
7958
+ const readSharedObject = deps.readSharedObject ?? sharedObjectReaderForTests ?? chainSharedObjectReader(cfg, input.packageId);
7959
+ const versions = await resolveSharedObjectVersionsFromGetObject(readSharedObject, {
7960
+ poolObjectId: input.poolObjectId,
7961
+ grantObjectId: input.grantObjectId,
7962
+ clockObjectId: SUI_CLOCK3
7963
+ });
7757
7964
  if (isOpen) {
7758
7965
  return buildSpendAccountSettleOpenRecipientKind({
7759
7966
  packageId: input.packageId,
@@ -7761,8 +7968,9 @@ async function prepareSpendAccountPayment(args) {
7761
7968
  coinType: input.coinType,
7762
7969
  poolObjectId: input.poolObjectId,
7763
7970
  grantObjectId: input.grantObjectId,
7764
- poolInitialSharedVersion: "1",
7765
- grantInitialSharedVersion: "1",
7971
+ poolInitialSharedVersion: versions.poolInitialSharedVersion,
7972
+ grantInitialSharedVersion: versions.grantInitialSharedVersion,
7973
+ clockInitialSharedVersion: versions.clockInitialSharedVersion,
7766
7974
  recipient: input.recipient,
7767
7975
  targetHash: input.targetHash,
7768
7976
  amount: input.amount,
@@ -7782,6 +7990,9 @@ async function prepareSpendAccountPayment(args) {
7782
7990
  amount: input.amount,
7783
7991
  paymentIdHash: input.paymentIdHash,
7784
7992
  termsHash: input.termsHash,
7993
+ poolInitialSharedVersion: versions.poolInitialSharedVersion,
7994
+ grantInitialSharedVersion: versions.grantInitialSharedVersion,
7995
+ clockInitialSharedVersion: versions.clockInitialSharedVersion,
7785
7996
  ...guardedFields
7786
7997
  });
7787
7998
  };
@@ -7802,7 +8013,8 @@ async function prepareSpendAccountPayment(args) {
7802
8013
  authority: session.snapshot,
7803
8014
  assertLive: session.assertLive
7804
8015
  };
7805
- const resolveExclusionRegistry2 = deps.resolveExclusionRegistry ?? (cfg.exclusionDirectoryObjectId ? (() => {
8016
+ const directorySwitch = cfg.exclusionDirectoryObjectId ? cfg.exclusionDirectoryObjectId : deps.resolveExclusionRegistry ? null : await publishedExclusionDirectoryId(cfg.gatewayUrl);
8017
+ const resolveExclusionRegistry2 = deps.resolveExclusionRegistry ?? (directorySwitch ? (() => {
7806
8018
  const reader = createGrpcExclusionRegistryReader(rpcClient(cfg));
7807
8019
  return async ({ directoryId, recipient }) => resolveExclusionRegistry({
7808
8020
  directoryObjectId: directoryId,
@@ -9022,10 +9234,15 @@ async function suipayLogin(args, cfg, deps = {}) {
9022
9234
  });
9023
9235
  }
9024
9236
  if (live.kind === "unknown") {
9025
- return error({
9026
- status: "failed",
9237
+ return text({
9238
+ status: "ok",
9239
+ reused: true,
9240
+ grantStatus: "unknown",
9027
9241
  code: "GRANT_STATUS_UNKNOWN",
9028
- detail: "Could not verify the existing grant. Credentials were not modified. Retry later; do not start a new consent (that would mint a second grant)."
9242
+ target: target ?? cfg.target,
9243
+ delegateAddress: identity.delegateAddress,
9244
+ gatewayUrl: endpoints.gatewayUrl,
9245
+ 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."
9029
9246
  });
9030
9247
  }
9031
9248
  }
@@ -9035,13 +9252,13 @@ async function suipayLogin(args, cfg, deps = {}) {
9035
9252
  identity,
9036
9253
  label,
9037
9254
  timeoutMs: LOGIN_BG_TIMEOUT_MS,
9038
- openBrowser: false,
9255
+ openBrowser: true,
9039
9256
  onUrl: deps.onUrl
9040
9257
  });
9041
9258
  const url = started.connectUrl;
9042
9259
  return text(
9043
9260
  [
9044
- `## ACTION REQUIRED: User must click this URL to sign in`,
9261
+ `## ACTION REQUIRED: The sign-in page was opened in the browser`,
9045
9262
  ``,
9046
9263
  `**Target:** ${target ?? cfg.target ?? "custom"} \xB7 ${endpoints.gatewayUrl}`,
9047
9264
  ``,
@@ -9053,11 +9270,13 @@ async function suipayLogin(args, cfg, deps = {}) {
9053
9270
  ``,
9054
9271
  `[Click here to open SuiPay sign-in](${url})`,
9055
9272
  ``,
9056
- `**IMPORTANT for the assistant**: do NOT summarize or omit the URL above.`,
9057
- `The user CANNOT proceed without seeing the exact URL. Surface it verbatim.`,
9273
+ `**IMPORTANT for the assistant**: the browser was already opened to this exact URL.`,
9274
+ `Do NOT navigate to /connect/agent yourself. That path without these query`,
9275
+ `parameters shows "Authorization link incomplete". If the page did not`,
9276
+ `appear, surface the URL above verbatim and do not shorten it.`,
9058
9277
  ``,
9059
- `1. Open the URL in any browser`,
9060
- `2. Connect the owner wallet and approve the spend-account grant (Access keys / policies)`,
9278
+ `1. Use the browser window that just opened, or open the URL above`,
9279
+ `2. Connect the owner wallet, choose or create a key, then Authorize on the review step`,
9061
9280
  `3. Once Connected appears, retry the original request \u2014 credentials at \`~/.suipay/credentials.json\` are picked up on the next tool call`,
9062
9281
  ``,
9063
9282
  `_The login link stays valid for 5 minutes. If it expires, call \`suipay_login\` again._`,
@@ -83,11 +83,10 @@ interface McpConfig {
83
83
  minMaxPerPayment: bigint;
84
84
  label: string;
85
85
  /**
86
- * The canonical `ExclusionDirectory` object id (the same
87
- * `SUIPAY_EXCLUSION_DIRECTORY_OBJECT_ID` the gateway reads). Present enables the
88
- * non-custodial pay path to resolve the per-recipient `RecipientRegistry` and
89
- * build a GUARDED delegate settle for a guarded offer. `null` when
90
- * unset — a guarded offer then FAILS CLOSED (no unguarded settle).
86
+ * Optional override of the gateway's published `ExclusionDirectory` id.
87
+ * When null, pay reads `GET /v1/delegate/deployment` and turns the resolver
88
+ * on only if that response names one. The id here does not choose the
89
+ * directory a settle uses — the offer's `registryIdentity` does.
91
90
  */
92
91
  exclusionDirectoryObjectId: string | null;
93
92
  }
@@ -304,6 +303,9 @@ type PayResult = {
304
303
  * See docs/design/mcp-non-custodial-signing.md and ADR-0013 §3.
305
304
  */
306
305
 
306
+ type SharedObjectReader = (objectId: string) => Promise<{
307
+ owner?: unknown;
308
+ } | null | undefined>;
307
309
  /**
308
310
  * Read/prepare-only session for the non-custodial MCP pay tool. It carries the
309
311
  * immutable grant snapshot and an optional prepare-time live gate — and no key,
@@ -355,6 +357,11 @@ interface SpendAccountPayDeps {
355
357
  * configured `ExclusionDirectory` id when present, else fails closed.
356
358
  */
357
359
  resolveExclusionRegistry?: PayOperationDeps['resolveExclusionRegistry'];
360
+ /**
361
+ * Read a shared object's owner so prepare binds the real initial version.
362
+ * Missing owner fails the prepare; version 1 is not a substitute.
363
+ */
364
+ readSharedObject?: SharedObjectReader;
358
365
  /**
359
366
  * Live trace for this prepare. Purely observational: emission records what the
360
367
  * canonical read decisions already returned — an event is written after a check
@@ -437,6 +444,9 @@ declare function buildSpendAccountPayKind(input: {
437
444
  amount: string;
438
445
  paymentIdHash: Uint8Array;
439
446
  termsHash: Uint8Array;
447
+ poolInitialSharedVersion?: string;
448
+ grantInitialSharedVersion?: string;
449
+ clockInitialSharedVersion?: string;
440
450
  } & GuardedSettleFields): Promise<{
441
451
  kindBytes: string;
442
452
  }>;
package/dist/http.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { r as handleSuipayMcpHttpRequest } from './http-DggnqM91.js';
1
+ export { r as handleSuipayMcpHttpRequest } from './http-RP-Xlg60.js';
2
2
  import '@cmdoss/suipay-core/policy/grant-target';
3
3
  import '@cmdoss/suipay-sdk/mcp/access-context';
4
4
  import '@cmdoss/suipay-sdk/buyer/settlement-dto';
package/dist/http.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  handleSuipayMcpHttpRequest
3
- } from "./chunk-6ZSOXII5.js";
3
+ } from "./chunk-NDJ7BFI4.js";
4
4
  export {
5
5
  handleSuipayMcpHttpRequest
6
6
  };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Wallet } from '@cmdoss/suipay-core/chain/types';
2
- import { S as SpendAccountSigningRequest, M as McpConfig, a as McpAuthContext, b as McpTraceHooks, c as SpendAccountPayDeps } from './http-DggnqM91.js';
3
- export { A as AgentHeldResolver, d as AgentHeldSettlement, e as MCP_TARGET_PRESETS, f as McpEnvelope, g as McpEnvelopeEntry, h as McpServerOptions, i as McpTarget, j as McpTraceContext, k as McpTraceRuntime, P as PACKAGE_NAME, l as PreparedPayment, m as SpendAccountPrepareResult, n as SpendAccountPrepareSession, T as TOOLS, o as buildSpendAccountPayKind, p as canonicalMcpRequestId, q as createServer, r as handleSuipayMcpHttpRequest, s as inspectMcpEnvelope, t as loadMcpConfig, u as parseMcpTargetFlag, v as prepareSpendAccountPayment, w as resetAgentHeldProcessLedger, x as resolveAgentHeldSettlement, y as resolveMcpTarget, z as sameMcpGateway, B as traceMcpHttpRequest, C as verifySpendAccountPayReconstructs } from './http-DggnqM91.js';
2
+ import { S as SpendAccountSigningRequest, M as McpConfig, a as McpAuthContext, b as McpTraceHooks, c as SpendAccountPayDeps } from './http-RP-Xlg60.js';
3
+ export { A as AgentHeldResolver, d as AgentHeldSettlement, e as MCP_TARGET_PRESETS, f as McpEnvelope, g as McpEnvelopeEntry, h as McpServerOptions, i as McpTarget, j as McpTraceContext, k as McpTraceRuntime, P as PACKAGE_NAME, l as PreparedPayment, m as SpendAccountPrepareResult, n as SpendAccountPrepareSession, T as TOOLS, o as buildSpendAccountPayKind, p as canonicalMcpRequestId, q as createServer, r as handleSuipayMcpHttpRequest, s as inspectMcpEnvelope, t as loadMcpConfig, u as parseMcpTargetFlag, v as prepareSpendAccountPayment, w as resetAgentHeldProcessLedger, x as resolveAgentHeldSettlement, y as resolveMcpTarget, z as sameMcpGateway, B as traceMcpHttpRequest, C as verifySpendAccountPayReconstructs } from './http-RP-Xlg60.js';
4
4
  import { GetObjectJson, createReader } from '@cmdoss/suipay-sdk/agent-sdk/reads';
5
5
  import { DelegatedKeySigner, DelegatedPayerSession, DelegatedPayerConfig, DelegatedPayerDeps, SpendIntent, DelegatedPayResult } from '@cmdoss/suipay-sdk/delegated-signer/payer';
6
6
  import { PaidHttpRequest } from '@cmdoss/suipay-sdk/protocol/paid-http-request';
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  loadBootProfile,
3
3
  main
4
- } from "./chunk-KR6WRUDY.js";
4
+ } from "./chunk-LQOZBRH6.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-6ZSOXII5.js";
29
+ } from "./chunk-NDJ7BFI4.js";
30
30
  export {
31
31
  MCP_TARGET_PRESETS,
32
32
  PACKAGE_NAME,
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@cmdoss/suipay-mcp",
3
- "version": "0.2.2-dev.7",
3
+ "version": "0.2.2-dev.9",
4
4
  "type": "module",
5
- "description": "SuiPay MCP \u2014 pay MPP-gated APIs from a spend-account grant with an agent-held delegate key.",
5
+ "description": "SuiPay MCP — pay MPP-gated APIs from a spend-account grant with an agent-held delegate key.",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
8
8
  "exports": {
@@ -20,16 +20,11 @@
20
20
  ],
21
21
  "publishConfig": {
22
22
  "access": "public",
23
- "provenance": true
23
+ "provenance": false
24
24
  },
25
25
  "engines": {
26
26
  "node": ">=20"
27
27
  },
28
- "scripts": {
29
- "build": "tsup",
30
- "test": "vitest run",
31
- "typecheck": "tsc -p tsconfig.json --noEmit"
32
- },
33
28
  "dependencies": {
34
29
  "@modelcontextprotocol/sdk": "^1.29.0",
35
30
  "@mysten/sui": "2.24.0",
@@ -38,11 +33,16 @@
38
33
  "open": "^11.0.0"
39
34
  },
40
35
  "devDependencies": {
41
- "@cmdoss/suipay-core": "workspace:*",
42
- "@cmdoss/suipay-sdk": "workspace:*",
43
- "@types/node": "catalog:",
44
- "tsup": "catalog:",
45
- "typescript": "catalog:",
46
- "vitest": "catalog:"
36
+ "@types/node": "^26",
37
+ "tsup": "^8.0.0",
38
+ "typescript": "^6.0.3",
39
+ "vitest": "^4.1.10",
40
+ "@cmdoss/suipay-core": "0.1.0",
41
+ "@cmdoss/suipay-sdk": "0.1.0"
42
+ },
43
+ "scripts": {
44
+ "build": "tsup",
45
+ "test": "vitest run",
46
+ "typecheck": "tsc -p tsconfig.json --noEmit"
47
47
  }
48
- }
48
+ }