@venlyfinance/settlement-mcp 0.1.0 → 0.2.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/CHANGELOG.md +54 -0
- package/README.md +104 -35
- package/dist/client/sdk-client.d.ts +67 -0
- package/dist/client/sdk-client.js +183 -0
- package/dist/constants.d.ts +9 -5
- package/dist/constants.js +14 -5
- package/dist/index.d.ts +5 -4
- package/dist/index.js +8 -7
- package/dist/prompts.d.ts +2 -0
- package/dist/prompts.js +43 -0
- package/dist/reconcile.js +6 -0
- package/dist/resources.d.ts +2 -0
- package/dist/resources.js +106 -0
- package/dist/results.d.ts +18 -0
- package/dist/results.js +25 -0
- package/dist/safety.d.ts +5 -1
- package/dist/safety.js +27 -4
- package/dist/server.js +15 -1
- package/dist/staging-smoke.d.ts +17 -0
- package/dist/staging-smoke.js +199 -0
- package/dist/tools/read-tools.js +83 -12
- package/dist/tools/write-tools.js +245 -29
- package/dist/types.d.ts +70 -125
- package/dist/types.js +5 -13
- package/package.json +18 -10
- package/scripts/staging-smoke.mjs +13 -0
- package/skills/build-international-account.md +26 -0
- package/skills/mock-to-staging.md +11 -0
- package/skills/{payment-link-lifecycle.md → payment-session-lifecycle.md} +8 -7
- package/dist/client/http-client.d.ts +0 -58
- package/dist/client/http-client.js +0 -163
package/dist/tools/read-tools.js
CHANGED
|
@@ -3,18 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { reconcileByReferenceCode } from "../reconcile.js";
|
|
6
|
-
|
|
7
|
-
function jsonResult(data) {
|
|
8
|
-
return {
|
|
9
|
-
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
10
|
-
};
|
|
11
|
-
}
|
|
12
|
-
function errorResult(message) {
|
|
13
|
-
return {
|
|
14
|
-
content: [{ type: "text", text: `Error: ${message}` }],
|
|
15
|
-
isError: true,
|
|
16
|
-
};
|
|
17
|
-
}
|
|
6
|
+
import { errorResult, jsonResult } from "../results.js";
|
|
18
7
|
const READ_ONLY = {
|
|
19
8
|
readOnlyHint: true,
|
|
20
9
|
destructiveHint: false,
|
|
@@ -71,6 +60,23 @@ export function registerReadTools(server, client) {
|
|
|
71
60
|
return errorResult(e.message);
|
|
72
61
|
}
|
|
73
62
|
});
|
|
63
|
+
server.registerTool("list_accounts", {
|
|
64
|
+
title: "List accounts",
|
|
65
|
+
description: "List Venly Finance accounts before creating duplicates (finance GET /accounts). Read-only.",
|
|
66
|
+
inputSchema: {
|
|
67
|
+
page: z.number().int().min(1).optional(),
|
|
68
|
+
size: z.number().int().min(1).max(200).optional(),
|
|
69
|
+
},
|
|
70
|
+
annotations: READ_ONLY,
|
|
71
|
+
}, async (params) => {
|
|
72
|
+
try {
|
|
73
|
+
const result = await client.listAccounts(params);
|
|
74
|
+
return jsonResult({ count: result.length, accounts: result });
|
|
75
|
+
}
|
|
76
|
+
catch (e) {
|
|
77
|
+
return errorResult(e.message);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
74
80
|
server.registerTool("get_account", {
|
|
75
81
|
title: "Get account",
|
|
76
82
|
description: "Fetch a settlement account (finance GET /accounts/{accountId}). Read-only.",
|
|
@@ -84,6 +90,24 @@ export function registerReadTools(server, client) {
|
|
|
84
90
|
return errorResult(e.message);
|
|
85
91
|
}
|
|
86
92
|
});
|
|
93
|
+
server.registerTool("list_wallets", {
|
|
94
|
+
title: "List account wallets",
|
|
95
|
+
description: "List wallets auto-provisioned for a Finance account (finance GET /accounts/{accountId}/wallets). Read-only.",
|
|
96
|
+
inputSchema: {
|
|
97
|
+
accountId: z.string().describe("Account UUID"),
|
|
98
|
+
page: z.number().int().min(1).optional(),
|
|
99
|
+
size: z.number().int().min(1).max(200).optional(),
|
|
100
|
+
},
|
|
101
|
+
annotations: READ_ONLY,
|
|
102
|
+
}, async ({ accountId, page, size }) => {
|
|
103
|
+
try {
|
|
104
|
+
const result = await client.listWallets(accountId, { page, size });
|
|
105
|
+
return jsonResult({ count: result.length, wallets: result });
|
|
106
|
+
}
|
|
107
|
+
catch (e) {
|
|
108
|
+
return errorResult(e.message);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
87
111
|
server.registerTool("list_virtual_bank_accounts", {
|
|
88
112
|
title: "List virtual bank accounts",
|
|
89
113
|
description: "List the EUR vIBANs on an account, each with its reconciliation " +
|
|
@@ -99,6 +123,22 @@ export function registerReadTools(server, client) {
|
|
|
99
123
|
return errorResult(e.message);
|
|
100
124
|
}
|
|
101
125
|
});
|
|
126
|
+
server.registerTool("get_virtual_bank_account", {
|
|
127
|
+
title: "Get virtual bank account",
|
|
128
|
+
description: "Fetch receiving-account details including status, IBAN/BIC and referenceCode. Read-only.",
|
|
129
|
+
inputSchema: {
|
|
130
|
+
accountId: z.string().describe("Account UUID"),
|
|
131
|
+
virtualBankAccountId: z.string().describe("Virtual bank account UUID"),
|
|
132
|
+
},
|
|
133
|
+
annotations: READ_ONLY,
|
|
134
|
+
}, async ({ accountId, virtualBankAccountId }) => {
|
|
135
|
+
try {
|
|
136
|
+
return jsonResult(await client.getVirtualBankAccount(accountId, virtualBankAccountId));
|
|
137
|
+
}
|
|
138
|
+
catch (e) {
|
|
139
|
+
return errorResult(e.message);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
102
142
|
server.registerTool("reconcile_by_reference_code", {
|
|
103
143
|
title: "Reconcile by referenceCode",
|
|
104
144
|
description: "Match observed incoming bank transactions on an account's EUR vIBANs to " +
|
|
@@ -132,6 +172,24 @@ export function registerReadTools(server, client) {
|
|
|
132
172
|
return errorResult(e.message);
|
|
133
173
|
}
|
|
134
174
|
});
|
|
175
|
+
server.registerTool("list_transfers", {
|
|
176
|
+
title: "List transfers",
|
|
177
|
+
description: "List transfer history for an account (finance GET /accounts/{accountId}/transfers). Read-only.",
|
|
178
|
+
inputSchema: {
|
|
179
|
+
accountId: z.string().describe("Account UUID"),
|
|
180
|
+
page: z.number().int().min(1).optional(),
|
|
181
|
+
size: z.number().int().min(1).max(200).optional(),
|
|
182
|
+
},
|
|
183
|
+
annotations: READ_ONLY,
|
|
184
|
+
}, async ({ accountId, page, size }) => {
|
|
185
|
+
try {
|
|
186
|
+
const result = await client.listTransfers(accountId, { page, size });
|
|
187
|
+
return jsonResult({ count: result.length, transfers: result });
|
|
188
|
+
}
|
|
189
|
+
catch (e) {
|
|
190
|
+
return errorResult(e.message);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
135
193
|
server.registerTool("get_transfer", {
|
|
136
194
|
title: "Get transfer",
|
|
137
195
|
description: "Fetch a transfer by id (finance GET /accounts/{accountId}/transfers/{transferId}). Read-only.",
|
|
@@ -165,6 +223,19 @@ export function registerReadTools(server, client) {
|
|
|
165
223
|
return errorResult(e.message);
|
|
166
224
|
}
|
|
167
225
|
});
|
|
226
|
+
server.registerTool("get_party", {
|
|
227
|
+
title: "Get party",
|
|
228
|
+
description: "Fetch an individual or organisation party, including KYC/KYB state when present. Read-only.",
|
|
229
|
+
inputSchema: { partyId: z.string().describe("Party UUID") },
|
|
230
|
+
annotations: READ_ONLY,
|
|
231
|
+
}, async ({ partyId }) => {
|
|
232
|
+
try {
|
|
233
|
+
return jsonResult(await client.getParty(partyId));
|
|
234
|
+
}
|
|
235
|
+
catch (e) {
|
|
236
|
+
return errorResult(e.message);
|
|
237
|
+
}
|
|
238
|
+
});
|
|
168
239
|
server.registerTool("get_reference_data", {
|
|
169
240
|
title: "Get reference data",
|
|
170
241
|
description: "Fetch settlement reference data: supported chains, fiat currencies, " +
|
|
@@ -8,16 +8,14 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { z } from "zod";
|
|
10
10
|
import { buildDryRun, evaluateWriteGate } from "../safety.js";
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
isError: true,
|
|
20
|
-
};
|
|
11
|
+
import { errorResult, jsonResult } from "../results.js";
|
|
12
|
+
import { normalizeLegacyFiatTransfer } from "../client/sdk-client.js";
|
|
13
|
+
function executionResult(gate, result) {
|
|
14
|
+
return jsonResult({
|
|
15
|
+
mode: gate.environment === "mock" ? "mock" : "live",
|
|
16
|
+
environment: gate.environment,
|
|
17
|
+
result,
|
|
18
|
+
});
|
|
21
19
|
}
|
|
22
20
|
const WRITE_ANNOTATIONS = {
|
|
23
21
|
readOnlyHint: false,
|
|
@@ -30,18 +28,215 @@ const confirmField = z
|
|
|
30
28
|
.default(false)
|
|
31
29
|
.describe("Must be true to attempt a live call. Even then, VENLY_MCP_LIVE=1 and " +
|
|
32
30
|
"credentials are also required, otherwise the tool dry-runs.");
|
|
31
|
+
const addressSchema = z
|
|
32
|
+
.object({
|
|
33
|
+
addressLine1: z.string().optional(),
|
|
34
|
+
addressLine2: z.string().optional(),
|
|
35
|
+
city: z.string().optional(),
|
|
36
|
+
state: z.string().optional(),
|
|
37
|
+
postalCode: z.string().optional(),
|
|
38
|
+
country: z.string().regex(/^[A-Z]{2}$/).optional(),
|
|
39
|
+
})
|
|
40
|
+
.optional();
|
|
41
|
+
const partySchema = z.object({
|
|
42
|
+
partyType: z.enum(["INDIVIDUAL", "ORGANISATION"]),
|
|
43
|
+
externalId: z.string().optional(),
|
|
44
|
+
firstName: z.string().optional(),
|
|
45
|
+
lastName: z.string().optional(),
|
|
46
|
+
name: z.string().optional(),
|
|
47
|
+
vatNumber: z.string().optional(),
|
|
48
|
+
address: addressSchema,
|
|
49
|
+
});
|
|
50
|
+
function partyValidationError(party) {
|
|
51
|
+
if (party.partyType === "INDIVIDUAL" && (!party.firstName || !party.lastName)) {
|
|
52
|
+
return "INDIVIDUAL parties require firstName and lastName";
|
|
53
|
+
}
|
|
54
|
+
if (party.partyType === "ORGANISATION" && !party.name) {
|
|
55
|
+
return "ORGANISATION parties require name";
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
33
59
|
export function registerWriteTools(server, client, env) {
|
|
60
|
+
server.registerTool("create_party", {
|
|
61
|
+
title: "Create a customer or organisation party",
|
|
62
|
+
description: "Create a Finance party. This creates the party record; it does not complete KYC/KYB. Dry-run by default outside explicit mock mode.",
|
|
63
|
+
inputSchema: {
|
|
64
|
+
partyType: z.enum(["INDIVIDUAL", "ORGANISATION"]),
|
|
65
|
+
externalId: z.string().optional(),
|
|
66
|
+
firstName: z.string().optional(),
|
|
67
|
+
lastName: z.string().optional(),
|
|
68
|
+
name: z.string().optional(),
|
|
69
|
+
vatNumber: z.string().optional(),
|
|
70
|
+
address: addressSchema,
|
|
71
|
+
confirm: confirmField,
|
|
72
|
+
},
|
|
73
|
+
annotations: WRITE_ANNOTATIONS,
|
|
74
|
+
}, async ({ confirm, ...party }) => {
|
|
75
|
+
const validationError = partyValidationError(party);
|
|
76
|
+
if (validationError)
|
|
77
|
+
return errorResult(validationError);
|
|
78
|
+
const gate = evaluateWriteGate(confirm, env);
|
|
79
|
+
if (!gate.armed) {
|
|
80
|
+
return jsonResult(buildDryRun("create_party", "POST", "finance", "/parties", party, gate));
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
return executionResult(gate, await client.createParty(party));
|
|
84
|
+
}
|
|
85
|
+
catch (e) {
|
|
86
|
+
return errorResult(e.message);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
server.registerTool("create_account", {
|
|
90
|
+
title: "Create an account and provision its wallet",
|
|
91
|
+
description: "Create a Finance account. Venly auto-provisions its wallet on the selected chain. Supply partyId or an inline party. Dry-run by default.",
|
|
92
|
+
inputSchema: {
|
|
93
|
+
externalId: z.string().min(1),
|
|
94
|
+
name: z.string().optional(),
|
|
95
|
+
chain: z.enum(["AVALANCHE", "BASE", "POLYGON"]),
|
|
96
|
+
address: z.string().optional().describe("Required for SELF_CUSTODY tenants"),
|
|
97
|
+
partyId: z.string().optional(),
|
|
98
|
+
party: partySchema.optional(),
|
|
99
|
+
confirm: confirmField,
|
|
100
|
+
},
|
|
101
|
+
annotations: WRITE_ANNOTATIONS,
|
|
102
|
+
}, async ({ confirm, ...body }) => {
|
|
103
|
+
if (!body.partyId && !body.party) {
|
|
104
|
+
return errorResult("create_account requires partyId or an inline party");
|
|
105
|
+
}
|
|
106
|
+
if (body.party) {
|
|
107
|
+
const validationError = partyValidationError(body.party);
|
|
108
|
+
if (validationError)
|
|
109
|
+
return errorResult(validationError);
|
|
110
|
+
}
|
|
111
|
+
const gate = evaluateWriteGate(confirm, env);
|
|
112
|
+
if (!gate.armed) {
|
|
113
|
+
return jsonResult(buildDryRun("create_account", "POST", "finance", "/accounts", body, gate));
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
return executionResult(gate, await client.createAccount(body));
|
|
117
|
+
}
|
|
118
|
+
catch (e) {
|
|
119
|
+
return errorResult(e.message);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
server.registerTool("create_virtual_bank_account", {
|
|
123
|
+
title: "Create a EUR receiving account",
|
|
124
|
+
description: "Provision a EUR SEPA virtual bank account and conversion target. Outside mock mode the Finance account must have KYC status VERIFIED. Dry-run by default.",
|
|
125
|
+
inputSchema: {
|
|
126
|
+
accountId: z.string(),
|
|
127
|
+
name: z.string().min(1),
|
|
128
|
+
inCurrency: z.literal("EUR"),
|
|
129
|
+
targetCryptocurrency: z.enum(["USDC", "EURC", "USDT", "USDS"]),
|
|
130
|
+
idempotencyKey: z.string().min(1).optional(),
|
|
131
|
+
confirm: confirmField,
|
|
132
|
+
},
|
|
133
|
+
annotations: WRITE_ANNOTATIONS,
|
|
134
|
+
}, async ({ accountId, confirm, ...input }) => {
|
|
135
|
+
const body = {
|
|
136
|
+
...input,
|
|
137
|
+
idempotencyKey: input.idempotencyKey ?? crypto.randomUUID(),
|
|
138
|
+
};
|
|
139
|
+
const gate = evaluateWriteGate(confirm, env);
|
|
140
|
+
if (!gate.armed) {
|
|
141
|
+
const dryRun = buildDryRun("create_virtual_bank_account", "POST", "finance", `/accounts/${accountId}/virtual-bank-accounts`, body, gate);
|
|
142
|
+
dryRun.note += " The account must have KYC status VERIFIED before live provisioning.";
|
|
143
|
+
return jsonResult(dryRun);
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
return executionResult(gate, await client.createVirtualBankAccount(accountId, body));
|
|
147
|
+
}
|
|
148
|
+
catch (e) {
|
|
149
|
+
return errorResult(e.message);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
server.registerTool("create_fiat_transfer", {
|
|
153
|
+
title: "Create a fiat-denominated internal transfer",
|
|
154
|
+
description: "Create an account-to-account transfer using the current Finance OpenAPI fields. Dry-run by default outside explicit mock mode.",
|
|
155
|
+
inputSchema: {
|
|
156
|
+
senderAccountId: z.string(),
|
|
157
|
+
receiverAccountId: z.string().optional(),
|
|
158
|
+
receiverExternalId: z.string().optional(),
|
|
159
|
+
currency: z.enum(["EUR", "GBP", "USD"]),
|
|
160
|
+
amount: z.number(),
|
|
161
|
+
description: z.string().optional(),
|
|
162
|
+
merchantReference: z.string().optional(),
|
|
163
|
+
idempotencyKey: z.string().min(1).optional(),
|
|
164
|
+
confirm: confirmField,
|
|
165
|
+
},
|
|
166
|
+
annotations: WRITE_ANNOTATIONS,
|
|
167
|
+
}, async ({ senderAccountId, confirm, ...input }) => {
|
|
168
|
+
if (!input.receiverAccountId && !input.receiverExternalId) {
|
|
169
|
+
return errorResult("A receiverAccountId or receiverExternalId is required");
|
|
170
|
+
}
|
|
171
|
+
const body = {
|
|
172
|
+
...input,
|
|
173
|
+
idempotencyKey: input.idempotencyKey ?? crypto.randomUUID(),
|
|
174
|
+
};
|
|
175
|
+
const gate = evaluateWriteGate(confirm, env);
|
|
176
|
+
if (!gate.armed) {
|
|
177
|
+
return jsonResult(buildDryRun("create_fiat_transfer", "POST", "finance", `/accounts/${senderAccountId}/transfers/fiat`, body, gate));
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
return executionResult(gate, await client.createCurrentFiatTransfer(senderAccountId, body));
|
|
181
|
+
}
|
|
182
|
+
catch (e) {
|
|
183
|
+
return errorResult(e.message);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
server.registerTool("create_crypto_transfer", {
|
|
187
|
+
title: "Create a crypto-denominated internal transfer",
|
|
188
|
+
description: "Create an account-to-account asset transfer using the current Finance OpenAPI fields. Dry-run by default outside explicit mock mode.",
|
|
189
|
+
inputSchema: {
|
|
190
|
+
senderAccountId: z.string(),
|
|
191
|
+
receiverAccountId: z.string().optional(),
|
|
192
|
+
receiverExternalId: z.string().optional(),
|
|
193
|
+
chain: z.enum(["AVALANCHE", "BASE", "POLYGON"]),
|
|
194
|
+
asset: z.string().min(1),
|
|
195
|
+
amount: z.number(),
|
|
196
|
+
description: z.string().optional(),
|
|
197
|
+
merchantReference: z.string().optional(),
|
|
198
|
+
idempotencyKey: z.string().min(1).optional(),
|
|
199
|
+
confirm: confirmField,
|
|
200
|
+
},
|
|
201
|
+
annotations: WRITE_ANNOTATIONS,
|
|
202
|
+
}, async ({ senderAccountId, confirm, ...input }) => {
|
|
203
|
+
if (!input.receiverAccountId && !input.receiverExternalId) {
|
|
204
|
+
return errorResult("A receiverAccountId or receiverExternalId is required");
|
|
205
|
+
}
|
|
206
|
+
const body = {
|
|
207
|
+
...input,
|
|
208
|
+
idempotencyKey: input.idempotencyKey ?? crypto.randomUUID(),
|
|
209
|
+
};
|
|
210
|
+
const gate = evaluateWriteGate(confirm, env);
|
|
211
|
+
if (!gate.armed) {
|
|
212
|
+
return jsonResult(buildDryRun("create_crypto_transfer", "POST", "finance", `/accounts/${senderAccountId}/transfers/crypto`, body, gate));
|
|
213
|
+
}
|
|
214
|
+
try {
|
|
215
|
+
return executionResult(gate, await client.createCryptoTransfer(senderAccountId, body));
|
|
216
|
+
}
|
|
217
|
+
catch (e) {
|
|
218
|
+
return errorResult(e.message);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
34
221
|
server.registerTool("stage_transfer", {
|
|
35
222
|
title: "Stage a fiat transfer (dry-run by default)",
|
|
36
223
|
description: "Stage a fiat-to-crypto transfer (finance POST /accounts/{senderAccountId}/transfers/fiat). " +
|
|
37
|
-
"
|
|
38
|
-
"
|
|
224
|
+
"Legacy fiatAmount/fiatCurrency inputs are normalized to the current OpenAPI fields; " +
|
|
225
|
+
"the dry-run shows the exact normalized request. DISARMED by default: returns that " +
|
|
226
|
+
"request without sending unless confirm:true AND VENLY_MCP_LIVE=1 AND credentials are present.",
|
|
39
227
|
inputSchema: {
|
|
40
228
|
senderAccountId: z.string().describe("Account initiating the transfer"),
|
|
41
229
|
receiverAccountId: z.string(),
|
|
42
|
-
fiatAmount: z
|
|
230
|
+
fiatAmount: z
|
|
231
|
+
.string()
|
|
232
|
+
.refine((value) => value.trim() !== "" && Number.isFinite(Number(value)), "fiatAmount must be a numeric decimal string")
|
|
233
|
+
.describe("Decimal string, e.g. \"1000.00\""),
|
|
43
234
|
fiatCurrency: z.string().describe("e.g. EUR"),
|
|
44
|
-
cryptocurrency: z
|
|
235
|
+
cryptocurrency: z
|
|
236
|
+
.string()
|
|
237
|
+
.optional()
|
|
238
|
+
.describe("Retired: rejected with guidance. The current contract resolves the fiat " +
|
|
239
|
+
"amount to the account's settlement asset; use create_crypto_transfer instead."),
|
|
45
240
|
description: z.string().optional(),
|
|
46
241
|
merchantReference: z.string().optional(),
|
|
47
242
|
confirm: confirmField,
|
|
@@ -49,20 +244,31 @@ export function registerWriteTools(server, client, env) {
|
|
|
49
244
|
annotations: WRITE_ANNOTATIONS,
|
|
50
245
|
}, async ({ senderAccountId, confirm, ...rest }) => {
|
|
51
246
|
const gate = evaluateWriteGate(confirm, env);
|
|
52
|
-
const
|
|
247
|
+
const legacyInput = {
|
|
53
248
|
receiverAccountId: rest.receiverAccountId,
|
|
54
249
|
fiatAmount: rest.fiatAmount,
|
|
55
250
|
fiatCurrency: rest.fiatCurrency,
|
|
56
251
|
cryptocurrency: rest.cryptocurrency,
|
|
57
252
|
description: rest.description,
|
|
58
253
|
merchantReference: rest.merchantReference,
|
|
254
|
+
idempotencyKey: crypto.randomUUID(),
|
|
59
255
|
};
|
|
256
|
+
// Normalize BEFORE the gate branch so the dry-run preview is byte-for-byte
|
|
257
|
+
// the request a live call would send (and the retired cryptocurrency field
|
|
258
|
+
// is rejected instead of silently dropped).
|
|
259
|
+
let body;
|
|
260
|
+
try {
|
|
261
|
+
body = normalizeLegacyFiatTransfer(legacyInput);
|
|
262
|
+
}
|
|
263
|
+
catch (e) {
|
|
264
|
+
return errorResult(e.message);
|
|
265
|
+
}
|
|
60
266
|
if (!gate.armed) {
|
|
61
267
|
return jsonResult(buildDryRun("stage_transfer", "POST", "finance", `/accounts/${senderAccountId}/transfers/fiat`, body, gate));
|
|
62
268
|
}
|
|
63
269
|
try {
|
|
64
|
-
const result = await client.createFiatTransfer(senderAccountId,
|
|
65
|
-
return
|
|
270
|
+
const result = await client.createFiatTransfer(senderAccountId, legacyInput);
|
|
271
|
+
return executionResult(gate, result);
|
|
66
272
|
}
|
|
67
273
|
catch (e) {
|
|
68
274
|
return errorResult(e.message);
|
|
@@ -91,7 +297,7 @@ export function registerWriteTools(server, client, env) {
|
|
|
91
297
|
}
|
|
92
298
|
try {
|
|
93
299
|
const result = await client.approveRampRequest(id, body);
|
|
94
|
-
return
|
|
300
|
+
return executionResult(gate, result);
|
|
95
301
|
}
|
|
96
302
|
catch (e) {
|
|
97
303
|
return errorResult(e.message);
|
|
@@ -118,23 +324,30 @@ export function registerWriteTools(server, client, env) {
|
|
|
118
324
|
}
|
|
119
325
|
try {
|
|
120
326
|
const result = await client.rejectRampRequest(id, body);
|
|
121
|
-
return
|
|
327
|
+
return executionResult(gate, result);
|
|
122
328
|
}
|
|
123
329
|
catch (e) {
|
|
124
330
|
return errorResult(e.message);
|
|
125
331
|
}
|
|
126
332
|
});
|
|
127
|
-
server.registerTool("
|
|
128
|
-
title: "Create a fiat-to-crypto payment
|
|
129
|
-
description: "Create a pay-in
|
|
130
|
-
"
|
|
333
|
+
server.registerTool("create_payment_session", {
|
|
334
|
+
title: "Create a fiat-to-crypto payment session (dry-run by default)",
|
|
335
|
+
description: "Create a hosted pay-in session (finance POST " +
|
|
336
|
+
"/accounts/{accountId}/fiat-to-crypto/payment-sessions); redirect the " +
|
|
337
|
+
"payer to the returned paymentUrl. DISARMED by default.",
|
|
131
338
|
inputSchema: {
|
|
132
339
|
accountId: z.string(),
|
|
133
340
|
inAmount: z.string().describe("Decimal string, e.g. \"250.00\""),
|
|
134
341
|
inCurrency: z.string().describe("e.g. EUR"),
|
|
135
|
-
outCryptocurrency: z.string().
|
|
136
|
-
|
|
342
|
+
outCryptocurrency: z.string().describe("e.g. USDC"),
|
|
343
|
+
callbackUrl: z.string().describe("Webhook URL notified on completion"),
|
|
344
|
+
successRedirectUrl: z.string().optional(),
|
|
345
|
+
failureRedirectUrl: z.string().optional(),
|
|
137
346
|
externalRef: z.string().optional(),
|
|
347
|
+
idempotencyKey: z
|
|
348
|
+
.string()
|
|
349
|
+
.optional()
|
|
350
|
+
.describe("UUID; generated when omitted"),
|
|
138
351
|
confirm: confirmField,
|
|
139
352
|
},
|
|
140
353
|
annotations: WRITE_ANNOTATIONS,
|
|
@@ -144,15 +357,18 @@ export function registerWriteTools(server, client, env) {
|
|
|
144
357
|
inAmount: rest.inAmount,
|
|
145
358
|
inCurrency: rest.inCurrency,
|
|
146
359
|
outCryptocurrency: rest.outCryptocurrency,
|
|
147
|
-
|
|
360
|
+
callbackUrl: rest.callbackUrl,
|
|
361
|
+
successRedirectUrl: rest.successRedirectUrl,
|
|
362
|
+
failureRedirectUrl: rest.failureRedirectUrl,
|
|
148
363
|
externalRef: rest.externalRef,
|
|
364
|
+
idempotencyKey: rest.idempotencyKey ?? crypto.randomUUID(),
|
|
149
365
|
};
|
|
150
366
|
if (!gate.armed) {
|
|
151
|
-
return jsonResult(buildDryRun("
|
|
367
|
+
return jsonResult(buildDryRun("create_payment_session", "POST", "finance", `/accounts/${accountId}/fiat-to-crypto/payment-sessions`, body, gate));
|
|
152
368
|
}
|
|
153
369
|
try {
|
|
154
|
-
const result = await client.
|
|
155
|
-
return
|
|
370
|
+
const result = await client.createPayInSession(accountId, body);
|
|
371
|
+
return executionResult(gate, result);
|
|
156
372
|
}
|
|
157
373
|
catch (e) {
|
|
158
374
|
return errorResult(e.message);
|