@avvio/payments 0.1.0 → 0.5.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/src/mcp.js CHANGED
@@ -34,8 +34,16 @@ const TOOLS = [
34
34
  name: 'list_corridors',
35
35
  annotations: { readOnlyHint: true },
36
36
  description:
37
- 'List every currency this organization can pay out to, with the beneficiary fields each one requires. Read this instead of hardcoding a form — field names differ by corridor and can change.',
38
- inputSchema: { type: 'object', properties: {} },
37
+ 'List every currency this organization can pay out to, or pass one currency to use its exact approved provider routing. Read this instead of hardcoding a form — field names differ by corridor and can change.',
38
+ inputSchema: {
39
+ type: 'object',
40
+ properties: {
41
+ currency: {
42
+ type: 'string',
43
+ description: 'Optional ISO code, e.g. MXN',
44
+ },
45
+ },
46
+ },
39
47
  },
40
48
  {
41
49
  name: 'get_requirements',
@@ -58,9 +66,15 @@ const TOOLS = [
58
66
  inputSchema: {
59
67
  type: 'object',
60
68
  properties: {
61
- amount: { type: 'string', description: 'Amount to send, e.g. "200.00"' },
69
+ amount: {
70
+ type: 'string',
71
+ description: 'Amount to send, e.g. "200.00"',
72
+ },
62
73
  to: { type: 'string', description: 'Destination currency, e.g. MXN' },
63
- from: { type: 'string', description: 'Source currency. Defaults to USD.' },
74
+ from: {
75
+ type: 'string',
76
+ description: 'Source currency. Defaults to USD.',
77
+ },
64
78
  },
65
79
  required: ['amount', 'to'],
66
80
  },
@@ -82,11 +96,13 @@ const TOOLS = [
82
96
  },
83
97
  externalId: {
84
98
  type: 'string',
85
- description: 'Your own id for this beneficiary. Makes creation idempotent.',
99
+ description:
100
+ 'Your own id for this beneficiary. Makes creation idempotent.',
86
101
  },
87
102
  details: {
88
103
  type: 'object',
89
- description: 'Corridor fields from get_requirements, e.g. {"clabeNumber":"012..."}',
104
+ description:
105
+ 'Corridor fields from get_requirements, e.g. {"clabeNumber":"012..."}',
90
106
  },
91
107
  },
92
108
  required: ['name', 'currency', 'details'],
@@ -102,6 +118,80 @@ const TOOLS = [
102
118
  properties: { endUserId: { type: 'string' } },
103
119
  },
104
120
  },
