@blanketbyte/wise-mcp 0.0.0-stage → 1.0.0
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/LICENSE +21 -0
- package/README.md +354 -2
- package/dist/auth/pages.js +298 -0
- package/dist/auth/provider.js +222 -0
- package/dist/auth/store.js +105 -0
- package/dist/cli.js +35 -0
- package/dist/config.js +55 -0
- package/dist/crypto.js +37 -0
- package/dist/fsutil.js +16 -0
- package/dist/index.js +198 -0
- package/dist/meta.js +10 -0
- package/dist/pending.js +34 -0
- package/dist/stdio.js +50 -0
- package/dist/tools.js +358 -0
- package/dist/wise/client.js +116 -0
- package/dist/wise/connections.js +76 -0
- package/dist/wise/service.js +350 -0
- package/package.json +69 -4
- package/public/apple-touch-icon.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/icon.png +0 -0
- package/public/logo.png +0 -0
- package/public/logo.svg +7 -0
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import { WiseApiError } from "./client.js";
|
|
2
|
+
const PROFILE_TTL_MS = 5 * 60 * 1000;
|
|
3
|
+
/** Activity types that move money between the user's own balances. */
|
|
4
|
+
const NEUTRAL_TYPES = new Set(["INTERBALANCE", "AUTO_CONVERSION", "CARD_CHECK"]);
|
|
5
|
+
/** Friendly `type` filter names → Wise `monetaryResourceType`. */
|
|
6
|
+
export const ACTIVITY_TYPES = {
|
|
7
|
+
card: "CARD_TRANSACTION",
|
|
8
|
+
transfer: "TRANSFER",
|
|
9
|
+
conversion: "BALANCE_TRANSACTION",
|
|
10
|
+
direct_debit: "DIRECT_DEBIT_TRANSACTION",
|
|
11
|
+
cashback: "BALANCE_CASHBACK",
|
|
12
|
+
interest: "BALANCE_INTEREST",
|
|
13
|
+
};
|
|
14
|
+
export const stripTags = (s) => (s ?? "")
|
|
15
|
+
.replace(/<[^>]*>/g, "")
|
|
16
|
+
.replace(/&/g, "&")
|
|
17
|
+
.replace(/</g, "<")
|
|
18
|
+
.replace(/>/g, ">")
|
|
19
|
+
.replace(/"/g, '"')
|
|
20
|
+
.replace(/'/g, "'")
|
|
21
|
+
.replace(/\s+/g, " ")
|
|
22
|
+
.trim();
|
|
23
|
+
/** "1,234.56" / "1.234,56" / "2,000" / "24.73" → number */
|
|
24
|
+
export function parseNumber(raw) {
|
|
25
|
+
let s = raw.replace(/[\s ']/g, "");
|
|
26
|
+
const lastComma = s.lastIndexOf(",");
|
|
27
|
+
const lastDot = s.lastIndexOf(".");
|
|
28
|
+
if (lastComma >= 0 && lastDot >= 0) {
|
|
29
|
+
s = lastComma > lastDot ? s.replace(/\./g, "").replace(",", ".") : s.replace(/,/g, "");
|
|
30
|
+
}
|
|
31
|
+
else if (lastComma >= 0) {
|
|
32
|
+
s = /^\d{1,3}(,\d{3})+$/.test(s) ? s.replace(/,/g, "") : s.replace(",", ".");
|
|
33
|
+
}
|
|
34
|
+
return Number(s);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Parses Wise's formatted activity amounts:
|
|
38
|
+
* "24.73 EUR" (out), "<positive>+ 4,500 EUR</positive>" (in), "<negative>- 5 EUR</negative>" (out).
|
|
39
|
+
* Returns undefined for non-amount text such as "Roundup".
|
|
40
|
+
*/
|
|
41
|
+
export function parseAmount(text, activityType) {
|
|
42
|
+
if (!text)
|
|
43
|
+
return undefined;
|
|
44
|
+
const plain = stripTags(text);
|
|
45
|
+
const m = plain.match(/([+\-−])?\s*([\d][\d.,\s ']*)\s*([A-Z]{3})\b/);
|
|
46
|
+
if (!m)
|
|
47
|
+
return undefined;
|
|
48
|
+
const amount = parseNumber(m[2]);
|
|
49
|
+
if (!Number.isFinite(amount))
|
|
50
|
+
return undefined;
|
|
51
|
+
let direction;
|
|
52
|
+
if (/<positive>/i.test(text) || m[1] === "+")
|
|
53
|
+
direction = "in";
|
|
54
|
+
else if (/<negative>/i.test(text) || m[1] === "-" || m[1] === "−")
|
|
55
|
+
direction = "out";
|
|
56
|
+
else
|
|
57
|
+
direction = activityType && NEUTRAL_TYPES.has(activityType) ? "neutral" : "out";
|
|
58
|
+
return { amount, currency: m[3], direction };
|
|
59
|
+
}
|
|
60
|
+
function profileName(p) {
|
|
61
|
+
return (p.fullName ??
|
|
62
|
+
p.businessName ??
|
|
63
|
+
p.details?.name ??
|
|
64
|
+
([p.firstName ?? p.details?.firstName, p.lastName ?? p.details?.lastName].filter(Boolean).join(" ") || undefined));
|
|
65
|
+
}
|
|
66
|
+
/** Wise accepts ISO 8601; plain dates are expanded to the whole UTC day. */
|
|
67
|
+
function isoBound(date, end) {
|
|
68
|
+
if (!date)
|
|
69
|
+
return undefined;
|
|
70
|
+
const d = new Date(/^\d{4}-\d{2}-\d{2}$/.test(date) ? `${date}T${end ? "23:59:59.999" : "00:00:00.000"}Z` : date);
|
|
71
|
+
if (Number.isNaN(d.getTime()))
|
|
72
|
+
throw new Error(`"${date}" isn't a valid date. Use YYYY-MM-DD or an ISO 8601 timestamp.`);
|
|
73
|
+
return d.toISOString();
|
|
74
|
+
}
|
|
75
|
+
export class WiseService {
|
|
76
|
+
client;
|
|
77
|
+
profiles;
|
|
78
|
+
constructor(client) {
|
|
79
|
+
this.client = client;
|
|
80
|
+
}
|
|
81
|
+
// --- Profiles ---------------------------------------------------------------
|
|
82
|
+
async listProfiles() {
|
|
83
|
+
if (this.profiles && Date.now() - this.profiles.at < PROFILE_TTL_MS)
|
|
84
|
+
return this.profiles.list;
|
|
85
|
+
const raw = await this.client.get("/v2/profiles");
|
|
86
|
+
const list = raw.map((p) => ({ id: p.id, type: String(p.type ?? "UNKNOWN"), name: profileName(p) }));
|
|
87
|
+
this.profiles = { at: Date.now(), list };
|
|
88
|
+
return list;
|
|
89
|
+
}
|
|
90
|
+
/** Accepts a profile id, "personal", "business" or (part of) a profile name. Default: the personal profile. */
|
|
91
|
+
async resolveProfile(ref) {
|
|
92
|
+
const profiles = await this.listProfiles();
|
|
93
|
+
if (!profiles.length)
|
|
94
|
+
throw new Error("This Wise user has no profiles.");
|
|
95
|
+
if (ref === undefined || ref === "")
|
|
96
|
+
return profiles.find((p) => p.type === "PERSONAL") ?? profiles[0];
|
|
97
|
+
const s = String(ref).trim().toLowerCase();
|
|
98
|
+
const found = profiles.find((p) => String(p.id) === s) ??
|
|
99
|
+
profiles.find((p) => p.type.toLowerCase() === s) ??
|
|
100
|
+
profiles.find((p) => p.name?.toLowerCase().includes(s));
|
|
101
|
+
if (!found) {
|
|
102
|
+
throw new Error(`Unknown profile "${ref}". Available: ${profiles.map((p) => `${p.id} (${p.type}${p.name ? ", " + p.name : ""})`).join("; ")}`);
|
|
103
|
+
}
|
|
104
|
+
return found;
|
|
105
|
+
}
|
|
106
|
+
/** One profile, or all of them when `ref` is omitted or "all". */
|
|
107
|
+
async resolveProfiles(ref) {
|
|
108
|
+
if (ref === undefined || ref === "" || String(ref).toLowerCase() === "all")
|
|
109
|
+
return this.listProfiles();
|
|
110
|
+
return [await this.resolveProfile(ref)];
|
|
111
|
+
}
|
|
112
|
+
// --- Balances ---------------------------------------------------------------
|
|
113
|
+
async listBalances(profiles, includeEmpty) {
|
|
114
|
+
const perProfile = await Promise.all(profiles.map(async (p) => {
|
|
115
|
+
const [standard, savings] = await Promise.all(["STANDARD", "SAVINGS"].map((types) => this.client.get(`/v4/profiles/${p.id}/balances`, { types })));
|
|
116
|
+
return [...standard, ...savings].map((b) => ({
|
|
117
|
+
id: b.id,
|
|
118
|
+
profile: { id: p.id, type: p.type },
|
|
119
|
+
kind: b.type === "SAVINGS" ? "jar" : "balance",
|
|
120
|
+
name: b.name ?? undefined,
|
|
121
|
+
currency: b.currency,
|
|
122
|
+
amount: b.amount?.value,
|
|
123
|
+
reserved: b.reservedAmount?.value || undefined,
|
|
124
|
+
total_worth: b.totalWorth?.value !== b.amount?.value ? b.totalWorth?.value : undefined,
|
|
125
|
+
}));
|
|
126
|
+
}));
|
|
127
|
+
return perProfile.flat().filter((b) => includeEmpty || Number(b.amount) !== 0 || Number(b.reserved ?? 0) !== 0);
|
|
128
|
+
}
|
|
129
|
+
// --- Activities (transactions, incl. card payments) ---------------------------
|
|
130
|
+
async listActivities(opts) {
|
|
131
|
+
const search = opts.search?.trim().toLowerCase();
|
|
132
|
+
const maxPages = search ? 20 : Math.ceil(opts.limit / 100) + 1;
|
|
133
|
+
const out = [];
|
|
134
|
+
let cursor = opts.cursor;
|
|
135
|
+
let pages = 0;
|
|
136
|
+
do {
|
|
137
|
+
// Without a search, page sizes line up with `limit` so Wise's cursor resumes exactly after the last item returned.
|
|
138
|
+
// With a search we keep whole pages (all matches), so a page can push the count slightly over `limit`.
|
|
139
|
+
const size = search ? 100 : Math.min(100, opts.limit - out.length);
|
|
140
|
+
const res = await this.client.get(`/v1/profiles/${opts.profile.id}/activities`, {
|
|
141
|
+
size,
|
|
142
|
+
since: isoBound(opts.since, false),
|
|
143
|
+
until: isoBound(opts.until, true),
|
|
144
|
+
monetaryResourceType: opts.resourceType,
|
|
145
|
+
status: opts.status,
|
|
146
|
+
nextCursor: cursor,
|
|
147
|
+
});
|
|
148
|
+
const items = (res.activities ?? []).map(mapActivity);
|
|
149
|
+
out.push(...(search ? items.filter((a) => matches(a, search)) : items));
|
|
150
|
+
cursor = res.cursor ?? undefined;
|
|
151
|
+
pages++;
|
|
152
|
+
} while (cursor && out.length < opts.limit && pages < maxPages);
|
|
153
|
+
return { activities: out, nextCursor: cursor, scannedPages: pages };
|
|
154
|
+
}
|
|
155
|
+
// --- Transfers ----------------------------------------------------------------
|
|
156
|
+
async listTransfers(opts) {
|
|
157
|
+
// Wise returns transfers oldest-first; fetch everything in range, then sort newest-first.
|
|
158
|
+
const PAGE = 100;
|
|
159
|
+
const MAX_PAGES = 20;
|
|
160
|
+
const all = [];
|
|
161
|
+
let offset = 0;
|
|
162
|
+
for (let page = 0; page < MAX_PAGES; page++) {
|
|
163
|
+
const batch = await this.client.get("/v1/transfers", {
|
|
164
|
+
profile: opts.profile.id,
|
|
165
|
+
limit: PAGE,
|
|
166
|
+
offset,
|
|
167
|
+
status: opts.status,
|
|
168
|
+
createdDateStart: isoBound(opts.since, false),
|
|
169
|
+
createdDateEnd: isoBound(opts.until, true),
|
|
170
|
+
});
|
|
171
|
+
all.push(...batch);
|
|
172
|
+
if (batch.length < PAGE)
|
|
173
|
+
break;
|
|
174
|
+
offset += PAGE;
|
|
175
|
+
}
|
|
176
|
+
const names = new Map();
|
|
177
|
+
if (all.length) {
|
|
178
|
+
const recipients = await this.listRecipients([opts.profile]).catch(() => []);
|
|
179
|
+
for (const r of recipients)
|
|
180
|
+
names.set(r.id, r.name);
|
|
181
|
+
}
|
|
182
|
+
const search = opts.search?.trim().toLowerCase();
|
|
183
|
+
const transfers = all
|
|
184
|
+
.map((t) => ({ ...mapTransfer(t), recipient_name: names.get(t.targetAccount) }))
|
|
185
|
+
.filter((t) => !search || JSON.stringify([t.reference, t.recipient_name, t.id]).toLowerCase().includes(search))
|
|
186
|
+
.sort((a, b) => String(b.created).localeCompare(String(a.created)));
|
|
187
|
+
return { total: transfers.length, transfers: transfers.slice(0, opts.limit), truncated: all.length >= PAGE * MAX_PAGES };
|
|
188
|
+
}
|
|
189
|
+
async getTransfer(id) {
|
|
190
|
+
const t = await this.client.get(`/v1/transfers/${id}`);
|
|
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 };
|
|
193
|
+
}
|
|
194
|
+
// --- Recipients -----------------------------------------------------------------
|
|
195
|
+
async listRecipients(profiles, currency) {
|
|
196
|
+
const lists = await Promise.all(profiles.map(async (p) => {
|
|
197
|
+
// Wise pages saved recipients with a seek position; follow it (capped) so long lists aren't cut at 100.
|
|
198
|
+
const items = [];
|
|
199
|
+
let seek;
|
|
200
|
+
for (let page = 0; page < 10; page++) {
|
|
201
|
+
const res = await this.client.get("/v2/accounts", { profileId: p.id, size: 100, currency: currency?.toUpperCase(), seekPosition: seek });
|
|
202
|
+
items.push(...(Array.isArray(res) ? res : (res.content ?? [])));
|
|
203
|
+
seek = Array.isArray(res) ? undefined : (res.seekPositionForNext ?? undefined);
|
|
204
|
+
if (seek === undefined || seek === null)
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
return items.map((r) => ({ ...mapRecipient(r), profile: { id: p.id, type: p.type } }));
|
|
208
|
+
}));
|
|
209
|
+
return lists.flat();
|
|
210
|
+
}
|
|
211
|
+
async getRecipient(id) {
|
|
212
|
+
return mapRecipient(await this.client.get(`/v2/accounts/${id}`));
|
|
213
|
+
}
|
|
214
|
+
// --- Quotes, recipients and transfers (write) ------------------------------------
|
|
215
|
+
/** Creates a Wise quote. Moves no money; a quote is the price a transfer would be made at. */
|
|
216
|
+
async createQuote(profile, q) {
|
|
217
|
+
const raw = await this.client.post(`/v3/profiles/${profile.id}/quotes`, {
|
|
218
|
+
sourceCurrency: q.sourceCurrency.toUpperCase(),
|
|
219
|
+
targetCurrency: q.targetCurrency.toUpperCase(),
|
|
220
|
+
sourceAmount: q.sourceAmount ?? null,
|
|
221
|
+
targetAmount: q.targetAmount ?? null,
|
|
222
|
+
targetAccount: q.targetAccount,
|
|
223
|
+
});
|
|
224
|
+
return mapQuote(raw);
|
|
225
|
+
}
|
|
226
|
+
/** Fields Wise needs to create a recipient in `target` currency (via a throwaway quote). */
|
|
227
|
+
async recipientRequirements(profile, source, target, amount) {
|
|
228
|
+
const quote = await this.createQuote(profile, { sourceCurrency: source, targetCurrency: target, sourceAmount: amount });
|
|
229
|
+
const raw = await this.client.get(`/v1/quotes/${quote.id}/account-requirements`, {}, { "Accept-Minor-Version": "1" });
|
|
230
|
+
return raw.map((r) => ({
|
|
231
|
+
type: r.type,
|
|
232
|
+
title: r.title,
|
|
233
|
+
usage: r.usageInfo ?? undefined,
|
|
234
|
+
fields: (r.fields ?? []).flatMap((f) => (f.group ?? []).map(mapRequirementField)),
|
|
235
|
+
}));
|
|
236
|
+
}
|
|
237
|
+
async createRecipient(profile, r) {
|
|
238
|
+
const raw = await this.client.post("/v1/accounts", {
|
|
239
|
+
profile: profile.id,
|
|
240
|
+
currency: r.currency.toUpperCase(),
|
|
241
|
+
type: r.type,
|
|
242
|
+
accountHolderName: r.accountHolderName,
|
|
243
|
+
ownedByCustomer: r.ownedByCustomer,
|
|
244
|
+
details: r.details,
|
|
245
|
+
});
|
|
246
|
+
return mapRecipient(raw);
|
|
247
|
+
}
|
|
248
|
+
/** Extra fields Wise needs for this transfer (e.g. transferPurpose, sourceOfFunds) that weren't provided. */
|
|
249
|
+
async missingTransferFields(quoteId, targetAccount, details) {
|
|
250
|
+
const raw = await this.client.post("/v1/transfer-requirements", { targetAccount, quoteUuid: quoteId, details });
|
|
251
|
+
const fields = (raw ?? []).flatMap((r) => (r.fields ?? []).flatMap((f) => (f.group ?? []).map(mapRequirementField)));
|
|
252
|
+
return fields
|
|
253
|
+
.map((f) => ({ ...f, key: f.key.replace(/^details\./, "") }))
|
|
254
|
+
.filter((f) => f.required && f.key !== "reference" && details[f.key] === undefined);
|
|
255
|
+
}
|
|
256
|
+
async createTransfer(t) {
|
|
257
|
+
// customerTransactionId makes this idempotent, so a retry after a timeout can't create a second transfer.
|
|
258
|
+
const raw = await this.client.post("/v1/transfers", { targetAccount: t.targetAccount, quoteUuid: t.quoteId, customerTransactionId: t.customerTransactionId, details: t.details }, { idempotent: true });
|
|
259
|
+
return mapTransfer(raw);
|
|
260
|
+
}
|
|
261
|
+
async cancelTransfer(id) {
|
|
262
|
+
return mapTransfer(await this.client.put(`/v1/transfers/${id}/cancel`));
|
|
263
|
+
}
|
|
264
|
+
// --- Rates ------------------------------------------------------------------------
|
|
265
|
+
async getRate(source, target, at) {
|
|
266
|
+
const res = await this.client.get("/v1/rates", { source: source.toUpperCase(), target: target.toUpperCase(), time: at ? isoBound(at, false) : undefined });
|
|
267
|
+
const r = Array.isArray(res) ? res[0] : res;
|
|
268
|
+
if (!r)
|
|
269
|
+
throw new WiseApiError(404, `No rate for ${source}→${target}`);
|
|
270
|
+
return { source: r.source, target: r.target, rate: r.rate, time: r.time };
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
function mapActivity(a) {
|
|
274
|
+
const amount = parseAmount(a.primaryAmount, a.type);
|
|
275
|
+
const secondary = parseAmount(a.secondaryAmount);
|
|
276
|
+
const secondaryText = stripTags(a.secondaryAmount);
|
|
277
|
+
return {
|
|
278
|
+
id: a.id,
|
|
279
|
+
date: a.createdOn,
|
|
280
|
+
type: a.type,
|
|
281
|
+
status: a.status,
|
|
282
|
+
title: stripTags(a.title),
|
|
283
|
+
description: stripTags(a.description) || undefined,
|
|
284
|
+
direction: amount?.direction,
|
|
285
|
+
amount: amount?.amount,
|
|
286
|
+
currency: amount?.currency,
|
|
287
|
+
amount_text: amount ? undefined : stripTags(a.primaryAmount) || undefined,
|
|
288
|
+
secondary_amount: secondary ? { amount: secondary.amount, currency: secondary.currency } : undefined,
|
|
289
|
+
secondary_text: !secondary && secondaryText ? secondaryText : undefined,
|
|
290
|
+
resource: a.resource?.type ? { type: a.resource.type, id: a.resource.id } : undefined,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
function matches(a, search) {
|
|
294
|
+
return [a.title, a.description, a.type, a.secondary_text, a.amount_text].some((v) => v?.toLowerCase().includes(search));
|
|
295
|
+
}
|
|
296
|
+
function mapTransfer(t) {
|
|
297
|
+
return {
|
|
298
|
+
id: t.id,
|
|
299
|
+
created: t.created,
|
|
300
|
+
status: t.status,
|
|
301
|
+
reference: t.reference || t.details?.reference || undefined,
|
|
302
|
+
source: { amount: t.sourceValue, currency: t.sourceCurrency },
|
|
303
|
+
target: { amount: t.targetValue, currency: t.targetCurrency },
|
|
304
|
+
recipient_id: t.targetAccount ?? undefined,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
function mapRecipient(r) {
|
|
308
|
+
return {
|
|
309
|
+
id: r.id,
|
|
310
|
+
name: typeof r.name === "string" ? r.name : (r.name?.fullName ?? r.accountHolderName),
|
|
311
|
+
currency: r.currency,
|
|
312
|
+
type: r.type,
|
|
313
|
+
account: r.accountSummary ?? r.longAccountSummary ?? undefined,
|
|
314
|
+
details: r.longAccountSummary && r.longAccountSummary !== r.accountSummary ? r.longAccountSummary : undefined,
|
|
315
|
+
owned_by_me: r.ownedByCustomer ?? undefined,
|
|
316
|
+
profile_id: r.profileId ?? r.profile ?? undefined,
|
|
317
|
+
active: r.active ?? undefined,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
function mapQuote(q) {
|
|
321
|
+
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.
|
|
323
|
+
const o = options.find((x) => x.payIn === "BALANCE") ?? options[0];
|
|
324
|
+
return {
|
|
325
|
+
id: String(q.id),
|
|
326
|
+
source: { amount: o?.sourceAmount ?? q.sourceAmount, currency: q.sourceCurrency },
|
|
327
|
+
target: { amount: o?.targetAmount ?? q.targetAmount, currency: q.targetCurrency },
|
|
328
|
+
rate: q.rate,
|
|
329
|
+
rate_type: q.rateType,
|
|
330
|
+
rate_expires: q.rateExpirationTime,
|
|
331
|
+
fee: o?.fee?.total !== undefined ? { amount: o.fee.total, currency: q.sourceCurrency } : undefined,
|
|
332
|
+
pay_in: o?.payIn,
|
|
333
|
+
pay_out: o?.payOut ?? q.payOut,
|
|
334
|
+
estimated_delivery: o?.estimatedDelivery,
|
|
335
|
+
delivery: o?.formattedEstimatedDelivery,
|
|
336
|
+
notices: (q.notices ?? []).map((n) => stripTags(n.text)).filter(Boolean),
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
function mapRequirementField(g) {
|
|
340
|
+
return {
|
|
341
|
+
key: String(g.key),
|
|
342
|
+
name: g.name,
|
|
343
|
+
required: !!g.required,
|
|
344
|
+
type: g.type,
|
|
345
|
+
example: g.example || undefined,
|
|
346
|
+
pattern: g.validationRegexp || undefined,
|
|
347
|
+
allowed: g.valuesAllowed?.length ? g.valuesAllowed.map((v) => ({ key: v.key, name: v.name })) : undefined,
|
|
348
|
+
refresh_on_change: g.refreshRequirementsOnChange || undefined,
|
|
349
|
+
};
|
|
350
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,71 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blanketbyte/wise-mcp",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
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
|
+
"mcpName": "io.github.BlanketByte/wise-mcp",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"mcp",
|
|
8
|
+
"model-context-protocol",
|
|
9
|
+
"wise",
|
|
10
|
+
"transferwise",
|
|
11
|
+
"claude",
|
|
12
|
+
"chatgpt",
|
|
13
|
+
"codex",
|
|
14
|
+
"mcp-server",
|
|
15
|
+
"banking",
|
|
16
|
+
"payments"
|
|
17
|
+
],
|
|
18
|
+
"homepage": "https://github.com/BlanketByte/wise-mcp#readme",
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/BlanketByte/wise-mcp/issues"
|
|
21
|
+
},
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/BlanketByte/wise-mcp.git"
|
|
25
|
+
},
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"author": {
|
|
28
|
+
"name": "BlanketByte",
|
|
29
|
+
"url": "https://blanketbyte.com"
|
|
30
|
+
},
|
|
31
|
+
"type": "module",
|
|
32
|
+
"bin": {
|
|
33
|
+
"wise-mcp": "dist/cli.js"
|
|
34
|
+
},
|
|
35
|
+
"main": "dist/index.js",
|
|
36
|
+
"files": [
|
|
37
|
+
"dist",
|
|
38
|
+
"public",
|
|
39
|
+
"README.md",
|
|
40
|
+
"LICENSE"
|
|
41
|
+
],
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=20"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsc",
|
|
47
|
+
"start": "node dist/cli.js serve",
|
|
48
|
+
"dev": "tsx watch src/cli.ts serve",
|
|
49
|
+
"typecheck": "tsc --noEmit",
|
|
50
|
+
"e2e": "tsx scripts/e2e.ts",
|
|
51
|
+
"e2e:stdio": "tsx scripts/e2e-stdio.ts",
|
|
52
|
+
"test": "npm run typecheck && npm run e2e && npm run e2e:stdio",
|
|
53
|
+
"mcpb": "bash scripts/build-mcpb.sh",
|
|
54
|
+
"prepublishOnly": "npm run build && npm test"
|
|
55
|
+
},
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"@modelcontextprotocol/sdk": "^1.32.1",
|
|
58
|
+
"express": "^5.3.0",
|
|
59
|
+
"zod": "^4.6.5"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@types/express": "^5.0.6",
|
|
63
|
+
"@types/node": "^26.6.5",
|
|
64
|
+
"tsx": "^4.23.15",
|
|
65
|
+
"typescript": "^7.0.2"
|
|
66
|
+
},
|
|
67
|
+
"publishConfig": {
|
|
68
|
+
"access": "public",
|
|
69
|
+
"provenance": true
|
|
70
|
+
}
|
|
71
|
+
}
|
|
Binary file
|
|
Binary file
|
package/public/icon.png
ADDED
|
Binary file
|
package/public/logo.png
ADDED
|
Binary file
|
package/public/logo.svg
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 160 160">
|
|
2
|
+
<title>Wise MCP</title>
|
|
3
|
+
<rect width="160" height="160" rx="36" fill="#1a4d5c"/>
|
|
4
|
+
<rect x="28" y="38" width="104" height="70" rx="8" fill="#9fe870"/>
|
|
5
|
+
<path transform="translate(54.4,47.4) scale(1.6)" d="M0 20.2803L8.65113 9.64374H8.64862L3.38975 0H32L20.9261 32H14.6508L23.8219 5.45086H11.9916L14.6223 10.2126L14.6092 10.3439L10.5644 15.2642H17.1828L15.4463 20.2803H0Z" fill="#163300"/>
|
|
6
|
+
<path d="M18 114 H142 L134 126 H26 Z" fill="#5ba3b8"/>
|
|
7
|
+
</svg>
|