@dishtaweera/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 +155 -2
- package/dist/index.js +72 -0
- package/dist/tools.js +230 -0
- package/dist/wise.js +43 -0
- package/package.json +51 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Deepana Ishtaweera
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,156 @@
|
|
|
1
|
-
#
|
|
1
|
+
# wise-mcp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
An MCP server for the [Wise](https://wise.com) (formerly TransferWise) API. It lets Claude, ChatGPT or any MCP agent check your balances, look up exchange rates, price transfers, manage recipients, and create, fund or track transfers. It authenticates with your Wise personal API token.
|
|
4
|
+
|
|
5
|
+
Built against **Wise Platform API version `2026Q4`** (`https://api.wise.com/2026Q4`). It runs locally over **stdio** for Claude Desktop and Claude Code, and over **Streamable HTTP** for ChatGPT, claude.ai and other remote clients.
|
|
6
|
+
|
|
7
|
+
## Tools
|
|
8
|
+
|
|
9
|
+
| Access level | Tool | Wise endpoint |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| `read` | `wise_list_profiles` | `GET /profiles` |
|
|
12
|
+
| | `wise_get_balances` | `GET /profiles/{id}/balances` |
|
|
13
|
+
| | `wise_get_exchange_rate` | `GET /rates` (current, historic, history) |
|
|
14
|
+
| | `wise_list_currencies` | `GET /currencies` |
|
|
15
|
+
| | `wise_create_quote` / `wise_get_quote` | `POST/GET /profiles/{id}/quotes` (moves no money) |
|
|
16
|
+
| | `wise_list_recipients` / `wise_get_recipient` | `GET /accounts` |
|
|
17
|
+
| | `wise_get_recipient_requirements` | `GET /quotes/{id}/account-requirements` |
|
|
18
|
+
| | `wise_list_transfers` / `wise_get_transfer` | `GET /transfers` |
|
|
19
|
+
| | `wise_get_delivery_estimate` | `GET /delivery-estimates/{id}` |
|
|
20
|
+
| | `wise_list_activities` | `GET /profiles/{id}/activities` |
|
|
21
|
+
| | `wise_get_balance_statement` | `GET /profiles/{id}/balance-statements/{balanceId}/statement.json` |
|
|
22
|
+
| `write` | `wise_create_recipient` | `POST /accounts` |
|
|
23
|
+
| | `wise_create_transfer` | `POST /transfers` (a draft; no money moves yet) |
|
|
24
|
+
| | `wise_cancel_transfer` | `PUT /transfers/{id}/cancel` |
|
|
25
|
+
| `payments` | `wise_fund_transfer` | `POST /profiles/{id}/transfers/{id}/payments` (**sends money**) |
|
|
26
|
+
|
|
27
|
+
`WISE_ACCESS` picks the level, and each level includes the ones above it.
|
|
28
|
+
|
|
29
|
+
**Recommended: keep the default `read`.** It covers recording and bookkeeping (balances, activity feed, transfer history, statements and rates) and an agent can't create or send anything at that level. `write` and `payments` are there if you ever need them, but they aren't recommended for agents. For recording, `wise_list_activities` and `wise_list_transfers` work with any personal token. `wise_get_balance_statement` gives line-by-line transactions, but only for accounts in the countries listed below. Tools above your level aren't registered at all, so the agent never sees them. `wise_fund_transfer` and `wise_cancel_transfer` are marked `destructiveHint`, so Claude and ChatGPT ask you before calling them.
|
|
30
|
+
|
|
31
|
+
A typical send flow:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
list_profiles → create_quote → list_recipients (or get_recipient_requirements → create_recipient)
|
|
35
|
+
→ create_transfer → [you confirm] → fund_transfer → get_transfer / get_delivery_estimate
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## What a personal token can and can't do
|
|
39
|
+
|
|
40
|
+
Wise documents these limits ([Personal API tokens](https://docs.wise.com/guides/developer/auth-and-security/personal-api-token)):
|
|
41
|
+
|
|
42
|
+
- Personal tokens are issued for **business accounts** (Settings → Connect and manage apps → API tokens).
|
|
43
|
+
- **Funding transfers and balance statements** only work for accounts based in the **US, Canada, Australia, New Zealand, Singapore and Malaysia**. Anywhere else, create the transfer with `wise_create_transfer` and approve it in the Wise app.
|
|
44
|
+
- **UK/EEA profiles** need Strong Customer Authentication (SCA) on protected calls. This server handles it automatically if you give it a key (see below).
|
|
45
|
+
- Some endpoints in Wise's reference are marked "user token only" but work with personal tokens. If one comes back with 403, Wise doesn't allow it for your token type.
|
|
46
|
+
|
|
47
|
+
## Setup
|
|
48
|
+
|
|
49
|
+
Published on npm as [`@dishtaweera/wise-mcp`](https://www.npmjs.com/package/@dishtaweera/wise-mcp). Your MCP client runs it with `npx`, so there is nothing to install or clone:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npx -y @dishtaweera/wise-mcp
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Requires Node 20+. Pass the variables below through your MCP client config. Try it against the **sandbox** first: create an account at [wise-sandbox.com](https://wise-sandbox.com), make an API token there, and leave `WISE_ENV=sandbox` (the default). Set `WISE_ENV=production` when you're ready.
|
|
56
|
+
|
|
57
|
+
| Variable | |
|
|
58
|
+
|---|---|
|
|
59
|
+
| `WISE_API_TOKEN` | **Required.** Your personal API token |
|
|
60
|
+
| `WISE_ENV` | `sandbox` (default) or `production` |
|
|
61
|
+
| `WISE_ACCESS` | `read` (default, recommended), `write` or `payments` (not recommended) |
|
|
62
|
+
| `WISE_PROFILE_ID` | Optional default profile, so tools don't need `profileId` |
|
|
63
|
+
| `WISE_PRIVATE_KEY_PATH` / `WISE_PRIVATE_KEY` | Optional RSA private key for SCA (a file path, or the inline PEM) |
|
|
64
|
+
| `MCP_AUTH_TOKEN` | HTTP mode only, **required** there: a shared secret of at least 32 characters |
|
|
65
|
+
| `PORT` | HTTP mode port (default 3000) |
|
|
66
|
+
|
|
67
|
+
### Claude Desktop
|
|
68
|
+
|
|
69
|
+
Add this to `~/Library/Application Support/Claude/claude_desktop_config.json` (on Windows, `%APPDATA%\Claude\claude_desktop_config.json`):
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"mcpServers": {
|
|
74
|
+
"wise": {
|
|
75
|
+
"command": "npx",
|
|
76
|
+
"args": ["-y", "@dishtaweera/wise-mcp"],
|
|
77
|
+
"env": {
|
|
78
|
+
"WISE_API_TOKEN": "your-token",
|
|
79
|
+
"WISE_ENV": "sandbox",
|
|
80
|
+
"WISE_ACCESS": "read"
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Claude Code
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
claude mcp add wise -e WISE_API_TOKEN=your-token -e WISE_ENV=sandbox -- npx -y @dishtaweera/wise-mcp
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### ChatGPT desktop app (local)
|
|
94
|
+
|
|
95
|
+
The ChatGPT desktop app can run local stdio servers through its Codex settings. Add this to `~/.codex/config.toml`:
|
|
96
|
+
|
|
97
|
+
```toml
|
|
98
|
+
[mcp_servers.wise]
|
|
99
|
+
command = "npx"
|
|
100
|
+
args = ["-y", "@dishtaweera/wise-mcp"]
|
|
101
|
+
|
|
102
|
+
[mcp_servers.wise.env]
|
|
103
|
+
WISE_API_TOKEN = "your-token"
|
|
104
|
+
WISE_ENV = "sandbox"
|
|
105
|
+
WISE_ACCESS = "read"
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### ChatGPT web (and claude.ai custom connectors)
|
|
109
|
+
|
|
110
|
+
These clients connect to a **remote HTTPS** URL, so run the server in HTTP mode:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
export MCP_AUTH_TOKEN=$(openssl rand -hex 32)
|
|
114
|
+
WISE_API_TOKEN=your-token npx -y @dishtaweera/wise-mcp --http # http://localhost:3000/mcp
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Expose it over HTTPS. For a quick test, use a tunnel such as `cloudflared tunnel --url http://localhost:3000`. For anything lasting, deploy it to any Node host and set the environment variables there.
|
|
118
|
+
|
|
119
|
+
ChatGPT connectors support only OAuth or no auth; they can't send a custom header. So the server also accepts the secret as the last part of the URL path:
|
|
120
|
+
|
|
121
|
+
- **ChatGPT**: Settings → Apps & Connectors → Advanced → turn on **Developer mode**, then create a connector with URL `https://your-host/mcp/<MCP_AUTH_TOKEN>` and authentication set to **No authentication**.
|
|
122
|
+
- **claude.ai**: Settings → Connectors → Add custom connector, using the same URL.
|
|
123
|
+
- **Clients that can send headers**: use `https://your-host/mcp` with `Authorization: Bearer <MCP_AUTH_TOKEN>`.
|
|
124
|
+
|
|
125
|
+
Treat that URL like a password: anyone who has it can use your Wise account at the access level you set. Use HTTPS only, and change `MCP_AUTH_TOKEN` if the URL leaks.
|
|
126
|
+
|
|
127
|
+
## SCA (UK/EEA accounts)
|
|
128
|
+
|
|
129
|
+
When Wise needs extra authentication, it answers a request with `403` and a one-time token in the `x-2fa-approval` header. The server signs that token with your private key (SHA256withRSA, base64) and resends the request with `x-2fa-approval` and `X-Signature` headers. To set it up:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
openssl genrsa -out wise-private.pem 2048
|
|
133
|
+
openssl rsa -pubout -in wise-private.pem -out wise-public.pem
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Upload `wise-public.pem` in Wise under Settings → API tokens → Manage public keys. Then set `WISE_PRIVATE_KEY_PATH=/path/to/wise-private.pem`. In the sandbox, SCA is off until you enable it on that same page.
|
|
137
|
+
|
|
138
|
+
## Development
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
git clone https://github.com/deepanaishtaweera/wise-mcp && cd wise-mcp
|
|
142
|
+
npm install
|
|
143
|
+
npm test # builds, then runs node:test against a mocked Wise API
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
The code is in three files: `src/wise.ts` (HTTP client and SCA signing), `src/tools.ts` (tool definitions) and `src/index.ts` (config and the stdio/HTTP transports). Responses are passed through as Wise's raw JSON text. Wise IDs are int64, so this avoids any chance of rounding them.
|
|
147
|
+
|
|
148
|
+
## References
|
|
149
|
+
|
|
150
|
+
- [Wise API reference (2026Q4)](https://docs.wise.com/api-reference) and its [OpenAPI spec](https://docs.wise.com/_bundle/api-reference/@latest/index.yaml)
|
|
151
|
+
- [Versioning](https://docs.wise.com/guides/developer/global-versioning)
|
|
152
|
+
- [Personal API tokens](https://docs.wise.com/guides/developer/auth-and-security/personal-api-token)
|
|
153
|
+
- [SCA over API](https://docs.wise.com/guides/developer/auth-and-security/sca-over-api) and [One Time Token](https://docs.wise.com/guides/developer/auth-and-security/one-time-token)
|
|
154
|
+
- [Fund a transfer](https://docs.wise.com/api-reference/transfer/transferfund)
|
|
155
|
+
- [Sandbox v2 migration](https://docs.wise.com/guides/developer/environments/sandbox-v2-migration)
|
|
156
|
+
- [ChatGPT developer mode](https://developers.openai.com/api/docs/guides/developer-mode)
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { createServer } from "node:http";
|
|
4
|
+
import { timingSafeEqual } from "node:crypto";
|
|
5
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
8
|
+
import { ACCESS_LEVELS, registerTools } from "./tools.js";
|
|
9
|
+
import { createWiseClient } from "./wise.js";
|
|
10
|
+
const env = process.env;
|
|
11
|
+
const fail = (msg) => {
|
|
12
|
+
console.error(`wise-mcp: ${msg}`);
|
|
13
|
+
process.exit(1);
|
|
14
|
+
};
|
|
15
|
+
const token = env.WISE_API_TOKEN ?? fail("WISE_API_TOKEN is required (Wise > Settings > API tokens).");
|
|
16
|
+
const wiseEnv = (env.WISE_ENV ?? "sandbox");
|
|
17
|
+
if (wiseEnv !== "sandbox" && wiseEnv !== "production")
|
|
18
|
+
fail("WISE_ENV must be sandbox or production.");
|
|
19
|
+
const access = (env.WISE_ACCESS ?? "read");
|
|
20
|
+
if (!ACCESS_LEVELS.includes(access))
|
|
21
|
+
fail(`WISE_ACCESS must be one of ${ACCESS_LEVELS.join(", ")}.`);
|
|
22
|
+
const defaultProfileId = env.WISE_PROFILE_ID ? Number(env.WISE_PROFILE_ID) : undefined;
|
|
23
|
+
const privateKey = env.WISE_PRIVATE_KEY ?? (env.WISE_PRIVATE_KEY_PATH && readFileSync(env.WISE_PRIVATE_KEY_PATH, "utf8"));
|
|
24
|
+
const wise = createWiseClient({ token, env: wiseEnv, privateKey: privateKey || undefined });
|
|
25
|
+
function buildServer() {
|
|
26
|
+
const server = new McpServer({ name: "wise-mcp", version: "1.0.0" }, {
|
|
27
|
+
instructions: `Wise (${wiseEnv}) banking tools, access level "${access}". ` +
|
|
28
|
+
"Typical send flow: wise_list_profiles -> wise_create_quote -> wise_list_recipients or " +
|
|
29
|
+
"wise_get_recipient_requirements + wise_create_recipient -> wise_create_transfer -> wise_fund_transfer. " +
|
|
30
|
+
"Always show the user the quote (amounts, fee, rate, recipient) and get explicit confirmation before funding a transfer.",
|
|
31
|
+
});
|
|
32
|
+
registerTools(server, wise, { access, defaultProfileId });
|
|
33
|
+
return server;
|
|
34
|
+
}
|
|
35
|
+
if (process.argv.includes("--http") || env.MCP_TRANSPORT === "http") {
|
|
36
|
+
// Exposes a bank account to the network, so refuse to run without a shared secret.
|
|
37
|
+
const secret = env.MCP_AUTH_TOKEN ?? "";
|
|
38
|
+
if (secret.length < 32)
|
|
39
|
+
fail("HTTP mode needs MCP_AUTH_TOKEN of at least 32 chars (e.g. `openssl rand -hex 32`).");
|
|
40
|
+
const port = Number(env.PORT ?? 3000);
|
|
41
|
+
const same = (a) => a.length === secret.length && timingSafeEqual(Buffer.from(a), Buffer.from(secret));
|
|
42
|
+
createServer(async (req, res) => {
|
|
43
|
+
const path = new URL(req.url ?? "/", "http://x").pathname;
|
|
44
|
+
const bearer = req.headers.authorization?.replace(/^Bearer /i, "") ?? "";
|
|
45
|
+
// ChatGPT connectors can't send custom headers, so the secret may also be the last path segment: /mcp/<secret>
|
|
46
|
+
const authed = (path === "/mcp" && same(bearer)) || (path.startsWith("/mcp/") && same(path.slice(5)));
|
|
47
|
+
if (!authed) {
|
|
48
|
+
res.writeHead(path === "/mcp" || path.startsWith("/mcp/") ? 401 : 404).end();
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
// Stateless: a fresh server per request keeps horizontal scaling and restarts trivial.
|
|
52
|
+
const server = buildServer();
|
|
53
|
+
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
54
|
+
res.on("close", () => {
|
|
55
|
+
transport.close();
|
|
56
|
+
server.close();
|
|
57
|
+
});
|
|
58
|
+
try {
|
|
59
|
+
await server.connect(transport);
|
|
60
|
+
await transport.handleRequest(req, res);
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
console.error(e);
|
|
64
|
+
if (!res.headersSent)
|
|
65
|
+
res.writeHead(500).end();
|
|
66
|
+
}
|
|
67
|
+
}).listen(port, () => console.error(`wise-mcp (${wiseEnv}, ${access}) listening on http://localhost:${port}/mcp`));
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
await buildServer().connect(new StdioServerTransport());
|
|
71
|
+
console.error(`wise-mcp (${wiseEnv}, ${access}) running on stdio`);
|
|
72
|
+
}
|
package/dist/tools.js
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
export const ACCESS_LEVELS = ["read", "write", "payments"];
|
|
4
|
+
const id = (what) => z.number().int().positive().describe(what);
|
|
5
|
+
const currency = (what) => z.string().length(3).toUpperCase().describe(`${what} (ISO 4217, e.g. EUR)`);
|
|
6
|
+
const profileId = id("Wise profile ID. Optional when WISE_PROFILE_ID is set; get it from wise_list_profiles.").optional();
|
|
7
|
+
export function registerTools(server, wise, opts) {
|
|
8
|
+
const pid = (p) => {
|
|
9
|
+
const v = p ?? opts.defaultProfileId;
|
|
10
|
+
if (!v)
|
|
11
|
+
throw new Error("profileId is required: call wise_list_profiles first, or set WISE_PROFILE_ID.");
|
|
12
|
+
return v;
|
|
13
|
+
};
|
|
14
|
+
const tool = (level, name, config, run) => {
|
|
15
|
+
if (ACCESS_LEVELS.indexOf(level) > ACCESS_LEVELS.indexOf(opts.access))
|
|
16
|
+
return;
|
|
17
|
+
server.registerTool(name, {
|
|
18
|
+
title: config.title,
|
|
19
|
+
description: config.description,
|
|
20
|
+
inputSchema: config.inputSchema,
|
|
21
|
+
annotations: {
|
|
22
|
+
title: config.title,
|
|
23
|
+
readOnlyHint: config.readOnly ?? false,
|
|
24
|
+
destructiveHint: config.destructive ?? false,
|
|
25
|
+
openWorldHint: true,
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
// @ts-expect-error SDK's generic callback type doesn't narrow from our generic shape
|
|
29
|
+
async (args) => {
|
|
30
|
+
try {
|
|
31
|
+
return { content: [{ type: "text", text: (await run(args)) || "OK" }] };
|
|
32
|
+
}
|
|
33
|
+
catch (e) {
|
|
34
|
+
return { isError: true, content: [{ type: "text", text: e instanceof Error ? e.message : String(e) }] };
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
// ---------- read ----------
|
|
39
|
+
tool("read", "wise_list_profiles", {
|
|
40
|
+
title: "List profiles",
|
|
41
|
+
description: "List the Wise profiles (personal and business) this token can access. Most other tools need a profile ID from here.",
|
|
42
|
+
inputSchema: {},
|
|
43
|
+
readOnly: true,
|
|
44
|
+
}, () => wise.request("GET", "/profiles"));
|
|
45
|
+
tool("read", "wise_get_balances", {
|
|
46
|
+
title: "Get balances",
|
|
47
|
+
description: "Get multi-currency balances for a profile: amount, reserved amount and balance ID per currency.",
|
|
48
|
+
inputSchema: { profileId, types: z.string().default("STANDARD").describe("Comma-separated: STANDARD, SAVINGS") },
|
|
49
|
+
readOnly: true,
|
|
50
|
+
}, (a) => wise.request("GET", `/profiles/${pid(a.profileId)}/balances`, { query: { types: a.types } }));
|
|
51
|
+
tool("read", "wise_get_exchange_rate", {
|
|
52
|
+
title: "Get exchange rate",
|
|
53
|
+
description: "Get the current mid-market rate for a currency pair, a historic rate at `time`, or a history between `from` and `to` grouped by `group`.",
|
|
54
|
+
inputSchema: {
|
|
55
|
+
source: currency("Source currency"),
|
|
56
|
+
target: currency("Target currency"),
|
|
57
|
+
time: z.string().optional().describe("ISO 8601 timestamp for a historic rate"),
|
|
58
|
+
from: z.string().optional().describe("History start (ISO 8601 date or timestamp)"),
|
|
59
|
+
to: z.string().optional().describe("History end (ISO 8601 date or timestamp)"),
|
|
60
|
+
group: z.enum(["day", "hour", "minute"]).optional(),
|
|
61
|
+
},
|
|
62
|
+
readOnly: true,
|
|
63
|
+
}, (a) => wise.request("GET", "/rates", { query: a }));
|
|
64
|
+
tool("read", "wise_list_currencies", {
|
|
65
|
+
title: "List currencies",
|
|
66
|
+
description: "List all currencies Wise can send to.",
|
|
67
|
+
inputSchema: {},
|
|
68
|
+
readOnly: true,
|
|
69
|
+
}, () => wise.request("GET", "/currencies"));
|
|
70
|
+
tool("read", "wise_create_quote", {
|
|
71
|
+
title: "Create quote",
|
|
72
|
+
description: "Price a transfer: returns the rate, fees, delivery estimate and a quote ID (valid ~30 min) needed to create a transfer. " +
|
|
73
|
+
"Moves no money, but needs a full-access token (read-only tokens get error.quote.mustBeAuthenticated); " +
|
|
74
|
+
"use wise_get_exchange_rate for rates alone. Give exactly one of sourceAmount or targetAmount.",
|
|
75
|
+
inputSchema: {
|
|
76
|
+
profileId,
|
|
77
|
+
sourceCurrency: currency("Currency you send"),
|
|
78
|
+
targetCurrency: currency("Currency the recipient gets"),
|
|
79
|
+
sourceAmount: z.number().positive().optional(),
|
|
80
|
+
targetAmount: z.number().positive().optional(),
|
|
81
|
+
targetAccount: id("Recipient ID, if already known (gives a more accurate fee)").optional(),
|
|
82
|
+
preferredPayIn: z.string().default("BALANCE").describe("BALANCE to pay from your Wise balance, or BANK_TRANSFER"),
|
|
83
|
+
},
|
|
84
|
+
}, (a) => {
|
|
85
|
+
if ((a.sourceAmount === undefined) === (a.targetAmount === undefined)) {
|
|
86
|
+
throw new Error("Give exactly one of sourceAmount or targetAmount.");
|
|
87
|
+
}
|
|
88
|
+
const { profileId: p, ...body } = a;
|
|
89
|
+
return wise.request("POST", `/profiles/${pid(p)}/quotes`, { body });
|
|
90
|
+
});
|
|
91
|
+
tool("read", "wise_get_quote", {
|
|
92
|
+
title: "Get quote",
|
|
93
|
+
description: "Fetch an existing quote by ID.",
|
|
94
|
+
inputSchema: { profileId, quoteId: z.string().describe("Quote ID (UUID)") },
|
|
95
|
+
readOnly: true,
|
|
96
|
+
}, (a) => wise.request("GET", `/profiles/${pid(a.profileId)}/quotes/${encodeURIComponent(a.quoteId)}`));
|
|
97
|
+
tool("read", "wise_list_recipients", {
|
|
98
|
+
title: "List recipients",
|
|
99
|
+
description: "List saved recipient accounts. Paginate with seekPosition from the previous response's seekPositionForNext.",
|
|
100
|
+
inputSchema: {
|
|
101
|
+
profileId,
|
|
102
|
+
currency: currency("Filter by target currency").optional(),
|
|
103
|
+
size: z.number().int().min(1).max(20).optional(),
|
|
104
|
+
seekPosition: z.number().int().optional(),
|
|
105
|
+
},
|
|
106
|
+
readOnly: true,
|
|
107
|
+
}, (a) => wise.request("GET", "/accounts", { query: { ...a, profileId: pid(a.profileId) } }));
|
|
108
|
+
tool("read", "wise_get_recipient", {
|
|
109
|
+
title: "Get recipient",
|
|
110
|
+
description: "Fetch one recipient account by ID.",
|
|
111
|
+
inputSchema: { recipientId: id("Recipient account ID") },
|
|
112
|
+
readOnly: true,
|
|
113
|
+
}, (a) => wise.request("GET", `/accounts/${a.recipientId}`));
|
|
114
|
+
tool("read", "wise_get_recipient_requirements", {
|
|
115
|
+
title: "Get recipient requirements",
|
|
116
|
+
description: "For a quote, list which recipient account types exist for the route (e.g. iban, sort_code, aba) and the exact fields each needs. " +
|
|
117
|
+
"Call this before wise_create_recipient.",
|
|
118
|
+
inputSchema: { quoteId: z.string().describe("Quote ID from wise_create_quote") },
|
|
119
|
+
readOnly: true,
|
|
120
|
+
}, (a) => wise.request("GET", `/quotes/${encodeURIComponent(a.quoteId)}/account-requirements`));
|
|
121
|
+
tool("read", "wise_list_transfers", {
|
|
122
|
+
title: "List transfers",
|
|
123
|
+
description: "List transfers for a profile, OLDEST first, optionally filtered by status, currency and creation date. " +
|
|
124
|
+
"For recent transfers set createdDateStart rather than relying on order.",
|
|
125
|
+
inputSchema: {
|
|
126
|
+
profileId,
|
|
127
|
+
status: z.string().optional().describe("Comma-separated, e.g. incoming_payment_waiting,processing,outgoing_payment_sent,cancelled"),
|
|
128
|
+
sourceCurrency: currency("Source currency").optional(),
|
|
129
|
+
targetCurrency: currency("Target currency").optional(),
|
|
130
|
+
createdDateStart: z.string().optional().describe("ISO 8601 date, inclusive"),
|
|
131
|
+
createdDateEnd: z.string().optional().describe("ISO 8601 date, inclusive"),
|
|
132
|
+
limit: z.number().int().min(1).max(100).default(20),
|
|
133
|
+
offset: z.number().int().min(0).default(0),
|
|
134
|
+
},
|
|
135
|
+
readOnly: true,
|
|
136
|
+
}, ({ profileId: p, ...q }) => wise.request("GET", "/transfers", { query: { ...q, profile: pid(p) } }));
|
|
137
|
+
tool("read", "wise_get_transfer", {
|
|
138
|
+
title: "Get transfer",
|
|
139
|
+
description: "Get a transfer's details and current status.",
|
|
140
|
+
inputSchema: { transferId: id("Transfer ID") },
|
|
141
|
+
readOnly: true,
|
|
142
|
+
}, (a) => wise.request("GET", `/transfers/${a.transferId}`));
|
|
143
|
+
tool("read", "wise_get_delivery_estimate", {
|
|
144
|
+
title: "Get delivery estimate",
|
|
145
|
+
description: "Get when a transfer is expected to arrive.",
|
|
146
|
+
inputSchema: { transferId: id("Transfer ID"), timezone: z.string().optional().describe("e.g. Europe/London") },
|
|
147
|
+
readOnly: true,
|
|
148
|
+
}, (a) => wise.request("GET", `/delivery-estimates/${a.transferId}`, { query: { timezone: a.timezone } }));
|
|
149
|
+
tool("read", "wise_list_activities", {
|
|
150
|
+
title: "List activities",
|
|
151
|
+
description: "List recent account activity (card spend, transfers, conversions, top-ups). Paginate with nextCursor from the previous response's cursor.",
|
|
152
|
+
inputSchema: {
|
|
153
|
+
profileId,
|
|
154
|
+
status: z.enum(["REQUIRES_ATTENTION", "IN_PROGRESS", "UPCOMING", "COMPLETED", "CANCELLED"]).optional(),
|
|
155
|
+
since: z.string().optional().describe("ISO 8601 timestamp"),
|
|
156
|
+
until: z.string().optional().describe("ISO 8601 timestamp"),
|
|
157
|
+
size: z.number().int().min(1).max(100).optional(),
|
|
158
|
+
nextCursor: z.string().optional(),
|
|
159
|
+
},
|
|
160
|
+
readOnly: true,
|
|
161
|
+
}, ({ profileId: p, ...q }) => wise.request("GET", `/profiles/${pid(p)}/activities`, { query: q }));
|
|
162
|
+
tool("read", "wise_get_balance_statement", {
|
|
163
|
+
title: "Get balance statement",
|
|
164
|
+
description: "Get the transactions on one balance between two times. Needs SCA (WISE_PRIVATE_KEY_PATH) for UK/EEA profiles; " +
|
|
165
|
+
"personal tokens only support this for US, CA, AU, NZ, SG and MY accounts.",
|
|
166
|
+
inputSchema: {
|
|
167
|
+
profileId,
|
|
168
|
+
balanceId: id("Balance ID from wise_get_balances"),
|
|
169
|
+
currency: currency("Balance currency"),
|
|
170
|
+
intervalStart: z.string().describe("UTC ISO 8601, e.g. 2026-10-01T00:00:00.000Z"),
|
|
171
|
+
intervalEnd: z.string().describe("UTC ISO 8601; max 469 days after start"),
|
|
172
|
+
type: z.enum(["COMPACT", "FLAT"]).default("COMPACT"),
|
|
173
|
+
},
|
|
174
|
+
readOnly: true,
|
|
175
|
+
}, ({ profileId: p, balanceId, ...q }) => wise.request("GET", `/profiles/${pid(p)}/balance-statements/${balanceId}/statement.json`, { query: q }));
|
|
176
|
+
// ---------- write: creates records, moves no money ----------
|
|
177
|
+
tool("write", "wise_create_recipient", {
|
|
178
|
+
title: "Create recipient",
|
|
179
|
+
description: "Save a recipient bank account. Get `type` and the required `details` fields from wise_get_recipient_requirements first " +
|
|
180
|
+
"(e.g. GBP sort_code: {legalType, sortCode, accountNumber}; EUR iban: {legalType, iban}).",
|
|
181
|
+
inputSchema: {
|
|
182
|
+
profileId,
|
|
183
|
+
currency: currency("Recipient account currency"),
|
|
184
|
+
type: z.string().describe("Account type from requirements, e.g. iban, sort_code, aba"),
|
|
185
|
+
accountHolderName: z.string().min(1),
|
|
186
|
+
ownedByCustomer: z.boolean().optional().describe("True if the account belongs to the profile owner"),
|
|
187
|
+
details: z.record(z.string(), z.unknown()).describe("Route-specific fields from the requirements"),
|
|
188
|
+
},
|
|
189
|
+
}, ({ profileId: p, ...rest }) => wise.request("POST", "/accounts", { body: { ...rest, profile: pid(p) } }));
|
|
190
|
+
tool("write", "wise_create_transfer", {
|
|
191
|
+
title: "Create transfer (draft)",
|
|
192
|
+
description: "Create a transfer from a quote to a recipient. This is a draft: no money leaves until it is funded " +
|
|
193
|
+
"(wise_fund_transfer, or in the Wise app). One transfer per quote.",
|
|
194
|
+
inputSchema: {
|
|
195
|
+
quoteUuid: z.string().describe("Quote ID from wise_create_quote"),
|
|
196
|
+
targetAccount: id("Recipient ID"),
|
|
197
|
+
reference: z.string().max(100).optional().describe("Shown on the recipient's statement; max length varies by route"),
|
|
198
|
+
details: z.record(z.string(), z.unknown()).optional().describe("Extra route-specific fields, e.g. transferPurpose, sourceOfFunds"),
|
|
199
|
+
customerTransactionId: z.string().uuid().optional().describe("Idempotency key; generated if omitted"),
|
|
200
|
+
},
|
|
201
|
+
}, (a) => wise.request("POST", "/transfers", {
|
|
202
|
+
body: {
|
|
203
|
+
targetAccount: a.targetAccount,
|
|
204
|
+
quoteUuid: a.quoteUuid,
|
|
205
|
+
customerTransactionId: a.customerTransactionId ?? randomUUID(),
|
|
206
|
+
details: { ...a.details, ...(a.reference !== undefined && { reference: a.reference }) },
|
|
207
|
+
},
|
|
208
|
+
}));
|
|
209
|
+
tool("write", "wise_cancel_transfer", {
|
|
210
|
+
title: "Cancel transfer",
|
|
211
|
+
description: "Cancel a transfer. Only works while it is still incoming_payment_waiting.",
|
|
212
|
+
inputSchema: { transferId: id("Transfer ID") },
|
|
213
|
+
destructive: true,
|
|
214
|
+
}, (a) => wise.request("PUT", `/transfers/${a.transferId}/cancel`));
|
|
215
|
+
// ---------- payments: moves real money ----------
|
|
216
|
+
tool("payments", "wise_fund_transfer", {
|
|
217
|
+
title: "Fund transfer (sends money)",
|
|
218
|
+
description: "Pay a created transfer from your Wise balance. THIS SENDS REAL MONEY and cannot be undone once processed. " +
|
|
219
|
+
"Only call after the user has explicitly confirmed the amount and recipient. " +
|
|
220
|
+
"Personal tokens support this only for US, CA, AU, NZ, SG and MY accounts; UK/EEA needs SCA (WISE_PRIVATE_KEY_PATH).",
|
|
221
|
+
inputSchema: {
|
|
222
|
+
profileId,
|
|
223
|
+
transferId: id("Transfer ID from wise_create_transfer"),
|
|
224
|
+
balanceId: id("Balance to debit; defaults to the balance in the transfer's source currency").optional(),
|
|
225
|
+
},
|
|
226
|
+
destructive: true,
|
|
227
|
+
}, (a) => wise.request("POST", `/profiles/${pid(a.profileId)}/transfers/${a.transferId}/payments`, {
|
|
228
|
+
body: { type: "BALANCE", ...(a.balanceId && { balanceId: a.balanceId }) },
|
|
229
|
+
}));
|
|
230
|
+
}
|
package/dist/wise.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { createSign, randomUUID } from "node:crypto";
|
|
2
|
+
export const API_VERSION = "2026Q4";
|
|
3
|
+
const HOSTS = {
|
|
4
|
+
sandbox: "https://api.wise-sandbox.com",
|
|
5
|
+
production: "https://api.wise.com",
|
|
6
|
+
};
|
|
7
|
+
export function createWiseClient(opts) {
|
|
8
|
+
const base = `${HOSTS[opts.env]}/${API_VERSION}`;
|
|
9
|
+
return {
|
|
10
|
+
async request(method, path, { query, body } = {}) {
|
|
11
|
+
const url = new URL(base + path);
|
|
12
|
+
for (const [k, v] of Object.entries(query ?? {}))
|
|
13
|
+
if (v !== undefined)
|
|
14
|
+
url.searchParams.set(k, String(v));
|
|
15
|
+
const headers = {
|
|
16
|
+
Authorization: `Bearer ${opts.token}`,
|
|
17
|
+
Accept: "application/json",
|
|
18
|
+
"X-External-Correlation-Id": randomUUID(),
|
|
19
|
+
};
|
|
20
|
+
if (body !== undefined)
|
|
21
|
+
headers["Content-Type"] = "application/json";
|
|
22
|
+
const send = (extra = {}) => fetch(url, { method, headers: { ...headers, ...extra }, body: body === undefined ? undefined : JSON.stringify(body) });
|
|
23
|
+
let res = await send();
|
|
24
|
+
// SCA: Wise answers 403 with a one-time token that must be signed (SHA256withRSA) and replayed.
|
|
25
|
+
const ott = res.headers.get("x-2fa-approval");
|
|
26
|
+
if (res.status === 403 && ott && res.headers.get("x-2fa-approval-result") === "REJECTED") {
|
|
27
|
+
if (!opts.privateKey) {
|
|
28
|
+
throw new Error("Wise requires Strong Customer Authentication for this call. Set WISE_PRIVATE_KEY_PATH to the RSA private key " +
|
|
29
|
+
"whose public key is uploaded in Wise (Settings > API tokens > Manage public keys).");
|
|
30
|
+
}
|
|
31
|
+
const signature = createSign("SHA256").update(ott).sign(opts.privateKey, "base64");
|
|
32
|
+
res = await send({ "x-2fa-approval": ott, "X-Signature": signature });
|
|
33
|
+
}
|
|
34
|
+
// Returned verbatim: Wise IDs are int64, so avoid a JSON round-trip that could round them.
|
|
35
|
+
const text = await res.text();
|
|
36
|
+
if (!res.ok) {
|
|
37
|
+
const trace = res.headers.get("x-trace-id");
|
|
38
|
+
throw new Error(`Wise API ${method} ${path} failed with HTTP ${res.status}${trace ? ` (x-trace-id ${trace})` : ""}: ${text.slice(0, 4000)}`);
|
|
39
|
+
}
|
|
40
|
+
return text;
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,53 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dishtaweera/wise-mcp",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for the Wise (TransferWise) API: balances, quotes, recipients and transfers for Claude, ChatGPT and other agents",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"build": "tsc",
|
|
7
|
+
"start": "node dist/index.js",
|
|
8
|
+
"start:http": "node dist/index.js --http",
|
|
9
|
+
"test": "npm run build && node --test test/*.test.js",
|
|
10
|
+
"prepublishOnly": "npm test"
|
|
11
|
+
},
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"type": "module",
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@modelcontextprotocol/sdk": "^1.32.2",
|
|
16
|
+
"zod": "^4.6.7"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@types/node": "^26.6.6",
|
|
20
|
+
"typescript": "^7.0.2"
|
|
21
|
+
},
|
|
22
|
+
"bin": {
|
|
23
|
+
"wise-mcp": "dist/index.js"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist"
|
|
27
|
+
],
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=20"
|
|
30
|
+
},
|
|
31
|
+
"author": "Deepana Ishtaweera",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/deepanaishtaweera/wise-mcp.git"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://github.com/deepanaishtaweera/wise-mcp#readme",
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/deepanaishtaweera/wise-mcp/issues"
|
|
39
|
+
},
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
42
|
+
},
|
|
43
|
+
"keywords": [
|
|
44
|
+
"mcp",
|
|
45
|
+
"model-context-protocol",
|
|
46
|
+
"wise",
|
|
47
|
+
"transferwise",
|
|
48
|
+
"banking",
|
|
49
|
+
"exchange-rates",
|
|
50
|
+
"claude",
|
|
51
|
+
"chatgpt"
|
|
52
|
+
]
|
|
53
|
+
}
|