@cmdoss/suipay-mcp 0.2.2-dev.2 → 0.2.2-dev.3

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
@@ -51,7 +51,7 @@ missing, expired, or revoked.
51
51
  | Tool | Purpose |
52
52
  | --- | --- |
53
53
  | `discover` | Search live gateway resources. After login: `name`, `path`, `url`, `targetHash`, catalog price, and `affordable` (can this session cover one call — not authorization). `pay({ url })` still settles |
54
- | `pay` | Fetch resource, prepare `spend_account` settlement, 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`); hosted `/mcp` refuses `file` |
54
+ | `pay` | Fetch resource, prepare `spend_account` settlement, settle when this process holds the grant key |
55
55
  | `receipts` | List settlement receipts, optionally by challenge ID |
56
56
  | `access_context` | Report current session / delegate / policy state (no secret material) |
57
57
  | `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 |
@@ -76,18 +76,13 @@ json: {"prompt":"a cat"}
76
76
  maxAmount: 50000
77
77
  ```
78
78
 
79
- To store local bytes (Walrus), do not paste base64. Name the file and field:
80
-
81
- ```
82
- url: https://gateway.example/v1/storage/store
83
- method: POST
84
- json: {"contentType":"text/plain"}
85
- file: /absolute/path/to/note.txt
86
- fileField: contentBase64
87
- ```
88
-
89
- `pay` uses SDK `createPayer` internally. A delivered image is returned as an MCP
90
- image block, not as a base64 wall in the JSON summary.
79
+ `pay` uses SDK `createPayer` internally. A binary body is written once under
80
+ `~/.suipay/deliveries/` and named first in the JSON as `savedTo` (absolute
81
+ path) and `savedToUrl` (`file://`). Any MIME — image, audio, pdf,
82
+ octet-stream. JSON stays compact (no base64 wall). An MCP `image` block is an
83
+ extra display hint when the type is `image/*`; hosts such as Claude Code's
84
+ TUI may not render it. Always quote `savedTo` to the user. Do not call `pay`
85
+ again to "save" a delivery that already settled.
91
86
 
92
87
  ## Targets
93
88
 
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  main
4
- } from "../chunk-AFEXT5X4.js";
5
- import "../chunk-62LFIYB7.js";
4
+ } from "../chunk-C2IYSV43.js";
5
+ import "../chunk-SRMAH6MK.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-62LFIYB7.js";
7
+ } from "./chunk-SRMAH6MK.js";
8
8
 
9
9
  // src/index.ts
10
10
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -6872,6 +6872,52 @@ function joinDiscoverCatalog(opts) {
6872
6872
  return scoped ? { accessMode: "scoped", count: services.length, services, allowedTargetHashes } : { accessMode: "open", count: services.length, services };
6873
6873
  }
6874
6874
 
6875
+ // src/tool-result.ts
6876
+ import { pathToFileURL } from "url";
6877
+
6878
+ // src/delivery-file.ts
6879
+ import { mkdirSync as mkdirSync2, writeFileSync, chmodSync as chmodSync2 } from "fs";
6880
+ import { dirname as dirname2, join as join3 } from "path";
6881
+ function mcpDeliveriesDir() {
6882
+ return join3(dirname2(mcpCredentialsPath()), "deliveries");
6883
+ }
6884
+ var EXT = {
6885
+ "image/png": ".png",
6886
+ "image/jpeg": ".jpg",
6887
+ "image/jpg": ".jpg",
6888
+ "image/webp": ".webp",
6889
+ "image/gif": ".gif",
6890
+ "image/svg+xml": ".svg",
6891
+ "audio/mpeg": ".mp3",
6892
+ "audio/wav": ".wav",
6893
+ "audio/ogg": ".ogg",
6894
+ "video/mp4": ".mp4",
6895
+ "application/pdf": ".pdf",
6896
+ "application/json": ".json",
6897
+ "text/plain": ".txt"
6898
+ };
6899
+ function extensionFor(contentType) {
6900
+ const type = contentType.split(";")[0]?.trim().toLowerCase() ?? "";
6901
+ return EXT[type] ?? ".bin";
6902
+ }
6903
+ function fileStem(digest, challengeId) {
6904
+ const raw = (digest ?? challengeId ?? `delivery-${Date.now()}`).trim();
6905
+ const safe = raw.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
6906
+ return (safe.slice(0, 80) || `delivery-${Date.now()}`).replace(/^\.+$/, "delivery");
6907
+ }
6908
+ function saveDeliveredBody(input) {
6909
+ if (input.bytes.byteLength === 0) return void 0;
6910
+ const dir = mcpDeliveriesDir();
6911
+ mkdirSync2(dir, { recursive: true, mode: 448 });
6912
+ const path = join3(
6913
+ dir,
6914
+ `${fileStem(input.digest, input.challengeId)}${extensionFor(input.contentType)}`
6915
+ );
6916
+ writeFileSync(path, input.bytes, { mode: 384 });
6917
+ chmodSync2(path, 384);
6918
+ return path;
6919
+ }
6920
+
6875
6921
  // src/tool-result.ts
