@venlyfinance/settlement-mcp 0.1.1 → 0.3.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 +89 -0
- package/README.md +98 -31
- package/dist/client/sdk-client.d.ts +67 -0
- package/dist/client/sdk-client.js +183 -0
- package/dist/constants.d.ts +8 -4
- package/dist/constants.js +16 -4
- package/dist/index.d.ts +5 -4
- package/dist/index.js +15 -7
- package/dist/prompts.d.ts +2 -0
- package/dist/prompts.js +43 -0
- package/dist/reconcile.d.ts +7 -0
- package/dist/reconcile.js +26 -2
- 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 +7 -1
- package/dist/safety.js +28 -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 +91 -14
- package/dist/tools/write-tools.js +246 -22
- package/dist/types.d.ts +69 -127
- 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/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,19 +123,41 @@ 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 " +
|
|
105
145
|
"the vIBAN whose referenceCode they carry. Fetches the account's vIBANs " +
|
|
106
146
|
"(finance GET .../virtual-bank-accounts) and matches against the supplied " +
|
|
107
|
-
"transactions.
|
|
147
|
+
"transactions. Matching is remittance-text tolerant: case- and " +
|
|
148
|
+
"separator-insensitive, and a transaction matches when its normalized " +
|
|
149
|
+
"reference CONTAINS the normalized code (real payers type 'invoice ref " +
|
|
150
|
+
"abc 123 ty'). Codes under 4 alphanumeric characters are refused. " +
|
|
151
|
+
"Read-only, no mutation. Returns the matched vIBAN, matched " +
|
|
108
152
|
"transactions, and total amount.",
|
|
109
153
|
inputSchema: {
|
|
110
154
|
accountId: z.string().describe("Account UUID whose vIBANs to reconcile against"),
|
|
111
155
|
referenceCode: z.string().describe("The reference code to reconcile"),
|
|
112
156
|
transactions: z
|
|
113
157
|
.array(z.object({
|
|
114
|
-
referenceCode: z
|
|
158
|
+
referenceCode: z
|
|
159
|
+
.string()
|
|
160
|
+
.describe("Remittance text as received - free-form is fine; matching normalizes it"),
|
|
115
161
|
amount: z.number(),
|
|
116
162
|
currency: z.string(),
|
|
117
163
|
remitterName: z.string().optional(),
|
|
@@ -132,6 +178,24 @@ export function registerReadTools(server, client) {
|
|
|
132
178
|
return errorResult(e.message);
|
|
133
179
|
}
|
|
134
180
|
});
|
|
181
|
+
server.registerTool("list_transfers", {
|
|
182
|
+
title: "List transfers",
|
|
183
|
+
description: "List transfer history for an account (finance GET /accounts/{accountId}/transfers). Read-only.",
|
|
184
|
+
inputSchema: {
|
|
185
|
+
accountId: z.string().describe("Account UUID"),
|
|
186
|
+
page: z.number().int().min(1).optional(),
|
|
187
|
+
size: z.number().int().min(1).max(200).optional(),
|
|
188
|
+
},
|
|
189
|
+
annotations: READ_ONLY,
|
|
190
|
+
}, async ({ accountId, page, size }) => {
|
|
191
|
+
try {
|
|
192
|
+
const result = await client.listTransfers(accountId, { page, size });
|
|
193
|
+
return jsonResult({ count: result.length, transfers: result });
|
|
194
|
+
}
|
|
195
|
+
catch (e) {
|
|
196
|
+
return errorResult(e.message);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
135
199
|
server.registerTool("get_transfer", {
|
|
136
200
|
title: "Get transfer",
|
|
137
201
|
description: "Fetch a transfer by id (finance GET /accounts/{accountId}/transfers/{transferId}). Read-only.",
|
|
@@ -165,6 +229,19 @@ export function registerReadTools(server, client) {
|
|
|
165
229
|
return errorResult(e.message);
|
|
166
230
|
}
|
|
167
231
|
});
|
|
232
|
+
server.registerTool("get_party", {
|
|
233
|
+
title: "Get party",
|
|
234
|
+
description: "Fetch an individual or organisation party, including KYC/KYB state when present. Read-only.",
|
|
235
|
+
inputSchema: { partyId: z.string().describe("Party UUID") },
|
|
236
|
+
annotations: READ_ONLY,
|
|
237
|
+
}, async ({ partyId }) => {
|
|
238
|
+
try {
|
|
239
|
+
return jsonResult(await client.getParty(partyId));
|
|
240
|
+
}
|
|
241
|
+
catch (e) {
|
|
242
|
+
return errorResult(e.message);
|
|
243
|
+
}
|
|
244
|
+
});
|
|
168
245
|
server.registerTool("get_reference_data", {
|
|
169
246
|
title: "Get reference data",
|
|
170
247
|
description: "Fetch settlement reference data: supported chains, fiat currencies, " +
|
|
@@ -8,16 +8,17 @@
|
|
|
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
|
-
|
|
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
|
+
// Explicit on every mutation result: this call DID execute (against local
|
|
17
|
+
// fixtures in mock, against the real API when armed).
|
|
18
|
+
dryRun: false,
|
|
19
|
+
environment: gate.environment,
|
|
20
|
+
result,
|
|
21
|
+
});
|
|
21
22
|
}
|
|
22
23
|
const WRITE_ANNOTATIONS = {
|
|
23
24
|
readOnlyHint: false,
|
|
@@ -30,18 +31,230 @@ const confirmField = z
|
|
|
30
31
|
.default(false)
|
|
31
32
|
.describe("Must be true to attempt a live call. Even then, VENLY_MCP_LIVE=1 and " +
|
|
32
33
|
"credentials are also required, otherwise the tool dry-runs.");
|
|
34
|
+
const addressSchema = z
|
|
35
|
+
.object({
|
|
36
|
+
addressLine1: z.string().optional(),
|
|
37
|
+
addressLine2: z.string().optional(),
|
|
38
|
+
city: z.string().optional(),
|
|
39
|
+
state: z.string().optional(),
|
|
40
|
+
postalCode: z.string().optional(),
|
|
41
|
+
country: z.string().regex(/^[A-Z]{2}$/).optional(),
|
|
42
|
+
})
|
|
43
|
+
.optional();
|
|
44
|
+
const partySchema = z.object({
|
|
45
|
+
partyType: z.enum(["INDIVIDUAL", "ORGANISATION"]),
|
|
46
|
+
externalId: z.string().optional(),
|
|
47
|
+
firstName: z.string().optional(),
|
|
48
|
+
lastName: z.string().optional(),
|
|
49
|
+
name: z.string().optional(),
|
|
50
|
+
vatNumber: z.string().optional(),
|
|
51
|
+
address: addressSchema,
|
|
52
|
+
});
|
|
53
|
+
function partyValidationError(party) {
|
|
54
|
+
if (party.partyType === "INDIVIDUAL" && (!party.firstName || !party.lastName)) {
|
|
55
|
+
return "INDIVIDUAL parties require firstName and lastName";
|
|
56
|
+
}
|
|
57
|
+
if (party.partyType === "ORGANISATION" && !party.name) {
|
|
58
|
+
return "ORGANISATION parties require name";
|
|
59
|
+
}
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
33
62
|
export function registerWriteTools(server, client, env) {
|
|
63
|
+
server.registerTool("create_party", {
|
|
64
|
+
title: "Create a customer or organisation party",
|
|
65
|
+
description: "Create a Finance party. This creates the party record; it does not complete KYC/KYB. Dry-run by default outside explicit mock mode.",
|
|
66
|
+
inputSchema: {
|
|
67
|
+
partyType: z.enum(["INDIVIDUAL", "ORGANISATION"]),
|
|
68
|
+
externalId: z.string().optional(),
|
|
69
|
+
firstName: z.string().optional(),
|
|
70
|
+
lastName: z.string().optional(),
|
|
71
|
+
name: z.string().optional(),
|
|
72
|
+
vatNumber: z.string().optional(),
|
|
73
|
+
address: addressSchema,
|
|
74
|
+
confirm: confirmField,
|
|
75
|
+
},
|
|
76
|
+
annotations: WRITE_ANNOTATIONS,
|
|
77
|
+
}, async ({ confirm, ...party }) => {
|
|
78
|
+
const validationError = partyValidationError(party);
|
|
79
|
+
if (validationError)
|
|
80
|
+
return errorResult(validationError);
|
|
81
|
+
const gate = evaluateWriteGate(confirm, env);
|
|
82
|
+
if (!gate.armed) {
|
|
83
|
+
return jsonResult(buildDryRun("create_party", "POST", "finance", "/parties", party, gate));
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
return executionResult(gate, await client.createParty(party));
|
|
87
|
+
}
|
|
88
|
+
catch (e) {
|
|
89
|
+
return errorResult(e.message);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
server.registerTool("create_account", {
|
|
93
|
+
title: "Create an account and provision its wallet",
|
|
94
|
+
description: "Create a Finance account. Venly auto-provisions its wallet on the selected chain. Supply partyId or an inline party. Dry-run by default.",
|
|
95
|
+
inputSchema: {
|
|
96
|
+
externalId: z.string().min(1),
|
|
97
|
+
name: z.string().optional(),
|
|
98
|
+
chain: z.enum(["AVALANCHE", "BASE", "POLYGON"]),
|
|
99
|
+
address: z.string().optional().describe("Required for SELF_CUSTODY tenants"),
|
|
100
|
+
partyId: z.string().optional(),
|
|
101
|
+
party: partySchema.optional(),
|
|
102
|
+
confirm: confirmField,
|
|
103
|
+
},
|
|
104
|
+
annotations: WRITE_ANNOTATIONS,
|
|
105
|
+
}, async ({ confirm, ...body }) => {
|
|
106
|
+
if (!body.partyId && !body.party) {
|
|
107
|
+
return errorResult("create_account requires partyId or an inline party");
|
|
108
|
+
}
|
|
109
|
+
if (body.party) {
|
|
110
|
+
const validationError = partyValidationError(body.party);
|
|
111
|
+
if (validationError)
|
|
112
|
+
return errorResult(validationError);
|
|
113
|
+
}
|
|
114
|
+
const gate = evaluateWriteGate(confirm, env);
|
|
115
|
+
if (!gate.armed) {
|
|
116
|
+
return jsonResult(buildDryRun("create_account", "POST", "finance", "/accounts", body, gate));
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
return executionResult(gate, await client.createAccount(body));
|
|
120
|
+
}
|
|
121
|
+
catch (e) {
|
|
122
|
+
return errorResult(e.message);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
server.registerTool("create_virtual_bank_account", {
|
|
126
|
+
title: "Create a EUR receiving account",
|
|
127
|
+
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.",
|
|
128
|
+
inputSchema: {
|
|
129
|
+
accountId: z.string(),
|
|
130
|
+
name: z.string().min(1),
|
|
131
|
+
inCurrency: z.literal("EUR"),
|
|
132
|
+
targetCryptocurrency: z.enum(["USDC", "EURC", "USDT", "USDS"]),
|
|
133
|
+
idempotencyKey: z.string().min(1).optional(),
|
|
134
|
+
confirm: confirmField,
|
|
135
|
+
},
|
|
136
|
+
annotations: WRITE_ANNOTATIONS,
|
|
137
|
+
}, async ({ accountId, confirm, ...input }) => {
|
|
138
|
+
const body = {
|
|
139
|
+
...input,
|
|
140
|
+
idempotencyKey: input.idempotencyKey ?? crypto.randomUUID(),
|
|
141
|
+
};
|
|
142
|
+
const gate = evaluateWriteGate(confirm, env);
|
|
143
|
+
if (!gate.armed) {
|
|
144
|
+
const dryRun = buildDryRun("create_virtual_bank_account", "POST", "finance", `/accounts/${accountId}/virtual-bank-accounts`, body, gate);
|
|
145
|
+
dryRun.note += " The account must have KYC status VERIFIED before live provisioning.";
|
|
146
|
+
return jsonResult(dryRun);
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
return executionResult(gate, await client.createVirtualBankAccount(accountId, body));
|
|
150
|
+
}
|
|
151
|
+
catch (e) {
|
|
152
|
+
return errorResult(e.message);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
server.registerTool("create_fiat_transfer", {
|
|
156
|
+
title: "Create a fiat-denominated internal transfer",
|
|
157
|
+
description: "Create an account-to-account transfer using the current Finance OpenAPI fields. Dry-run by default outside explicit mock mode.",
|
|
158
|
+
inputSchema: {
|
|
159
|
+
senderAccountId: z.string(),
|
|
160
|
+
receiverAccountId: z
|
|
161
|
+
.string()
|
|
162
|
+
.optional()
|
|
163
|
+
.describe("Receiver's Venly account id. Exactly one of receiverAccountId / receiverExternalId is required."),
|
|
164
|
+
receiverExternalId: z
|
|
165
|
+
.string()
|
|
166
|
+
.optional()
|
|
167
|
+
.describe("Receiver's integrator-assigned externalId. Exactly one of receiverAccountId / receiverExternalId is required."),
|
|
168
|
+
currency: z.enum(["EUR", "GBP", "USD"]),
|
|
169
|
+
amount: z.number(),
|
|
170
|
+
description: z.string().optional(),
|
|
171
|
+
merchantReference: z.string().optional(),
|
|
172
|
+
idempotencyKey: z.string().min(1).optional(),
|
|
173
|
+
confirm: confirmField,
|
|
174
|
+
},
|
|
175
|
+
annotations: WRITE_ANNOTATIONS,
|
|
176
|
+
}, async ({ senderAccountId, confirm, ...input }) => {
|
|
177
|
+
if (!input.receiverAccountId === !input.receiverExternalId) {
|
|
178
|
+
return errorResult("Provide exactly one of receiverAccountId or receiverExternalId - a transfer needs one receiver, addressed one way.");
|
|
179
|
+
}
|
|
180
|
+
const body = {
|
|
181
|
+
...input,
|
|
182
|
+
idempotencyKey: input.idempotencyKey ?? crypto.randomUUID(),
|
|
183
|
+
};
|
|
184
|
+
const gate = evaluateWriteGate(confirm, env);
|
|
185
|
+
if (!gate.armed) {
|
|
186
|
+
return jsonResult(buildDryRun("create_fiat_transfer", "POST", "finance", `/accounts/${senderAccountId}/transfers/fiat`, body, gate));
|
|
187
|
+
}
|
|
188
|
+
try {
|
|
189
|
+
return executionResult(gate, await client.createCurrentFiatTransfer(senderAccountId, body));
|
|
190
|
+
}
|
|
191
|
+
catch (e) {
|
|
192
|
+
return errorResult(e.message);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
server.registerTool("create_crypto_transfer", {
|
|
196
|
+
title: "Create a crypto-denominated internal transfer",
|
|
197
|
+
description: "Create an account-to-account asset transfer using the current Finance OpenAPI fields. Dry-run by default outside explicit mock mode.",
|
|
198
|
+
inputSchema: {
|
|
199
|
+
senderAccountId: z.string(),
|
|
200
|
+
receiverAccountId: z
|
|
201
|
+
.string()
|
|
202
|
+
.optional()
|
|
203
|
+
.describe("Receiver's Venly account id. Exactly one of receiverAccountId / receiverExternalId is required."),
|
|
204
|
+
receiverExternalId: z
|
|
205
|
+
.string()
|
|
206
|
+
.optional()
|
|
207
|
+
.describe("Receiver's integrator-assigned externalId. Exactly one of receiverAccountId / receiverExternalId is required."),
|
|
208
|
+
chain: z.enum(["AVALANCHE", "BASE", "POLYGON"]),
|
|
209
|
+
asset: z.string().min(1),
|
|
210
|
+
amount: z.number(),
|
|
211
|
+
description: z.string().optional(),
|
|
212
|
+
merchantReference: z.string().optional(),
|
|
213
|
+
idempotencyKey: z.string().min(1).optional(),
|
|
214
|
+
confirm: confirmField,
|
|
215
|
+
},
|
|
216
|
+
annotations: WRITE_ANNOTATIONS,
|
|
217
|
+
}, async ({ senderAccountId, confirm, ...input }) => {
|
|
218
|
+
if (!input.receiverAccountId === !input.receiverExternalId) {
|
|
219
|
+
return errorResult("Provide exactly one of receiverAccountId or receiverExternalId - a transfer needs one receiver, addressed one way.");
|
|
220
|
+
}
|
|
221
|
+
const body = {
|
|
222
|
+
...input,
|
|
223
|
+
idempotencyKey: input.idempotencyKey ?? crypto.randomUUID(),
|
|
224
|
+
};
|
|
225
|
+
const gate = evaluateWriteGate(confirm, env);
|
|
226
|
+
if (!gate.armed) {
|
|
227
|
+
return jsonResult(buildDryRun("create_crypto_transfer", "POST", "finance", `/accounts/${senderAccountId}/transfers/crypto`, body, gate));
|
|
228
|
+
}
|
|
229
|
+
try {
|
|
230
|
+
return executionResult(gate, await client.createCryptoTransfer(senderAccountId, body));
|
|
231
|
+
}
|
|
232
|
+
catch (e) {
|
|
233
|
+
return errorResult(e.message);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
34
236
|
server.registerTool("stage_transfer", {
|
|
35
|
-
title: "
|
|
36
|
-
description: "
|
|
37
|
-
"
|
|
38
|
-
"
|
|
237
|
+
title: "DEPRECATED - use create_fiat_transfer",
|
|
238
|
+
description: "DEPRECATED: legacy alias of create_fiat_transfer kept for 0.1.x compatibility; " +
|
|
239
|
+
"it will be removed in 0.4.0. Prefer create_fiat_transfer, whose inputs match the " +
|
|
240
|
+
"current OpenAPI contract directly. " +
|
|
241
|
+
"Stages a fiat-to-crypto transfer (finance POST /accounts/{senderAccountId}/transfers/fiat). " +
|
|
242
|
+
"Legacy fiatAmount/fiatCurrency inputs are normalized to the current OpenAPI fields; " +
|
|
243
|
+
"the dry-run shows the exact normalized request. DISARMED by default: returns that " +
|
|
244
|
+
"request without sending unless confirm:true AND VENLY_MCP_LIVE=1 AND credentials are present.",
|
|
39
245
|
inputSchema: {
|
|
40
246
|
senderAccountId: z.string().describe("Account initiating the transfer"),
|
|
41
247
|
receiverAccountId: z.string(),
|
|
42
|
-
fiatAmount: z
|
|
248
|
+
fiatAmount: z
|
|
249
|
+
.string()
|
|
250
|
+
.refine((value) => value.trim() !== "" && Number.isFinite(Number(value)), "fiatAmount must be a numeric decimal string")
|
|
251
|
+
.describe("Decimal string, e.g. \"1000.00\""),
|
|
43
252
|
fiatCurrency: z.string().describe("e.g. EUR"),
|
|
44
|
-
cryptocurrency: z
|
|
253
|
+
cryptocurrency: z
|
|
254
|
+
.string()
|
|
255
|
+
.optional()
|
|
256
|
+
.describe("Retired: rejected with guidance. The current contract resolves the fiat " +
|
|
257
|
+
"amount to the account's settlement asset; use create_crypto_transfer instead."),
|
|
45
258
|
description: z.string().optional(),
|
|
46
259
|
merchantReference: z.string().optional(),
|
|
47
260
|
confirm: confirmField,
|
|
@@ -49,20 +262,31 @@ export function registerWriteTools(server, client, env) {
|
|
|
49
262
|
annotations: WRITE_ANNOTATIONS,
|
|
50
263
|
}, async ({ senderAccountId, confirm, ...rest }) => {
|
|
51
264
|
const gate = evaluateWriteGate(confirm, env);
|
|
52
|
-
const
|
|
265
|
+
const legacyInput = {
|
|
53
266
|
receiverAccountId: rest.receiverAccountId,
|
|
54
267
|
fiatAmount: rest.fiatAmount,
|
|
55
268
|
fiatCurrency: rest.fiatCurrency,
|
|
56
269
|
cryptocurrency: rest.cryptocurrency,
|
|
57
270
|
description: rest.description,
|
|
58
271
|
merchantReference: rest.merchantReference,
|
|
272
|
+
idempotencyKey: crypto.randomUUID(),
|
|
59
273
|
};
|
|
274
|
+
// Normalize BEFORE the gate branch so the dry-run preview is byte-for-byte
|
|
275
|
+
// the request a live call would send (and the retired cryptocurrency field
|
|
276
|
+
// is rejected instead of silently dropped).
|
|
277
|
+
let body;
|
|
278
|
+
try {
|
|
279
|
+
body = normalizeLegacyFiatTransfer(legacyInput);
|
|
280
|
+
}
|
|
281
|
+
catch (e) {
|
|
282
|
+
return errorResult(e.message);
|
|
283
|
+
}
|
|
60
284
|
if (!gate.armed) {
|
|
61
285
|
return jsonResult(buildDryRun("stage_transfer", "POST", "finance", `/accounts/${senderAccountId}/transfers/fiat`, body, gate));
|
|
62
286
|
}
|
|
63
287
|
try {
|
|
64
|
-
const result = await client.createFiatTransfer(senderAccountId,
|
|
65
|
-
return
|
|
288
|
+
const result = await client.createFiatTransfer(senderAccountId, legacyInput);
|
|
289
|
+
return executionResult(gate, result);
|
|
66
290
|
}
|
|
67
291
|
catch (e) {
|
|
68
292
|
return errorResult(e.message);
|
|
@@ -91,7 +315,7 @@ export function registerWriteTools(server, client, env) {
|
|
|
91
315
|
}
|
|
92
316
|
try {
|
|
93
317
|
const result = await client.approveRampRequest(id, body);
|
|
94
|
-
return
|
|
318
|
+
return executionResult(gate, result);
|
|
95
319
|
}
|
|
96
320
|
catch (e) {
|
|
97
321
|
return errorResult(e.message);
|
|
@@ -118,7 +342,7 @@ export function registerWriteTools(server, client, env) {
|
|
|
118
342
|
}
|
|
119
343
|
try {
|
|
120
344
|
const result = await client.rejectRampRequest(id, body);
|
|
121
|
-
return
|
|
345
|
+
return executionResult(gate, result);
|
|
122
346
|
}
|
|
123
347
|
catch (e) {
|
|
124
348
|
return errorResult(e.message);
|
|
@@ -162,7 +386,7 @@ export function registerWriteTools(server, client, env) {
|
|
|
162
386
|
}
|
|
163
387
|
try {
|
|
164
388
|
const result = await client.createPayInSession(accountId, body);
|
|
165
|
-
return
|
|
389
|
+
return executionResult(gate, result);
|
|
166
390
|
}
|
|
167
391
|
catch (e) {
|
|
168
392
|
return errorResult(e.message);
|