@blanketbyte/wise-mcp 1.0.7 → 1.0.8

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/dist/tools.js CHANGED
@@ -3,7 +3,7 @@ import { appendFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { z } from "zod";
5
5
  import { config } from "./config.js";
6
- import { ACTIVITY_TYPES } from "./wise/service.js";
6
+ import { ACTIVITY_TYPES, UNPAID_PRICE_NOTE } from "./wise/service.js";
7
7
  import { WiseAccessRevokedError } from "./wise/client.js";
8
8
  import { storePending, takePending, discardPending } from "./pending.js";
9
9
  const ok = (data) => ({
@@ -273,6 +273,7 @@ function registerWriteTools(server, wise, { sessionKey, connectionId }, safe) {
273
273
  (quote.fee ? `, fee ${money(quote.fee.amount, sourceCurrency)}` : "") +
274
274
  (sourceCurrency !== recipient.currency ? `, rate ${quote.rate}` : "") +
275
275
  (quote.pay_in && quote.pay_in !== "BALANCE" ? `, priced for paying by ${quote.pay_in}` : "") +
276
+ (quote.pay_in === "BALANCE" ? ", when paid from your Wise balance" : "") +
276
277
  (details.reference ? `, reference "${details.reference}"` : "") +
277
278
  (quote.delivery ? `, ${stripHtml(quote.delivery)}` : "");
278
279
  const { code, expiresAt } = storePending(sessionKey, {
@@ -282,7 +283,7 @@ function registerWriteTools(server, wise, { sessionKey, connectionId }, safe) {
282
283
  targetAccount: recipient.id,
283
284
  customerTransactionId: randomUUID(),
284
285
  details,
285
- expected: { amount: quote.target.amount, currency: recipient.currency },
286
+ expected: { amount: quote.target.amount, currency: recipient.currency, options: quote.pay_in_options.map((o) => Number(o.target_amount)) },
286
287
  }, summary);
287
288
  await log("transfer_prepared", { code, summary });
288
289
  return ok({
@@ -328,11 +329,12 @@ function registerWriteTools(server, wise, { sessionKey, connectionId }, safe) {
328
329
  try {
329
330
  if (a.kind === "transfer") {
330
331
  const t = await wise.createTransfer({ targetAccount: a.targetAccount, quoteId: a.quoteId, customerTransactionId: a.customerTransactionId, details: a.details });
331
- // Never leave the user with a transfer they didn't approve (e.g. priced with another pay-in and a different
332
- // fee): it's still unpaid, so cancel it and say what happened. Wise's transfer source amount excludes the
333
- // fee, so compare what the recipient gets.
332
+ // Wise shows a new (unpaid) transfer at the bank-transfer price of its quote and re-prices it when it's paid
333
+ // from the balance. Anything that isn't one of this quote's prices is a transfer the user didn't approve:
334
+ // it's still unpaid, so cancel it and say what happened.
334
335
  const got = { amount: Number(t.target.amount), currency: String(t.target.currency ?? "").toUpperCase() };
335
- if (Math.abs(got.amount - a.expected.amount) > 0.005 || got.currency !== a.expected.currency.toUpperCase()) {
336
+ const close = (x) => Math.abs(got.amount - x) <= 0.005;
337
+ if (got.currency !== a.expected.currency.toUpperCase() || !(close(a.expected.amount) || a.expected.options.some(close))) {
336
338
  const cancelled = await wise.cancelTransfer(t.id).then((c) => c.status, (err) => `cancel failed: ${err instanceof Error ? err.message : err}`);
337
339
  await log("transfer_amount_mismatch", {
338
340
  code: confirmation_code,
@@ -356,7 +358,13 @@ function registerWriteTools(server, wise, { sessionKey, connectionId }, safe) {
356
358
  transfer_id: t.id,
357
359
  wise_status: t.status,
358
360
  summary: p.summary,
359
- next_step: "Open the Wise app (or wise.com): the transfer is waiting for you to pay. No money has moved yet.",
361
+ ...(close(a.expected.amount)
362
+ ? {}
363
+ : {
364
+ wise_shows_until_paid: `${money(got.amount, got.currency)} to the recipient (Wise's bank-transfer price)`,
365
+ note: `${UNPAID_PRICE_NOTE} Paid from the balance, the recipient gets the approved ${money(a.expected.amount, a.expected.currency)}.`,
366
+ }),
367
+ next_step: "Open the Wise app (or wise.com) and pay it from your Wise balance. No money has moved yet.",
360
368
  });
361
369
  }
362
370
  if (a.kind === "recipient") {
@@ -189,7 +189,15 @@ export class WiseService {
189
189
  async getTransfer(id) {
190
190
  const t = await this.client.get(`/v1/transfers/${id}`);
191
191
  const recipient = t.targetAccount ? await this.getRecipient(t.targetAccount).catch(() => undefined) : undefined;
192
- return { ...mapTransfer(t), recipient, rate: t.rate, quote_id: t.quoteUuid ?? t.quote, has_active_issues: t.hasActiveIssues, raw_status: t.status };
192
+ return {
193
+ ...mapTransfer(t),
194
+ recipient,
195
+ rate: t.rate,
196
+ quote_id: t.quoteUuid ?? t.quote,
197
+ has_active_issues: t.hasActiveIssues,
198
+ raw_status: t.status,
199
+ note: t.status === "incoming_payment_waiting" ? UNPAID_PRICE_NOTE : undefined,
200
+ };
193
201
  }
194
202
  // --- Recipients -----------------------------------------------------------------
195
203
  async listRecipients(profiles, currency) {
@@ -220,8 +228,7 @@ export class WiseService {
220
228
  sourceAmount: q.sourceAmount ?? null,
221
229
  targetAmount: q.targetAmount ?? null,
222
230
  targetAccount: q.targetAccount,
223
- // Without this Wise prices the transfer with its default pay-in (often a bank transfer, with a fee), even
224
- // though the user pays from their Wise balance in the app. BALANCE goes first and the transfer uses it.
231
+ // The user pays from their Wise balance in the app: list (and show) that price first.
225
232
  preferredPayIn: "BALANCE",
226
233
  });
227
234
  return mapQuote(raw);
@@ -296,6 +303,8 @@ function mapActivity(a) {
296
303
  function matches(a, search) {
297
304
  return [a.title, a.description, a.type, a.secondary_text, a.amount_text].some((v) => v?.toLowerCase().includes(search));
298
305
  }
306
+ /** Wise prices a transfer made via the API as a bank-transfer pay-in until it's paid (seen: 1 EUR → 0.05 EUR, fee 0.95). */
307
+ export const UNPAID_PRICE_NOTE = "Amounts of an unpaid transfer are Wise's bank-transfer price (with its fee). Paying it from the Wise balance in the app re-prices it, e.g. without that fee.";
299
308
  function mapTransfer(t) {
300
309
  return {
301
310
  id: t.id,
@@ -322,9 +331,8 @@ function mapRecipient(r) {
322
331
  }
323
332
  function mapQuote(q) {
324
333
  const options = (q.paymentOptions ?? []).filter((o) => !o.disabled && (!q.payOut || o.payOut === q.payOut));
325
- // Show the price of the pay-in Wise will use for the transfer: the quote's own payIn, otherwise the first option
326
- // (createQuote asks for BALANCE first). Showing another option's price once hid a 0.95 EUR fee.
327
- const o = options.find((x) => q.payIn && x.payIn === q.payIn) ?? options[0];
334
+ // The user pays in the Wise app, usually from the balance: show that price (createQuote lists BALANCE first).
335
+ const o = options.find((x) => x.payIn === "BALANCE") ?? options[0];
328
336
  return {
329
337
  id: String(q.id),
330
338
  source: { amount: o?.sourceAmount ?? q.sourceAmount, currency: q.sourceCurrency },
@@ -338,6 +346,8 @@ function mapQuote(q) {
338
346
  estimated_delivery: o?.estimatedDelivery,
339
347
  delivery: o?.formattedEstimatedDelivery,
340
348
  notices: (q.notices ?? []).map((n) => stripTags(n.text)).filter(Boolean),
349
+ // Price per pay-in method: an unpaid transfer shows the BANK_TRANSFER one until it's paid.
350
+ pay_in_options: options.map((x) => ({ pay_in: x.payIn, fee: x.fee?.total, target_amount: x.targetAmount })),
341
351
  };
342
352
  }
343
353
  function mapRequirementField(g) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blanketbyte/wise-mcp",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
4
4
  "description": "MCP server for Wise: read balances, card payments, transfers and recipients, and prepare transfers you approve twice. Runs locally (npx, for Claude, Codex, Cursor…) or as a remote connector for Claude and ChatGPT.",
5
5
  "mcpName": "io.github.BlanketByte/wise-mcp",
6
6
  "keywords": [