6876
6922
  var MAX_MCP_IMAGE_BYTES = 5 * 1024 * 1024;
6877
6923
  function text(value) {
@@ -6893,6 +6939,11 @@ function closedError(code, detail) {
6893
6939
  function closedPayError(code, detail) {
6894
6940
  return error({ status: "failed", paid: false, code, detail });
6895
6941
  }
6942
+ function receiptChallengeId(receipt) {
6943
+ if (!receipt || typeof receipt !== "object") return void 0;
6944
+ const id = receipt.challengeId;
6945
+ return typeof id === "string" && id.length > 0 ? id : void 0;
6946
+ }
6896
6947
  function paidResult(result) {
6897
6948
  if (!isBinaryBody(result.body)) return text(result);
6898
6949
  const body = result.body;
@@ -6901,8 +6952,28 @@ function paidResult(result) {
6901
6952
  contentType: body.contentType,
6902
6953
  byteLength: body.byteLength
6903
6954
  };
6955
+ let savedTo;
6956
+ try {
6957
+ savedTo = saveDeliveredBody({
6958
+ bytes: body.bytes,
6959
+ contentType: body.contentType,
6960
+ digest: result.txDigest,
6961
+ challengeId: receiptChallengeId(result.receipt)
6962
+ });
6963
+ } catch {
6964
+ savedTo = void 0;
6965
+ }
6966
+ const payload = savedTo ? {
6967
+ savedTo,
6968
+ savedToUrl: pathToFileURL(savedTo).href,
6969
+ ...result,
6970
+ body: shape
6971
+ } : { ...result, body: shape };
6904
6972
  const content = [
6905
- { type: "text", text: JSON.stringify({ ...result, body: shape }, null, 2) }
6973
+ {
6974
+ type: "text",
6975
+ text: JSON.stringify(payload, null, 2)
6976
+ }
6906
6977
  ];
6907
6978
  if (body.contentType.startsWith("image/") && body.bytes.byteLength > 0 && body.bytes.byteLength <= MAX_MCP_IMAGE_BYTES) {
6908
6979
  content.push({
@@ -6918,115 +6989,6 @@ function settlePayResult(result) {
6918
6989
  return error(result);
6919
6990
  }
6920
6991
 
6921
- // src/pay-file.ts
6922
- import { isAbsolute } from "path";
6923
- import { openSync as openSync2, closeSync as closeSync2, fstatSync, readFileSync as readFileSync2, constants } from "fs";
6924
- var MCP_MAX_FILE_FIELD_BYTES = 32 * 1024;
6925
- var FIELD = /^[A-Za-z][A-Za-z0-9_]{0,63}$/;
6926
- var FORBIDDEN = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
6927
- function fail2(code, detail) {
6928
- return { code, detail };
6929
- }
6930
- function parseField(value) {
6931
- if (typeof value !== "string" || !FIELD.test(value) || FORBIDDEN.has(value)) {
6932
- return fail2("INVALID_FILE_FIELD", "fileField must be a simple JSON key");
6933
- }
6934
- return value;
6935
- }
6936
- function parsePath(value) {
6937
- if (typeof value !== "string" || value.length === 0 || value.includes("\0")) {
6938
- return fail2("INVALID_FILE", "file must be a non-empty absolute path");
6939
- }
6940
- if (!isAbsolute(value) || value.includes("://")) {
6941
- return fail2("INVALID_FILE", "file must be an absolute filesystem path, not a URL");
6942
- }
6943
- return value;
6944
- }
6945
- function readRegularFile(path) {
6946
- let fd;
6947
- try {
6948
- const flags = constants.O_RDONLY | (constants.O_NOFOLLOW !== void 0 ? constants.O_NOFOLLOW : 0);
6949
- fd = openSync2(path, flags);
6950
- const st = fstatSync(fd);
6951
- if (!st.isFile()) {
6952
- return fail2("INVALID_FILE", "file must be a regular file");
6953
- }
6954
- if (st.size === 0) {
6955
- return fail2("EMPTY_FILE", "file is empty");
6956
- }
6957
- if (st.size > MCP_MAX_FILE_FIELD_BYTES) {
6958
- return fail2(
6959
- "FILE_TOO_LARGE",
6960
- `file exceeds ${MCP_MAX_FILE_FIELD_BYTES} decoded bytes`
6961
- );
6962
- }
6963
- const bytes = readFileSync2(fd);
6964
- if (bytes.byteLength === 0) {
6965
- return fail2("EMPTY_FILE", "file is empty");
6966
- }
6967
- if (bytes.byteLength > MCP_MAX_FILE_FIELD_BYTES) {
6968
- return fail2(
6969
- "FILE_TOO_LARGE",
6970
- `file exceeds ${MCP_MAX_FILE_FIELD_BYTES} decoded bytes`
6971
- );
6972
- }
6973
- return bytes;
6974
- } catch (err) {
6975
- const code = err.code;
6976
- if (code === "ENOENT") return fail2("FILE_NOT_FOUND", "file does not exist");
6977
- if (code === "ELOOP" || code === "EMLINK") {
6978
- return fail2("INVALID_FILE", "file must be a regular file");
6979
- }
6980
- return fail2("INVALID_FILE", "file could not be read");
6981
- } finally {
6982
- if (fd !== void 0) closeSync2(fd);
6983
- }
6984
- }
6985
- function toCanonicalBase64(bytes) {
6986
- return Buffer.from(bytes).toString("base64");
6987
- }
6988
- function jsonWithLocalFile(input) {
6989
- const hasFile = input.file !== void 0 && input.file !== null && input.file !== "";
6990
- const hasField = input.fileField !== void 0 && input.fileField !== null && input.fileField !== "";
6991
- if (!hasFile && !hasField) return { json: input.json };
6992
- if (hasFile !== hasField) {
6993
- return {
6994
- error: fail2(
6995
- "INVALID_FILE_ARGS",
6996
- "file and fileField are required together"
6997
- )
6998
- };
6999
- }
7000
- const method = input.method === void 0 || input.method === null || input.method === "" ? "GET" : String(input.method).trim().toUpperCase();
7001
- if (method !== "POST") {
7002
- return { error: fail2("INVALID_FILE_ARGS", "file is only valid on POST") };
7003
- }
7004
- const field = parseField(input.fileField);
7005
- if (typeof field !== "string") return { error: field };
7006
- const path = parsePath(input.file);
7007
- if (typeof path !== "string") return { error: path };
7008
- let json;
7009
- if (input.json === void 0) {
7010
- json = {};
7011
- } else if (typeof input.json === "object" && input.json !== null && !Array.isArray(input.json)) {
7012
- json = { ...input.json };
7013
- } else {
7014
- return { error: fail2("MALFORMED_REQUEST", "POST requires json object") };
7015
- }
7016
- if (Object.prototype.hasOwnProperty.call(json, field)) {
7017
- return {
7018
- error: fail2(
7019
- "FILE_FIELD_EXISTS",
7020
- "json already has fileField; refuse to overwrite"
7021
- )
7022
- };
7023
- }
7024
- const bytes = readRegularFile(path);
7025
- if (!(bytes instanceof Uint8Array)) return { error: bytes };
7026
- json[field] = toCanonicalBase64(bytes);
7027
- return { json };
7028
- }
7029
-
7030
6992
  // src/tools.ts
7031
6993
  function payerTrace(deps) {
7032
6994
  const hooks = deps.trace;
@@ -7105,27 +7067,15 @@ function intentViolation(intent, prepared) {
7105
7067
  }
7106
7068
  async function pay(args, cfg, deps = {}) {
7107
7069
  if (!args.url) return error("url is required");
7108
- const hasFileArg = args.file !== void 0 && args.file !== null && args.file !== "" || args.fileField !== void 0 && args.fileField !== null && args.fileField !== "";
7109
- if (hasFileArg && deps.auth) {
7110
- return closedPayError(
7111
- "FILE_STDIO_ONLY",
7112
- "file is stdio only; hosted /mcp does not read the buyer disk"
7113
- );
7114
- }
7115
7070
  let intent;
7116
7071
  try {
7117
7072
  intent = normalizePayIntent(args);
7118
7073
  } catch {
7119
7074
  return closedPayError("INVALID_INTENT", "payment intent is invalid");
7120
7075
  }
7121
- const attached = jsonWithLocalFile(args);
7122
- if ("error" in attached) {
7123
- return closedPayError(attached.error.code, attached.error.detail);
7124
- }
7125
- const paidArgs = { ...args, json: attached.json };
7126
7076
  let request;
7127
7077
  try {
7128
- request = normalizeMcpPaidHttpRequest(paidArgs);
7078
+ request = normalizeMcpPaidHttpRequest(args);
7129
7079
  } catch (err) {
7130
7080
  const raw = err instanceof Error ? err.message : "";
7131
7081
  const detail = /method/i.test(raw) ? "method must be GET or POST" : /json/i.test(raw) ? "POST requires json" : "request could not be normalized";
@@ -7647,7 +7597,7 @@ var TOOLS = [
7647
7597
  },
7648
7598
  {
7649
7599
  name: "pay",
7650
- description: "Pay a HTTP resource via delegate. On hosted /mcp the gateway PREPARES an unsigned transaction and returns it (prepare-only \u2014 it never holds the delegate key and cannot settle). Only a process holding the delegate key the grant names can complete settlement (e.g. local stdio MCP, or any self-hosted process with the matching key). Pass recipient and/or maxAmount to state who you intend to pay and what you will spend; both are checked before anything is signed or handed back. Local V1 allowance pay is retired.",
7600
+ description: "Pay a HTTP resource via delegate. On hosted /mcp the gateway PREPARES an unsigned transaction and returns it (prepare-only \u2014 it never holds the delegate key and cannot settle). Only a process holding the delegate key the grant names can complete settlement (e.g. local stdio MCP, or any self-hosted process with the matching key). Pass recipient and/or maxAmount to state who you intend to pay and what you will spend; both are checked before anything is signed or handed back. Local V1 allowance pay is retired. A paid binary body is written under ~/.suipay/deliveries/ and named as savedTo (absolute path) and savedToUrl (file://) at the top of the JSON. Always quote savedTo to the user so they can open the file. Do not assume the host rendered an image block. Never call pay again to display or save a delivery that already settled.",
7651
7601
  inputSchema: {
7652
7602
  type: "object",
7653
7603
  properties: {
@@ -7661,14 +7611,6 @@ var TOOLS = [
7661
7611
  type: "object",
7662
7612
  description: "JSON request body for a POST. Sent as application/json and bound to the payment."
7663
7613
  },
7664
- file: {
7665
- type: "string",
7666
- description: "Absolute local path whose bytes fill json[fileField] as canonical base64. Stdio only; requires fileField. GET must omit this."
7667
- },
7668
- fileField: {
7669
- type: "string",
7670
- description: "JSON key to set from file (e.g. contentBase64). Required with file. Must not already be present on json."
7671
- },
7672
7614
  recipient: {
7673
7615
  type: "string",
7674
7616
  description: "Sui address you expect to be paid. The payment is refused, unsigned, if the prepared transaction pays anyone else."
@@ -7807,8 +7749,6 @@ function createServer2(cfg, auth, trace, options = {}) {
7807
7749
  url: String(args.url ?? ""),
7808
7750
  ...args.method !== void 0 ? { method: args.method } : {},
7809
7751
  ...args.json !== void 0 ? { json: args.json } : {},
7810
- ...args.file !== void 0 ? { file: args.file } : {},
7811
- ...args.fileField !== void 0 ? { fileField: args.fileField } : {},
7812
7752
  ...args.recipient !== void 0 ? { recipient: args.recipient } : {},
7813
7753
  ...args.maxAmount !== void 0 ? { maxAmount: args.maxAmount } : {}
7814
7754
  },
@@ -282,14 +282,6 @@ declare const TOOLS: readonly [{
282
282
  readonly type: "object";
283
283
  readonly description: "JSON request body for a POST. Sent as application/json and bound to the payment.";
284
284
  };
285
- readonly file: {
286
- readonly type: "string";
287
- readonly description: "Absolute local path whose bytes fill json[fileField] as canonical base64. Stdio only; requires fileField. GET must omit this.";
288
- };
289
- readonly fileField: {
290
- readonly type: "string";
291
- readonly description: "JSON key to set from file (e.g. contentBase64). Required with file. Must not already be present on json.";
292
- };
293
285
  readonly recipient: {
294
286
  readonly type: "string";
295
287
  readonly description: "Sui address you expect to be paid. The payment is refused, unsigned, if the prepared transaction pays anyone else.";
package/dist/http.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { m as handleSuipayMcpHttpRequest } from './http-IivHoDeb.js';
1
+ export { m as handleSuipayMcpHttpRequest } from './http-B4YTVw0k.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-62LFIYB7.js";
3
+ } from "./chunk-SRMAH6MK.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 { M as McpConfig, a as McpAuthContext, b as McpTraceHooks } from './http-IivHoDeb.js';
3
- export { A as AgentHeldResolver, c as AgentHeldSettlement, d as MCP_TARGET_PRESETS, e as McpEnvelope, f as McpEnvelopeEntry, g as McpServerOptions, h as McpTarget, i as McpTraceContext, j as McpTraceRuntime, P as PACKAGE_NAME, T as TOOLS, k as canonicalMcpRequestId, l as createServer, m as handleSuipayMcpHttpRequest, n as inspectMcpEnvelope, o as loadMcpConfig, p as parseMcpTargetFlag, r as resetAgentHeldProcessLedger, q as resolveAgentHeldSettlement, s as resolveMcpTarget, t as sameMcpGateway, u as traceMcpHttpRequest } from './http-IivHoDeb.js';
2
+ import { M as McpConfig, a as McpAuthContext, b as McpTraceHooks } from './http-B4YTVw0k.js';
3
+ export { A as AgentHeldResolver, c as AgentHeldSettlement, d as MCP_TARGET_PRESETS, e as McpEnvelope, f as McpEnvelopeEntry, g as McpServerOptions, h as McpTarget, i as McpTraceContext, j as McpTraceRuntime, P as PACKAGE_NAME, T as TOOLS, k as canonicalMcpRequestId, l as createServer, m as handleSuipayMcpHttpRequest, n as inspectMcpEnvelope, o as loadMcpConfig, p as parseMcpTargetFlag, r as resetAgentHeldProcessLedger, q as resolveAgentHeldSettlement, s as resolveMcpTarget, t as sameMcpGateway, u as traceMcpHttpRequest } from './http-B4YTVw0k.js';
4
4
  import { GrantSnapshot } from '@cmdoss/suipay-core/policy/grant-target';
5
5
  export { resolveGrantTarget } from '@cmdoss/suipay-core/policy/grant-target';
6
6
  import { PaidHttpRequest } from '@cmdoss/suipay-sdk/protocol/paid-http-request';
@@ -340,11 +340,6 @@ interface StartedStdioLogin {
340
340
  */
341
341
  declare function startStdioLogin(opts: StdioLoginOptions): Promise<StartedStdioLogin>;
342
342
 
343
- /**
344
- * MCP tool payloads. Settlement honesty matches the playground stamp:
345
- * money that moved is never reported as a tool error (that invites a second pay).
346
- */
347
-
348
343
  type ToolResult = CallToolResult;
349
344
 
350
345
  interface ToolDeps {
@@ -382,8 +377,6 @@ declare function pay(args: {
382
377
  url: string;
383
378
  method?: unknown;
384
379
  json?: unknown;
385
- file?: unknown;
386
- fileField?: unknown;
387
380
  recipient?: unknown;
388
381
  maxAmount?: unknown;
389
382
  }, 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-AFEXT5X4.js";
4
+ } from "./chunk-C2IYSV43.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-62LFIYB7.js";
29
+ } from "./chunk-SRMAH6MK.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": "@cmdoss/suipay-mcp",
3
- "version": "0.2.2-dev.2",
3
+ "version": "0.2.2-dev.3",
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
  "main": "./dist/index.js",