121
+ {
122
+ name: 'get_beneficiary',
123
+ annotations: { readOnlyHint: true },
124
+ description:
125
+ 'One beneficiary by the id this API returned, with their payment methods and each destinationAccountId.',
126
+ inputSchema: {
127
+ type: 'object',
128
+ properties: { recipientId: { type: 'string' } },
129
+ required: ['recipientId'],
130
+ },
131
+ },
132
+ {
133
+ name: 'get_beneficiary_by_external_id',
134
+ annotations: { readOnlyHint: true },
135
+ description:
136
+ 'One beneficiary by YOUR own id for them — the externalId you sent when you created them. Use this instead of listing everyone and filtering: it is unique per organization, so it returns exactly one or NOT_FOUND.',
137
+ inputSchema: {
138
+ type: 'object',
139
+ properties: {
140
+ externalId: {
141
+ type: 'string',
142
+ description: 'Your id for this beneficiary, e.g. "payroll-4471".',
143
+ },
144
+ },
145
+ required: ['externalId'],
146
+ },
147
+ },
148
+ {
149
+ name: 'update_beneficiary',
150
+ annotations: { readOnlyHint: false },
151
+ description:
152
+ 'Correct a beneficiary’s contact details: name, email, phone, country, individual/business. Only the fields you pass change. BANK DETAILS CANNOT BE EDITED — a wrong account is a new payment method, and the old one is deleted.',
153
+ inputSchema: {
154
+ type: 'object',
155
+ properties: {
156
+ recipientId: { type: 'string' },
157
+ name: { type: 'string' },
158
+ email: { type: 'string' },
159
+ phone: { type: 'string' },
160
+ country: { type: 'string', description: 'ISO-3166 alpha-2, e.g. MX' },
161
+ type: { type: 'string', enum: ['individual', 'business'] },
162
+ },
163
+ required: ['recipientId'],
164
+ },
165
+ },
166
+ {
167
+ name: 'delete_beneficiary_method',
168
+ annotations: { destructiveHint: true, readOnlyHint: false },
169
+ description:
170
+ 'Remove one way of paying a beneficiary — an account that closed, or one entered wrong. Irreversible, and the destinationAccountId it carried stops being payable, so it requires confirm:true. The beneficiary and their other methods are untouched.',
171
+ inputSchema: {
172
+ type: 'object',
173
+ properties: {
174
+ recipientId: { type: 'string' },
175
+ methodId: {
176
+ type: 'string',
177
+ description: 'From the beneficiary’s paymentMethods[].id.',
178
+ },
179
+ confirm: {
180
+ type: 'boolean',
181
+ description:
182
+ 'Must be true. The account cannot be restored, only re-registered.',
183
+ },
184
+ },
185
+ required: ['recipientId', 'methodId', 'confirm'],
186
+ },
187
+ },
188
+ {
189
+ name: 'list_payment_reasons',
190
+ annotations: { readOnlyHint: true },
191
+ description:
192
+ 'The stated payment reasons this organization may use. Some corridors require one on a payout; read this rather than inventing a value, because a rejected one is a 400 on a payout already promised to somebody.',
193
+ inputSchema: { type: 'object', properties: {} },
194
+ },
105
195
  {
106
196
  name: 'send_payout',
107
197
  annotations: { destructiveHint: true, readOnlyHint: false },
@@ -124,6 +214,12 @@ const TOOLS = [
124
214
  description:
125
215
  'What you told the payer they would receive. The send is refused if the binding quote drifts more than 2% from it.',
126
216
  },
217
+ amountLeg: {
218
+ type: 'string',
219
+ enum: ['source', 'destination', 'source_net'],
220
+ description:
221
+ "Which side `amount` describes. 'source' (default) takes the fees out of what you send. 'destination' pays the beneficiary that exact figure in THEIR currency and adds the fees to your debit. 'source_net' means the same but keeps the figure in the sender's currency — 'send them $200 worth' — converted at the market rate. The two locking modes need capabilities.exactOutput from list_corridors.",
222
+ },
127
223
  idempotencyKey: {
128
224
  type: 'string',
129
225
  description:
@@ -131,18 +227,14 @@ const TOOLS = [
131
227
  },
132
228
  confirm: {
133
229
  type: 'boolean',
134
- description: 'Must be true. Explicit acknowledgement that this moves money.',
230
+ description:
231
+ 'Must be true. Explicit acknowledgement that this moves money.',
135
232
  },
136
233
  },
137
234
  // idempotencyKey is required because a timeout is the case this tool is
138
235
  // most likely to hit, and retrying is what an agent does by reflex.
139
236
  // Without a caller-supplied key that retry is a second payment.
140
- required: [
141
- 'amount',
142
- 'destinationAccountId',
143
- 'confirm',
144
- 'idempotencyKey',
145
- ],
237
+ required: ['amount', 'destinationAccountId', 'confirm', 'idempotencyKey'],
146
238
  },
147
239
  },
