@mysten-incubation/suipay-mcp 0.3.0-dev.5 → 0.3.0-dev.7

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.5` in MCP config for the `latest` dist-tag.
8
+ Pin `@mysten-incubation/suipay-mcp@0.3.0-dev.7` 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.5
16
+ claude mcp add --scope user suipay -- npx -y @mysten-incubation/suipay-mcp@0.3.0-dev.7
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.5
158
+ npx @mysten-incubation/suipay-mcp@0.3.0-dev.7
159
159
 
160
160
  # Laptop stack
161
- npx @mysten-incubation/suipay-mcp@0.3.0-dev.5 --local
161
+ npx @mysten-incubation/suipay-mcp@0.3.0-dev.7 --local
162
162
  ```
163
163
 
164
164
  `SUIPAY_MCP_TARGET=local|dev` selects the same preset. Explicit
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  main
4
- } from "../chunk-IVHVB3IY.js";
4
+ } from "../chunk-X23ENZQ6.js";
5
5
  import {
6
6
  logError
7
- } from "../chunk-7S4TYZJM.js";
7
+ } from "../chunk-VKRYN5FA.js";
8
8
 
9
9
  // src/bin/suipay.ts
10
10
  main().catch((err) => {
@@ -957,6 +957,70 @@ function createSpendAccountChain(options) {
957
957
  });
958
958
  return kindBytes(tx);
959
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
+ },
960
1024
  async buildPausePolicyKind(input) {
961
1025
  const tx = new Transaction2();
962
1026
  tx.setSender(input.owner);
@@ -13894,7 +13958,7 @@ function grantObjectIdFromBootstrap(grants, nowMs) {
13894
13958
  }
13895
13959
  if (live.length === 1) return { kind: "live", grantObjectId: live[0] };
13896
13960
  if (live.length > 1) return { kind: "ambiguous" };
13897
- if (named > 0) return { kind: "absent" };
13961
+ if (named > 0) return { kind: "expired" };
13898
13962
  if (grants.length > 0) return { kind: "unknown", detail: "delegate grants list was not usable" };
13899
13963
  return { kind: "absent" };
13900
13964
  }
@@ -14341,7 +14405,8 @@ async function suipayLogin(args, cfg, deps = {}) {
14341
14405
  target: target ?? cfg.target,
14342
14406
  delegateAddress: identity.delegateAddress,
14343
14407
  gatewayUrl: endpoints.gatewayUrl,
14344
- 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."
14408
+ nextAction: "poll suipay_login_status to verify authorization before retrying payment",
14409
+ 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."
14345
14410
  });
14346
14411
  }
14347
14412
  }
@@ -14376,7 +14441,7 @@ async function suipayLogin(args, cfg, deps = {}) {
14376
14441
  ``,
14377
14442
  `1. Use the browser window that just opened, or open the URL above`,
14378
14443
  `2. Connect the owner wallet, create a key, then Authorize on the review step`,
14379
- `3. Once Connected appears, retry the original request \u2014 credentials at \`~/.suipay/credentials.json\` are picked up on the next tool call`,
14444
+ `3. Poll \`suipay_login_status\` to confirm authorization \u2014 credentials at \`~/.suipay/credentials.json\` are active once approved`,
14380
14445
  ``,
14381
14446
  `_The login link stays valid for 5 minutes. If it expires, call \`suipay_login\` again._`,
14382
14447
  ``,
@@ -14386,11 +14451,113 @@ async function suipayLogin(args, cfg, deps = {}) {
14386
14451
  delegateAddress: identity.delegateAddress,
14387
14452
  connectUrl: url,
14388
14453
  consoleUrl: endpoints.consoleUrl,
14389
- gatewayUrl: endpoints.gatewayUrl
14454
+ gatewayUrl: endpoints.gatewayUrl,
14455
+ nextAction: "poll suipay_login_status to verify authorization before retrying payment"
14390
14456
  })
14391
14457
  ].join("\n")
14392
14458
  );
14393
14459
  }
