@blanketbyte/wise-mcp 1.0.6 → 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) => ({
@@ -272,6 +272,8 @@ function registerWriteTools(server, wise, { sessionKey, connectionId }, safe) {
272
272
  `${recipient.name} (${recipient.account ?? recipient.type}) receives ${money(quote.target.amount, recipient.currency)}` +
273
273
  (quote.fee ? `, fee ${money(quote.fee.amount, sourceCurrency)}` : "") +
274
274
  (sourceCurrency !== recipient.currency ? `, rate ${quote.rate}` : "") +
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" : "") +
275
277
  (details.reference ? `, reference "${details.reference}"` : "") +
276
278
  (quote.delivery ? `, ${stripHtml(quote.delivery)}` : "");
277
279
  const { code, expiresAt } = storePending(sessionKey, {
@@ -281,7 +283,7 @@ function registerWriteTools(server, wise, { sessionKey, connectionId }, safe) {
281
283
  targetAccount: recipient.id,
282
284
  customerTransactionId: randomUUID(),
283
285
  details,
284
- expected: { amount: quote.source.amount, currency: sourceCurrency },
286
+ expected: { amount: quote.target.amount, currency: recipient.currency, options: quote.pay_in_options.map((o) => Number(o.target_amount)) },
285
287
  }, summary);
286
288
  await log("transfer_prepared", { code, summary });
287
289
  return ok({
@@ -290,7 +292,7 @@ function registerWriteTools(server, wise, { sessionKey, connectionId }, safe) {
290
292
  quote,
291
293
  balance_check: available >= quote.source.amount
292
294
  ? `OK: ${money(available, sourceCurrency)} available`
293
- : `Not enough ${sourceCurrency} balance (${money(available, sourceCurrency)}); the user can still pay another way in the Wise app`,
295
+ : `Not enough ${sourceCurrency} balance (${money(available, sourceCurrency)}): the price assumes paying from the Wise balance, so tell the user to top it up first (paying another way in the app can cost more)`,
294
296
  confirmation_code: code,
295
297
  expires_at: expiresAt.toISOString(),
296
298
  next_step: "Show the summary. Only call confirm_action if the user explicitly approves. Afterwards they pay it in the Wise app.",
@@ -327,10 +329,12 @@ function registerWriteTools(server, wise, { sessionKey, connectionId }, safe) {
327
329
  try {
328
330
  if (a.kind === "transfer") {
329
331
  const t = await wise.createTransfer({ targetAccount: a.targetAccount, quoteId: a.quoteId, customerTransactionId: a.customerTransactionId, details: a.details });
330
- // Wise has been seen creating a transfer for a different amount than its own quote. Never leave the
331
- // user with a transfer they didn't approve: it's still unpaid, so cancel it and say what happened.
332
- const got = { amount: Number(t.source.amount), currency: String(t.source.currency ?? "").toUpperCase() };
333
- if (Math.abs(got.amount - a.expected.amount) > 0.005 || got.currency !== a.expected.currency.toUpperCase()) {
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.
335
+ const got = { amount: Number(t.target.amount), currency: String(t.target.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))) {
334
338
  const cancelled = await wise.cancelTransfer(t.id).then((c) => c.status, (err) => `cancel failed: ${err instanceof Error ? err.message : err}`);
335
339
  await log("transfer_amount_mismatch", {
336
340
  code: confirmation_code,
@@ -341,12 +345,12 @@ function registerWriteTools(server, wise, { sessionKey, connectionId }, safe) {
341
345
  got,
342
346
  cancelled,
343
347
  });
344
- return fail(`Wise created transfer ${t.id} for ${money(got.amount, got.currency)} instead of the approved ` +
345
- `${money(a.expected.amount, a.expected.currency)} (quote ${a.quoteId}). ` +
348
+ return fail(`Wise created transfer ${t.id} where the recipient gets ${money(got.amount, got.currency)} instead of the approved ` +
349
+ `${money(a.expected.amount, a.expected.currency)} (quote ${a.quoteId}), so the price differs from the summary. ` +
346
350
  (cancelled === "cancelled"
347
351
  ? "It was cancelled automatically, so nothing will be paid. "
348
352
  : `Automatic cancellation didn't succeed (${cancelled}): tell the user to cancel it in the Wise app and NOT pay it. `) +
349
- "Don't retry; tell the user this is a Wise-side mismatch.");
353
+ "Don't retry; tell the user.");
350
354
  }
351
355
  await log("transfer_created", { code: confirmation_code, summary: p.summary, transfer_id: t.id, status: t.status });
352
356
  return ok({
@@ -354,7 +358,13 @@ function registerWriteTools(server, wise, { sessionKey, connectionId }, safe) {
354
358
  transfer_id: t.id,
355
359
  wise_status: t.status,
356
360
  summary: p.summary,
357
- 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.",
358
368
  });
359
369
  }
360
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,6 +228,8 @@ export class WiseService {
220
228
  sourceAmount: q.sourceAmount ?? null,
221
229
  targetAmount: q.targetAmount ?? null,
222
230
  targetAccount: q.targetAccount,
231
+ // The user pays from their Wise balance in the app: list (and show) that price first.
232
+ preferredPayIn: "BALANCE",
223
233
  });
224
234
  return mapQuote(raw);
225
235
  }
@@ -293,6 +303,8 @@ function mapActivity(a) {
293
303
  function matches(a, search) {
294
304
  return [a.title, a.description, a.type, a.secondary_text, a.amount_text].some((v) => v?.toLowerCase().includes(search));
295
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.";
296
308
  function mapTransfer(t) {
297
309
  return {
298
310
  id: t.id,
@@ -319,7 +331,7 @@ function mapRecipient(r) {
319
331
  }
320
332
  function mapQuote(q) {
321
333
  const options = (q.paymentOptions ?? []).filter((o) => !o.disabled && (!q.payOut || o.payOut === q.payOut));
322
- // You pay in the Wise app, usually from your balance: show that option's price.
334
+ // The user pays in the Wise app, usually from the balance: show that price (createQuote lists BALANCE first).
323
335
  const o = options.find((x) => x.payIn === "BALANCE") ?? options[0];
324
336
  return {
325
337
  id: String(q.id),
@@ -334,6 +346,8 @@ function mapQuote(q) {
334
346
  estimated_delivery: o?.estimatedDelivery,
335
347
  delivery: o?.formattedEstimatedDelivery,
336
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 })),
337
351
  };
338
352
  }
339
353
  function mapRequirementField(g) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blanketbyte/wise-mcp",
3
- "version": "1.0.6",
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": [