148
240
  {
@@ -173,16 +265,77 @@ const TOOLS = [
173
265
  name: 'list_events',
174
266
  annotations: { readOnlyHint: true },
175
267
  description:
176
- 'The change feed: one row per payout transition, with a `sequence` cursor. This is the reconciliation primitive — carry `nextSince` back as `since` and you observe every revision. A payout that completed and was then returned by the bank appears here as a second row.',
268
+ 'The change feed: one row per transition, with a `sequence` cursor. This is the reconciliation primitive — carry `nextSince` back as `since` and you observe every revision. Each row\u2019s `data` is the webhook body for the same event (amounts, fee, reference). Payout, batch, approval and endpoint events share the feed; pass `type` to narrow it.',
177
269
  inputSchema: {
178
270
  type: 'object',
179
271
  properties: {
180
- since: { type: 'string', description: 'A `sequence` from a previous page. Digits only.' },
272
+ since: {
273
+ type: 'string',
274
+ description: 'A `sequence` from a previous page. Digits only.',
275
+ },
181
276
  limit: { type: 'number', description: '1-500, default 100.' },
182
- payoutId: { type: 'string', description: 'Only this payout\u2019s transitions.' },
277
+ payoutId: {
278
+ type: 'string',
279
+ description: 'Only this payout\u2019s transitions.',
280
+ },
281
+ type: {
282
+ type: 'string',
283
+ description:
284
+ 'Comma-separated event types, e.g. payout.completed,payout.failed,payout.returned. Unknown values are a 400.',
285
+ },
286
+ },
287
+ },
288
+ },
289
+ {
290
+ name: 'list_approvals',
291
+ annotations: { readOnlyHint: true },
292
+ description:
293
+ 'Payouts and batch runs waiting on the organization\u2019s human approvers. send_payout answers 202 with `approvalId` when a policy holds it; this is that queue. Approving is a dashboard action — there is no tool for it, by design.',
294
+ inputSchema: {
295
+ type: 'object',
296
+ properties: {
297
+ status: { type: 'string', description: 'pending, approved, rejected, expired, executing, executed, execution_failed, execution_unknown.' },
298
+ limit: { type: 'number', description: '1-100, default 50.' },
183
299
  },
184
300
  },
185
301
  },
302
+ {
303
+ name: 'get_approval',
304
+ annotations: { readOnlyHint: true },
305
+ description:
306
+ 'One approval by the `approvalId` a 202 returned. Once `status` is `executed`, `payoutId` is the payout it became.',
307
+ inputSchema: {
308
+ type: 'object',
309
+ properties: { approvalId: { type: 'string' } },
310
+ required: ['approvalId'],
311
+ },
312
+ },
313
+ {
314
+ name: 'list_audit_events',
315
+ annotations: { readOnlyHint: true },
316
+ description:
317
+ 'Who did what, from where, with which credential: every audited mutation on the organization, newest first, refused attempts included. Rows carry the key prefix, actor, IP, requestId, outcome and errorType. Page with `cursor` = the `nextCursor` from the last page.',
318
+ inputSchema: {
319
+ type: 'object',
320
+ properties: {
321
+ cursor: { type: 'string', description: 'A `nextCursor` from a previous page. Digits only.' },
322
+ limit: { type: 'number', description: '1-100, default 50.' },
323
+ action: { type: 'string', description: 'e.g. payout.create, payout_batch.confirm, recipient.delete, api_key.rotate.' },
324
+ resourceId: { type: 'string', description: 'Everything done to one payout, batch, approval, beneficiary, key or endpoint.' },
325
+ apiKey: { type: 'string', description: 'A key prefix.' },
326
+ actorUserId: { type: 'string' },
327
+ createdAfter: { type: 'string', description: 'Inclusive, ISO-8601 with a timezone.' },
328
+ createdBefore: { type: 'string', description: 'Inclusive, ISO-8601 with a timezone.' },
329
+ },
330
+ },
331
+ },
332
+ {
333
+ name: 'get_policy',
334
+ annotations: { readOnlyHint: true },
335
+ description:
336
+ 'What this organization is bound by, read live. Call it FIRST: payout caps in USD (null = no cap), the approval threshold and how many approvers a held payout needs, which features are on (mass_payouts, developer), rate limits per minute, idempotency windows, the currencies that require purposeOfPayment, and fees.payout (the pre-quote fee schedule; null = only priced inside a quote). Plan sends against it instead of discovering a cap from a 422 or an approval from a 202.',
337
+ inputSchema: { type: 'object', properties: {} },
338
+ },
186
339
  {
187
340
  name: 'get_balance',
188
341
  annotations: { readOnlyHint: true },
@@ -190,6 +343,24 @@ const TOOLS = [
190
343
  'The organization\u2019s available balance. Check this before sending: an underfunded payout is refused, and the refusal is a 400 rather than a queued payment.',
191
344
  inputSchema: { type: 'object', properties: {} },
192
345
  },
346
+ {
347
+ name: 'list_balance_transactions',
348
+ annotations: { readOnlyHint: true },
349
+ description:
350
+ 'Every change to the balance, newest first, each with `balanceAfter`: funding, payouts, returns, holds and their release, adjustments. Page with `cursor` = the `nextCursor` from the last page. An empty page means no rows yet, not an error.',
351
+ inputSchema: {
352
+ type: 'object',
353
+ properties: {
354
+ cursor: { type: 'string', description: 'A `nextCursor` from a previous page. Digits only.' },
355
+ limit: { type: 'number', description: '1-100, default 100.' },
356
+ type: { type: 'string', description: 'Comma-separated: funding, payout, payout_return, hold, hold_release, adjustment.' },
357
+ orderId: { type: 'string', description: 'Everything that moved for one payout.' },
358
+ currency: { type: 'string' },
359
+ createdAfter: { type: 'string', description: 'Inclusive, ISO-8601 with a timezone.' },
360
+ createdBefore: { type: 'string', description: 'Inclusive, ISO-8601 with a timezone.' },
361
+ },
362
+ },
363
+ },
193
364
  {
194
365
  name: 'get_funding',
195
366
  annotations: { readOnlyHint: true },
@@ -209,11 +380,23 @@ const TOOLS = [
209
380
  inputSchema: {
210
381
  type: 'object',
211
382
  properties: {
212
- amount: { type: 'string', description: 'Decimal string, e.g. "200.00".' },
213
- destinationCurrency: { type: 'string', description: 'ISO code, e.g. MXN' },
214
- endUserId: { type: 'string', description: 'Your id for the person being paid.' },
383
+ amount: {
384
+ type: 'string',
385
+ description: 'Decimal string, e.g. "200.00".',
386
+ },
387
+ destinationCurrency: {
388
+ type: 'string',
389
+ description: 'ISO code, e.g. MXN',
390
+ },
391
+ endUserId: {
392
+ type: 'string',
393
+ description: 'Your id for the person being paid.',
394
+ },
215
395
  reference: { type: 'string' },
216
- expiresInMinutes: { type: 'number', description: '1-10080, default 60.' },
396
+ expiresInMinutes: {
397
+ type: 'number',
398
+ description: '1-10080, default 60.',
399
+ },
217
400
  },
218
401
  required: ['amount', 'destinationCurrency', 'endUserId'],
219
402
  },
@@ -227,10 +410,14 @@ const TOOLS = [
227
410
  type: 'object',
228
411
  properties: {
229
412
  payoutId: { type: 'string' },
230
- transactionHash: { type: 'string', description: '0x-prefixed 32-byte hex.' },
413
+ transactionHash: {
414
+ type: 'string',
415
+ description: '0x-prefixed 32-byte hex.',
416
+ },
231
417
  confirm: {
232
418
  type: 'boolean',
233
- description: 'Must be true. This commits funds you have already sent.',
419
+ description:
420
+ 'Must be true. This commits funds you have already sent.',
234
421
  },
235
422
  },
236
423
  required: ['payoutId', 'transactionHash', 'confirm'],
@@ -258,15 +445,52 @@ const TOOLS = [
258
445
  async function dispatch(client, name, args) {
259
446
  switch (name) {
260
447
  case 'list_corridors':
261
- return client.corridors();
448
+ return client.corridors(args.currency);
262
449
  case 'get_requirements':
263
450
  return client.requirements(args.currency);
264
451
  case 'quote':
265
- return client.quote({ amount: args.amount, to: args.to, from: args.from });
452
+ return client.quote({
453
+ amount: args.amount,
454
+ to: args.to,
455
+ from: args.from,
456
+ });
266
457
  case 'create_beneficiary':
267
458
  return client.createBeneficiary(args);
268
459
  case 'list_beneficiaries':
269
460
  return client.listBeneficiaries({ endUserId: args.endUserId });
461
+ case 'get_beneficiary':
462
+ return client.getBeneficiary(args.recipientId);
463
+ case 'get_beneficiary_by_external_id':
464
+ return client.getBeneficiaryByExternalId(args.externalId);
465
+ case 'update_beneficiary':
466
+ // An empty patch is a round trip that changes nothing and reads back as
467
+ // success — which an agent will report as "done". Name the editable
468
+ // fields instead, since the one it probably wanted is not among them.
469
+ if (
470
+ !['name', 'email', 'phone', 'country', 'type'].some(
471
+ (f) => args[f] !== undefined,
472
+ )
473
+ ) {
474
+ throw new Error(
475
+ 'update_beneficiary needs at least one of name, email, phone, country, type. Bank details are not editable: register a new payment method and delete the old one.',
476
+ );
477
+ }
478
+ return client.updateBeneficiary(args.recipientId, {
479
+ name: args.name,
480
+ email: args.email,
481
+ phone: args.phone,
482
+ country: args.country,
483
+ type: args.type,
484
+ });
485
+ case 'delete_beneficiary_method':
486
+ if (args.confirm !== true) {
487
+ throw new Error(
488
+ 'delete_beneficiary_method is irreversible and requires confirm:true. The account can only be re-registered, not restored — check with whoever owns this beneficiary first.',
489
+ );
490
+ }
491
+ return client.deleteBeneficiaryMethod(args.recipientId, args.methodId);
492
+ case 'list_payment_reasons':
493
+ return client.paymentReasons();
270
494
  case 'send_payout':
271
495
  if (args.confirm !== true) {
272
496
  throw new Error(
@@ -276,6 +500,7 @@ async function dispatch(client, name, args) {
276
500
  return client.payout({
277
501
  amount: args.amount,
278
502
  destinationAccountId: args.destinationAccountId,
503
+ amountLeg: args.amountLeg,
279
504
  purposeOfPayment: args.purposeOfPayment,
280
505
  reference: args.reference,
281
506
  expectDestinationAmount: args.expectDestinationAmount,
@@ -295,9 +520,37 @@ async function dispatch(client, name, args) {
295
520
  since: args.since,
296
521
  limit: args.limit,
297
522
  payoutId: args.payoutId,
523
+ type: args.type,
298
524
  });
525
+ case 'list_approvals':
526
+ return client.listApprovals({ status: args.status, limit: args.limit });
527
+ case 'get_approval':
528
+ return client.getApproval(args.approvalId);
529
+ case 'list_audit_events':
530
+ return client.listAuditEvents({
531
+ cursor: args.cursor,
532
+ limit: args.limit,
533
+ action: args.action,
534
+ resourceId: args.resourceId,
535
+ apiKey: args.apiKey,
536
+ actorUserId: args.actorUserId,
537
+ createdAfter: args.createdAfter,
538
+ createdBefore: args.createdBefore,
539
+ });
540
+ case 'get_policy':
541
+ return client.getPolicy();
299
542
  case 'get_balance':
300
543
  return client.balance();
544
+ case 'list_balance_transactions':
545
+ return client.listBalanceTransactions({
546
+ cursor: args.cursor,
547
+ limit: args.limit,
548
+ type: args.type,
549
+ orderId: args.orderId,
550
+ currency: args.currency,
551
+ createdAfter: args.createdAfter,
552
+ createdBefore: args.createdBefore,
553
+ });
301
554
  case 'get_funding':
302
555
  return client.getFunding(args.payoutId);
303
556
  case 'create_payout_link':
@@ -353,8 +606,7 @@ function createServer({ makeClient = () => new PayoutsClient(), write } = {}) {
353
606
  // Echo the client's version when they name one: this server's
354
607
  // surface is stable across the versions that matter, and refusing
355
608
  // over a version string helps nobody.
356
- protocolVersion:
357
- (params && params.protocolVersion) || '2025-06-18',
609
+ protocolVersion: (params && params.protocolVersion) || '2025-06-18',
358
610
  capabilities: { tools: {} },
359
611
  serverInfo: SERVER,
360
612
  });
@@ -371,9 +623,7 @@ function createServer({ makeClient = () => new PayoutsClient(), write } = {}) {
371
623
  try {
372
624
  const out = await dispatch(clientOnce(), name, args);
373
625
  return reply({
374
- content: [
375
- { type: 'text', text: JSON.stringify(out, null, 2) },
376
- ],
626
+ content: [{ type: 'text', text: JSON.stringify(out, null, 2) }],
377
627
  });
378
628
  } catch (err) {
379
629
  // Tool errors belong in the result with isError, not as protocol