14460
+ function defaultKeyName(delegateAddress) {
14461
+ const clean = delegateAddress.trim().toLowerCase();
14462
+ const hex = clean.startsWith("0x") ? clean.slice(2) : clean;
14463
+ const prefix = hex.slice(0, 8).padEnd(8, "0");
14464
+ return `agent-${prefix}`;
14465
+ }
14466
+ function safeRetryDetail(rawDetail) {
14467
+ if (!rawDetail) {
14468
+ return "Grant status could not be verified on chain. Retry shortly.";
14469
+ }
14470
+ let stripped = rawDetail.replace(/https?:\/\/[^\s"'<>]+/gi, "[redacted]");
14471
+ if (stripped.includes("ECONNREFUSED") || stripped.includes("ETIMEDOUT") || stripped.includes("fetch failed")) {
14472
+ return "Network connection to gateway or chain node failed. Retry shortly.";
14473
+ }
14474
+ return stripped;
14475
+ }
14476
+ var loginStatusCache = /* @__PURE__ */ new Map();
14477
+ var DEFAULT_LOGIN_STATUS_RETRY_AFTER_MS = 2e3;
14478
+ var DEFAULT_LOGIN_STATUS_CACHE_TTL_MS = 1500;
14479
+ async function suipayLoginStatus(_args = {}, cfg, deps = {}) {
14480
+ if (deps.auth) {
14481
+ return mcpFailure(
14482
+ "request.local_only",
14483
+ "suipay_login_status is for local stdio only; hosted /mcp never holds a spendable key"
14484
+ );
14485
+ }
14486
+ const loadKeypair = deps.loadLocalKeypair ?? loadLocalKeypair;
14487
+ const keypair = loadKeypair();
14488
+ if (!keypair) {
14489
+ return text({
14490
+ status: "not_started",
14491
+ nextAction: "run suipay_login"
14492
+ });
14493
+ }
14494
+ const delegateAddress = keypair.toSuiAddress();
14495
+ const stored = loadLocalSigningKeyRecord();
14496
+ const gatewayUrl = stored?.gatewayUrl ?? cfg.gatewayUrl;
14497
+ const cacheKey = `${gatewayUrl}:${delegateAddress}`;
14498
+ const now = Date.now();
14499
+ const cacheTtlMs = deps.cacheTtlMs ?? DEFAULT_LOGIN_STATUS_CACHE_TTL_MS;
14500
+ if (!deps.bypassCache) {
14501
+ const cached = loginStatusCache.get(cacheKey);
14502
+ if (cached && now - cached.timestamp < cacheTtlMs) {
14503
+ return cached.result;
14504
+ }
14505
+ }
14506
+ const checkFn = deps.checkLiveDelegateGrant ?? checkLiveDelegateGrant;
14507
+ let live;
14508
+ try {
14509
+ live = await checkFn({
14510
+ gatewayUrl,
14511
+ delegateAddress,
14512
+ signPersonalMessage: async (bytes) => {
14513
+ const { signature } = await keypair.signPersonalMessage(bytes);
14514
+ return signature;
14515
+ },
14516
+ ...deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}
14517
+ });
14518
+ } catch (err) {
14519
+ const rawMessage = err instanceof Error ? err.message : String(err);
14520
+ live = {
14521
+ kind: "unknown",
14522
+ detail: rawMessage
14523
+ };
14524
+ }
14525
+ let result;
14526
+ if (live.kind === "live") {
14527
+ result = text({
14528
+ status: "connected",
14529
+ grantObjectId: live.grantObjectId,
14530
+ delegateAddress
14531
+ });
14532
+ } else if (live.kind === "expired") {
14533
+ result = text({
14534
+ status: "expired",
14535
+ delegateAddress,
14536
+ detail: "Grant has expired. Run suipay_login to reconnect.",
14537
+ nextAction: "run suipay_login"
14538
+ });
14539
+ } else if (live.kind === "absent") {
14540
+ result = text({
14541
+ status: "pending",
14542
+ delegateAddress,
14543
+ defaultKeyName: defaultKeyName(delegateAddress)
14544
+ });
14545
+ } else if (live.kind === "ambiguous") {
14546
+ result = text({
14547
+ status: "ambiguous",
14548
+ delegateAddress,
14549
+ detail: "Multiple live grants found for this delegate key. Revoke the duplicate grants and reconnect."
14550
+ });
14551
+ } else {
14552
+ result = text({
14553
+ status: "retry",
14554
+ retryAfterMs: DEFAULT_LOGIN_STATUS_RETRY_AFTER_MS,
14555
+ detail: safeRetryDetail(live.detail)
14556
+ });
14557
+ }
14558
+ loginStatusCache.set(cacheKey, { timestamp: Date.now(), result });
14559
+ return result;
14560
+ }
14394
14561
  async function logout(args, _cfg, deps = {}) {
14395
14562
  if (args.force) {
14396
14563
  clearCreds();
@@ -14494,6 +14661,14 @@ var TOOLS = [
14494
14661
  }
14495
14662
  }
14496
14663
  },
14664
+ {
14665
+ name: "suipay_login_status",
14666
+ 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.`,
14667
+ inputSchema: {
14668
+ type: "object",
14669
+ properties: {}
14670
+ }
14671
+ },
14497
14672
  {
14498
14673
  name: "suipay_logout",
14499
14674
  description: "Remove the local delegate-key credentials file. Missing file is success. Force clears a corrupt file without printing the seed.",
@@ -14641,6 +14816,14 @@ function createServer2(cfg, auth, trace, options = {}) {
14641
14816
  }
14642
14817
  );
14643
14818
  }
14819
+ case "suipay_login_status":
14820
+ if (auth) {
14821
+ return mcpFailure(
14822
+ "request.local_only",
14823
+ "suipay_login_status is for local stdio only; hosted /mcp never holds a spendable key"
14824
+ );
14825
+ }
14826
+ return await suipayLoginStatus(args, cfg, { auth });
14644
14827
  case "suipay_logout":
14645
14828
  if (auth) {
14646
14829
  return mcpFailure(
@@ -5,7 +5,7 @@ import {
5
5
  loadMcpConfig,
6
6
  loadPaymentProfile,
7
7
  logError
8
- } from "./chunk-7S4TYZJM.js";
8
+ } from "./chunk-VKRYN5FA.js";
9
9
 
10
10
  // src/index.ts
11
11
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -4971,6 +4971,13 @@ declare const TOOLS: readonly [{
4971
4971
  };
4972
4972
  };
4973
4973
  };
4974
+ }, {
4975
+ readonly name: "suipay_login_status";
4976
+ readonly description: string;
4977
+ readonly inputSchema: {
4978
+ readonly type: "object";
4979
+ readonly properties: {};
4980
+ };
4974
4981
  }, {
4975
4982
  readonly name: "suipay_logout";
4976
4983
  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-Du_v0e8E.js';
1
+ export { I as handleSuipayMcpHttpRequest } from './http-OHkrRnYJ.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-7S4TYZJM.js";
3
+ } from "./chunk-VKRYN5FA.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-Du_v0e8E.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-Du_v0e8E.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-OHkrRnYJ.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-OHkrRnYJ.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-IVHVB3IY.js";
4
+ } from "./chunk-X23ENZQ6.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-7S4TYZJM.js";
29
+ } from "./chunk-VKRYN5FA.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.5",
3
+ "version": "0.3.0-dev.7",
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": {