@venlyfinance/settlement-mcp 0.4.1 → 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.
@@ -1,6 +1,6 @@
1
1
  import { FundflowClient, VenlyFinanceClient, type MockCall } from "@venlyfinance/sdk";
2
2
  import { type VenlyEnvironment } from "../constants.js";
3
- import type { Account, CryptoCurrency, CreateAccountInput, CreateCryptoTransferInput, CreateFiatTransferInput, CreatePartyInput, CreatePayInSessionRequest, CreateVirtualBankAccountInput, CurrentCreateFiatTransferInput, FiatCurrency, ListRampRequestsParams, OptimisticLockingBody, Party, PaymentSession, RampRequestDto, RampRequestListItem, SupportedChains, Transfer, VenlyClient, VirtualBankAccount, VenlyFee, Wallet } from "../types.js";
3
+ import type { Account, CryptoCurrency, CreateAccountInput, CreateCryptoTransferInput, CreateFiatTransferInput, CreatePartyInput, CreatePayInSessionRequest, CreateVirtualBankAccountInput, CurrentCreateFiatTransferInput, FiatCurrency, ListRampRequestsParams, OptimisticLockingBody, Party, PaymentSession, Payout, PayoutBankAccount, PayoutRoute, CreatePayoutInput, CreatePayoutRouteInput, RegisterPayoutBankAccountInput, PayoutOwnershipProof, CompleteOwnershipProofInput, RampRequestDto, RampRequestListItem, SupportedChains, Transfer, VenlyClient, VirtualBankAccount, VenlyFee, Wallet } from "../types.js";
4
4
  /**
5
5
  * Normalize the legacy stage_transfer input to the current Finance
6
6
  * CreateFiatTransferInput wire shape. Exported so the write tool can show the
@@ -64,4 +64,17 @@ export declare class SdkVenlyClient implements VenlyClient {
64
64
  approveRampRequest(id: string, body: OptimisticLockingBody): Promise<RampRequestDto>;
65
65
  rejectRampRequest(id: string, body: OptimisticLockingBody): Promise<RampRequestDto>;
66
66
  createPayInSession(accountId: string, body: CreatePayInSessionRequest): Promise<PaymentSession>;
67
+ listPayouts(accountId: string, params?: {
68
+ page?: number;
69
+ size?: number;
70
+ status?: string;
71
+ }): Promise<Payout[]>;
72
+ getPayout(accountId: string, payoutId: string): Promise<Payout>;
73
+ requestPayout(accountId: string, body: CreatePayoutInput): Promise<Payout>;
74
+ listPayoutRoutes(accountId: string): Promise<PayoutRoute[]>;
75
+ createPayoutRoute(accountId: string, body: CreatePayoutRouteInput): Promise<PayoutRoute>;
76
+ preparePayoutOwnershipProof(accountId: string, routeId: string): Promise<PayoutOwnershipProof>;
77
+ completePayoutOwnershipProof(accountId: string, routeId: string, body: CompleteOwnershipProofInput): Promise<PayoutRoute>;
78
+ listPayoutBankAccounts(partyId: string): Promise<PayoutBankAccount[]>;
79
+ registerPayoutBankAccount(partyId: string, body: RegisterPayoutBankAccountInput): Promise<PayoutBankAccount>;
67
80
  }
@@ -180,4 +180,43 @@ export class SdkVenlyClient {
180
180
  this.assertReady();
181
181
  return this.finance.paymentSessions.create(accountId, body);
182
182
  }
183
+ // ----- Payout surface (contract 1.3.0) -----
184
+ async listPayouts(accountId, params) {
185
+ this.assertReady();
186
+ const page = await this.finance.payouts.list(accountId, params);
187
+ return page.items;
188
+ }
189
+ async getPayout(accountId, payoutId) {
190
+ this.assertReady();
191
+ return this.finance.payouts.get(accountId, payoutId);
192
+ }
193
+ async requestPayout(accountId, body) {
194
+ this.assertReady();
195
+ return this.finance.payouts.request(accountId, body);
196
+ }
197
+ async listPayoutRoutes(accountId) {
198
+ this.assertReady();
199
+ return this.finance.payoutRoutes.list(accountId);
200
+ }
201
+ async createPayoutRoute(accountId, body) {
202
+ this.assertReady();
203
+ return this.finance.payoutRoutes.create(accountId, body);
204
+ }
205
+ async preparePayoutOwnershipProof(accountId, routeId) {
206
+ this.assertReady();
207
+ return this.finance.payoutRoutes.prepareOwnershipProof(accountId, routeId);
208
+ }
209
+ async completePayoutOwnershipProof(accountId, routeId, body) {
210
+ this.assertReady();
211
+ return this.finance.payoutRoutes.completeOwnershipProof(accountId, routeId, body);
212
+ }
213
+ async listPayoutBankAccounts(partyId) {
214
+ this.assertReady();
215
+ const page = await this.finance.payoutBankAccounts.list(partyId);
216
+ return page.items;
217
+ }
218
+ async registerPayoutBankAccount(partyId, body) {
219
+ this.assertReady();
220
+ return this.finance.payoutBankAccounts.register(partyId, body);
221
+ }
183
222
  }
@@ -1,9 +1,9 @@
1
1
  /** Shared constants. The default environment is MOCK so an unconfigured run
2
2
  * never touches real infrastructure; staging/production are explicit. */
3
3
  export declare const SERVER_NAME = "venly-finance-mcp-server";
4
- export declare const SERVER_VERSION = "0.4.1";
4
+ export declare const SERVER_VERSION = "0.5.0";
5
5
  export declare const ENVIRONMENT_FLAG = "VENLY_ENV";
6
- export type VenlyEnvironment = "mock" | "staging" | "production";
6
+ export type VenlyEnvironment = "mock" | "qa" | "staging" | "production";
7
7
  export declare function resolveVenlyEnvironment(env: Record<string, string | undefined>): VenlyEnvironment;
8
8
  /** The env flag that must equal "1" for any write tool to execute live. */
9
9
  export declare const LIVE_FLAG = "VENLY_MCP_LIVE";
package/dist/constants.js CHANGED
@@ -1,17 +1,17 @@
1
1
  /** Shared constants. The default environment is MOCK so an unconfigured run
2
2
  * never touches real infrastructure; staging/production are explicit. */
3
3
  export const SERVER_NAME = "venly-finance-mcp-server";
4
- export const SERVER_VERSION = "0.4.1";
4
+ export const SERVER_VERSION = "0.5.0";
5
5
  export const ENVIRONMENT_FLAG = "VENLY_ENV";
6
6
  export function resolveVenlyEnvironment(env) {
7
7
  // Default is MOCK (since 0.3.0): the mock-first product must not point at
8
8
  // real infrastructure when unconfigured. Set VENLY_ENV explicitly for
9
9
  // staging or production.
10
10
  const value = env[ENVIRONMENT_FLAG] ?? "mock";
11
- if (value === "mock" || value === "staging" || value === "production") {
11
+ if (value === "mock" || value === "qa" || value === "staging" || value === "production") {
12
12
  return value;
13
13
  }
14
- throw new Error(`${ENVIRONMENT_FLAG} must be one of mock, staging, production; received ${JSON.stringify(value)}`);
14
+ throw new Error(`${ENVIRONMENT_FLAG} must be one of mock, qa, staging, production; received ${JSON.stringify(value)}`);
15
15
  }
16
16
  /** The env flag that must equal "1" for any write tool to execute live. */
17
17
  export const LIVE_FLAG = "VENLY_MCP_LIVE";
package/dist/frontend.js CHANGED
@@ -1,12 +1,35 @@
1
1
  import { z } from "zod";
2
2
  export const REGISTRY_URL_TEMPLATE = "https://raw.githubusercontent.com/Venly/venly-settlement-sdk/main/ui/r/{name}.json";
3
3
  const JOURNEYS = {
4
+ auth: `# Auth (sign-in, 2FA, sign-up)
5
+ Shell: outside the app shell - a centred card column.
6
+ Registry items: venly-tokens; block: auth (SignInForm, TwoFactorForm, SignUpForm).
7
+ Binding: an AuthAdapter YOU implement - the Venly APIs authenticate machines
8
+ (client credentials), never people, so end-user auth is your identity layer
9
+ (OAuth/OIDC, Better Auth, Auth0, Clerk, Keycloak). createMockAuthAdapter ships
10
+ for demos: deterministic 2FA code 000000, expireSession() driver.
11
+ States that must exist: signed out, bad credentials (ONE combined message -
12
+ no user enumeration), 2FA challenge with wrong-code path, session expired
13
+ (session() returns null - redirect, no other signal), duplicate sign-up email.
14
+ Rules that must hold: credential errors never confirm which half was wrong;
15
+ the code field is six slots with paste distribution and full keyboard support;
16
+ the mock never claims an email was sent.`,
17
+ team: `# Team
18
+ Shell: in-shell content column.
19
+ Registry items: venly-tokens, data-table, status-pill; block: team (TeamTable, InviteDialog).
20
+ Binding: a TeamAdapter over your auth provider (createMockTeamAdapter for demos).
21
+ States that must exist: ACTIVE/INVITED/DISABLED members on first paint, invite
22
+ created (display-only link in mock - never a fake sent-email claim), role
23
+ change persisting, self-actions blocked with the reason.
24
+ Rules that must hold: member status is word + glyph; role controls live in the
25
+ row; you cannot change your own role or disable yourself - the control is
26
+ disabled AND explains why.`,
4
27
  "home-balances": `# Home / balances
5
28
  Shell: left nav rail + thin top bar; full-width content.
6
- Registry items: venly-tokens, balance-card, data-table, status-pill.
7
- Hooks: useAccounts, useVirtualBankAccounts; balances rendered per account/currency.
8
- States that must exist: loading, zero accounts (first-run guidance), balances with reserved buckets.
9
- Rules that must hold: available is the emphasised figure and the only one above the rule; reserved is demoted by position and scale, never colour; unspendable buckets carry the padlock; never assume stablecoin parity - render the quoted rate.`,
29
+ Registry items: venly-tokens, balance-card, data-table, status-pill; block: balances (BalancesBlock, BalanceMiniature).
30
+ Hooks: useAccounts, useWallets; balances rendered per asset across the account's wallets.
31
+ States that must exist: loading, zero balances (first-run guidance), reserved buckets, entirely reserved (available 0 rendered honestly - the acct-escrow seed exercises it), balance load error degrading locally with a retry.
32
+ Rules that must hold: available is the emphasised figure and the only one above the rule; reserved is demoted by position and scale, never colour, and carries the still-yours qualifier; unspendable buckets carry the padlock; masking covers every figure including the chrome miniature; arithmetic mismatches are surfaced, never corrected; never assume stablecoin parity - render the quoted rate.`,
10
33
  receive: `# Receive
11
34
  Shell: content column - a warning callout, the field card, an advisory below.
12
35
  Registry items: venly-tokens, field-list; block: receive.
@@ -27,10 +50,16 @@ States that must exist: loading, empty ledger, rows with pending/failed pills, o
27
50
  Rules that must hold: a row click opens the panel, never navigates; no scrim - the source row stays tinted; settled rows stay quiet (colour is a budget; pills only where action or failure lives); the panel's hero is the amount; the failure reason rides the terminal timeline node.`,
28
51
  "onboarding-status": `# Onboarding / verification status
29
52
  Shell: full page, form clamped ~600px; a status home once submitted.
30
- Registry items: venly-tokens, timeline, status-pill, field-list.
31
- Hooks: useParties, useCreateParty; verification status from the party/account records.
32
- States that must exist: collecting (per-section progress), submitted/waiting (say who acts next, on which channel, what still works meanwhile), approved, declined (humane copy + what to do next), re-verification on a live account.
33
- Rules that must hold: never render a fake progress percentage - use real per-item status; a waiting state answers how long / who acts / what still works; a decline explains and offers a next step, not a dead end; creating a party is NOT completed verification - show the honest state.`,
53
+ Registry items: venly-tokens, timeline, status-pill, field-list; block: onboarding (CompanyForm, VerificationStatusHome, RestrictedBanner).
54
+ Hooks: useCreateParty, useCreateAccount, useParty, useAccount; verification status from the party/account records verbatim.
55
+ States that must exist: collecting (review before submit), submitted/waiting (say who acts next, on which channel, what still works meanwhile), approved, declined (humane copy, review-request as the primary action), re-verification on a live account (banner naming what pauses and what keeps working).
56
+ Rules that must hold: never render a fake progress percentage - use real status; a waiting state answers how long / who acts / what still works, and where no review window is published the copy says so instead of inventing one; a decline explains and offers a next step, not a dead end; creating a party is NOT completed verification - show the honest state.`,
57
+ "withdraw-bank-accounts": `# Withdraw + bank accounts (off-ramp)
58
+ Shell: settings page for the whitelist; full page for the flow, form clamped ~600px.
59
+ Registry items: venly-tokens, data-table, status-pill, timeline, field-list, arithmetic-ladder; blocks: bank-accounts (BankAccountsBlock, AddBankAccountForm), withdraw (WithdrawFlow, WithdrawalsTable, ConnectedWithdrawDetail).
60
+ Hooks: useCompanyBankAccounts, useBankAccountConfig, useCreateCompanyBankAccount, useRampRequests, useRampRequest, useCreateRampRequest, useFeeQuote, useRampPairs, useReferenceData, useFourEyesApproval, useInitiateRamp, describeRampStatus.
61
+ States that must exist: empty whitelist (one CTA), account in review / verified / declined, no-verified-destination block, amount over balance (two-place signal), fee quote with its unit, awaiting approval (creator sees why they can't approve), stale decision (409 - refetch and re-decide, never auto-retry), awaiting funds (deposit instructions + mandatory reference + tx-hash report), processing, paid out, failed, rejected, cancelled, on hold.
62
+ Rules that must hold: destinations are the company's OWN verified accounts - unverified rows are disabled with the reason, never hidden; the pre-create review renders only known figures (no invented rate, no bank-receives placeholder - the created record carries the fiat arithmetic and the detail opens on it); a refusal never reads as a wait; the event timeline renders actor, role and absolute timestamps.`,
34
63
  reconciliation: `# Reconciliation
35
64
  Shell: split pane (roughly one-third list, two-thirds evidence) - not a drawer.
36
65
  Registry items: venly-tokens, data-table, side-panel, status-pill, field-list.
@@ -104,7 +133,7 @@ registry install. On a fresh Vite/React app that means, in order:
104
133
  works - it never imports base-library components itself).
105
134
  3. Add the registry once to components.json -
106
135
  { "registries": { "@venlyfinance": "${REGISTRY_URL_TEMPLATE}" } }
107
- 4. \`npx shadcn@latest add @venlyfinance/receive @venlyfinance/send @venlyfinance/activity -y -o\`.
136
+ 4. \`npx shadcn@latest add @venlyfinance/balances @venlyfinance/activity @venlyfinance/receive @venlyfinance/send @venlyfinance/auth @venlyfinance/team @venlyfinance/onboarding -y -o\`.
108
137
  Each block auto-installs its components, the venly-tokens file AND its
109
138
  npm dependencies (@venlyfinance/react, @venlyfinance/sdk, TanStack
110
139
  Query) - no separate npm install step is needed.
package/dist/resources.js CHANGED
@@ -15,14 +15,16 @@ Venly Finance provides financial infrastructure through several regulated partne
15
15
  - fiat-to-crypto payment sessions;
16
16
  - account-to-account fiat-denominated and crypto-denominated transfers;
17
17
  - payment-request authorization, settlement and reversal primitives;
18
+ - third-party payouts (contract 1.3.0): beneficiary bank accounts registered per party (SEPA and US ACH, masked details), payout routes activated by wallet-ownership proof, and payouts with a full lifecycle (REQUESTED through COMPLETED, REJECTED, FAILED or RETURNED, failures carrying a reason);
18
19
  - Fundflow on/off-ramp workflows with four-eyes approval.
19
20
 
20
21
  Current boundaries:
21
22
 
22
- - EUR is the currently documented virtual-bank-account currency. Do not infer global bank-account coverage.
23
+ - EUR SEPA and USD ACH are the documented virtual-bank-account types. Do not infer global bank-account coverage.
23
24
  - Creating a party does not complete KYC/KYB. Live virtual-bank-account provisioning requires a VERIFIED account.
25
+ - The payout surface is on the QA contract (Finance API 1.3.0); production may trail it. Verify against the environment you target.
24
26
  - Card issuing is not exposed by the current Finance OpenAPI contract.
25
- - A bank charter, deposit insurance and external-bank payout coverage are not supplied or implied by this MCP.
27
+ - A bank charter and deposit insurance are not supplied or implied by this MCP.
26
28
  - Production x402 settlement is not implemented; the x402 tool is a quote-only stub.
27
29
  `,
28
30
  },
@@ -33,7 +35,7 @@ Current boundaries:
33
35
  description: "Environment, write, compliance and secret-handling rules.",
34
36
  text: `# Venly Finance MCP safety
35
37
 
36
- - Set VENLY_ENV explicitly to mock, staging or production. An absent value defaults to mock (since 0.3.0), so an unconfigured server never points at real infrastructure.
38
+ - Set VENLY_ENV explicitly to mock, qa, staging or production. An absent value defaults to mock (since 0.3.0), so an unconfigured server never points at real infrastructure.
37
39
  - Mock mode uses synthetic SDK fixtures, no credentials and no network. Every mutation result is labelled mode=mock.
38
40
  - Staging writes require confirm=true, VENLY_MCP_LIVE=1 and VENLY_CLIENT_ID/VENLY_CLIENT_SECRET.
39
41
  - Production requires every staging gate plus VENLY_MCP_PRODUCTION=1.
@@ -1,4 +1,4 @@
1
- export declare const EXPECTED_TOOLS: readonly ["list_ramp_requests", "get_ramp_request", "list_accounts", "get_account", "list_wallets", "list_virtual_bank_accounts", "get_virtual_bank_account", "reconcile_by_reference_code", "list_transfers", "get_transfer", "list_parties", "get_party", "get_reference_data", "create_party", "create_account", "create_virtual_bank_account", "create_fiat_transfer", "create_crypto_transfer", "approve_ramp_request", "reject_ramp_request", "create_payment_session", "quote_x402_payment", "get_journey_blueprint", "review_screen"];
1
+ export declare const EXPECTED_TOOLS: readonly ["list_ramp_requests", "get_ramp_request", "list_accounts", "get_account", "list_wallets", "list_virtual_bank_accounts", "get_virtual_bank_account", "reconcile_by_reference_code", "list_transfers", "get_transfer", "list_parties", "get_party", "get_reference_data", "create_party", "create_account", "create_virtual_bank_account", "create_fiat_transfer", "create_crypto_transfer", "approve_ramp_request", "reject_ramp_request", "create_payment_session", "list_payouts", "get_payout", "list_payout_routes", "list_payout_bank_accounts", "register_payout_bank_account", "create_payout_route", "prepare_payout_ownership_proof", "complete_payout_ownership_proof", "request_payout", "quote_x402_payment", "get_journey_blueprint", "review_screen"];
2
2
  export declare const EXPECTED_RESOURCE_URIS: readonly ["venly://capabilities", "venly://safety", "venly://workflows/international-account", "venly://workflows/mock-to-staging", "venly://frontend/agents"];
3
3
  export declare const EXPECTED_PROMPTS: readonly ["build_international_account"];
4
4
  export interface DiscoveryNames {
@@ -25,6 +25,15 @@ export const EXPECTED_TOOLS = [
25
25
  "approve_ramp_request",
26
26
  "reject_ramp_request",
27
27
  "create_payment_session",
28
+ "list_payouts",
29
+ "get_payout",
30
+ "list_payout_routes",
31
+ "list_payout_bank_accounts",
32
+ "register_payout_bank_account",
33
+ "create_payout_route",
34
+ "prepare_payout_ownership_proof",
35
+ "complete_payout_ownership_proof",
36
+ "request_payout",
28
37
  "quote_x402_payment",
29
38
  "get_journey_blueprint",
30
39
  "review_screen",
@@ -270,4 +270,83 @@ export function registerReadTools(server, client) {
270
270
  return errorResult(e.message);
271
271
  }
272
272
  });
273
+ server.registerTool("list_payouts", {
274
+ title: "List payouts",
275
+ description: "List third-party payouts for an account (finance GET /v1/accounts/{accountId}/payouts). " +
276
+ "A payout moves crypto out of the account and settles fiat to a registered beneficiary " +
277
+ "bank account. Statuses: REQUESTED, SENDING, PROVIDER_PROCESSING, COMPLETED, REJECTED, " +
278
+ "FAILED, RETURNED. Read-only.",
279
+ inputSchema: {
280
+ accountId: z.string(),
281
+ status: z
282
+ .enum([
283
+ "REQUESTED",
284
+ "SENDING",
285
+ "PROVIDER_PROCESSING",
286
+ "COMPLETED",
287
+ "REJECTED",
288
+ "FAILED",
289
+ "RETURNED",
290
+ ])
291
+ .optional(),
292
+ page: z.number().int().min(1).optional(),
293
+ size: z.number().int().min(1).max(200).optional(),
294
+ },
295
+ annotations: READ_ONLY,
296
+ }, async ({ accountId, ...params }) => {
297
+ try {
298
+ const result = await client.listPayouts(accountId, params);
299
+ return jsonResult({ count: result.length, payouts: result });
300
+ }
301
+ catch (e) {
302
+ return errorResult(e.message);
303
+ }
304
+ });
305
+ server.registerTool("get_payout", {
306
+ title: "Get one payout",
307
+ description: "Fetch one payout by id (finance GET /v1/accounts/{accountId}/payouts/{payoutId}). " +
308
+ "COMPLETED payouts carry settledFiatAmount and completedAt; REJECTED/FAILED/RETURNED " +
309
+ "carry a failureReason. Read-only.",
310
+ inputSchema: { accountId: z.string(), payoutId: z.string() },
311
+ annotations: READ_ONLY,
312
+ }, async ({ accountId, payoutId }) => {
313
+ try {
314
+ return jsonResult(await client.getPayout(accountId, payoutId));
315
+ }
316
+ catch (e) {
317
+ return errorResult(e.message);
318
+ }
319
+ });
320
+ server.registerTool("list_payout_routes", {
321
+ title: "List payout routes",
322
+ description: "List an account's payout routes (finance GET /v1/accounts/{accountId}/payout-routes). " +
323
+ "A route binds a beneficiary bank account to this account and a deposit asset; only an " +
324
+ "ACTIVE route (ownership proof completed) can carry payouts. Read-only.",
325
+ inputSchema: { accountId: z.string() },
326
+ annotations: READ_ONLY,
327
+ }, async ({ accountId }) => {
328
+ try {
329
+ const result = await client.listPayoutRoutes(accountId);
330
+ return jsonResult({ count: result.length, payoutRoutes: result });
331
+ }
332
+ catch (e) {
333
+ return errorResult(e.message);
334
+ }
335
+ });
336
+ server.registerTool("list_payout_bank_accounts", {
337
+ title: "List beneficiary bank accounts",
338
+ description: "List the payout (beneficiary) bank accounts registered on a party " +
339
+ "(finance GET /v1/parties/{partyId}/payout-bank-accounts). Rail details come back " +
340
+ "masked. Only ACTIVE accounts can back a payout route. Read-only.",
341
+ inputSchema: { partyId: z.string() },
342
+ annotations: READ_ONLY,
343
+ }, async ({ partyId }) => {
344
+ try {
345
+ const result = await client.listPayoutBankAccounts(partyId);
346
+ return jsonResult({ count: result.length, payoutBankAccounts: result });
347
+ }
348
+ catch (e) {
349
+ return errorResult(e.message);
350
+ }
351
+ });
273
352
  }
@@ -332,4 +332,151 @@ export function registerWriteTools(server, client, env) {
332
332
  return errorResult(e.message);
333
333
  }
334
334
  });
335
+ server.registerTool("register_payout_bank_account", {
336
+ title: "Register a beneficiary bank account",
337
+ description: "Register a payout (beneficiary) bank account on a party (finance POST " +
338
+ "/v1/parties/{partyId}/payout-bank-accounts). The account starts PENDING; an operator " +
339
+ "activates it before it can back a payout route. Rail details come back masked. " +
340
+ "Dry-run by default.",
341
+ inputSchema: {
342
+ partyId: z.string(),
343
+ rail: z.enum(["SEPA", "US_ACH"]),
344
+ fiatCurrency: z.string().min(3).max(3),
345
+ label: z.string().optional(),
346
+ accountHolderName: z.string().min(1),
347
+ iban: z.string().optional().describe("SEPA rail"),
348
+ bic: z.string().optional().describe("SEPA rail"),
349
+ accountNumber: z.string().optional().describe("US_ACH rail"),
350
+ abaRoutingNumber: z.string().optional().describe("US_ACH rail"),
351
+ accountType: z.enum(["CHECKING", "SAVINGS"]).optional().describe("US_ACH rail"),
352
+ bankName: z.string().optional(),
353
+ confirm: confirmField,
354
+ },
355
+ annotations: WRITE_ANNOTATIONS,
356
+ }, async ({ partyId, confirm, iban, bic, accountNumber, abaRoutingNumber, accountType, ...rest }) => {
357
+ const body = {
358
+ ...rest,
359
+ railDetails: { iban, bic, accountNumber, abaRoutingNumber, accountType },
360
+ };
361
+ const gate = evaluateWriteGate(confirm, env);
362
+ if (!gate.armed) {
363
+ return jsonResult(buildDryRun("register_payout_bank_account", "POST", "finance", `/parties/${partyId}/payout-bank-accounts`, body, gate));
364
+ }
365
+ try {
366
+ return executionResult(gate, await client.registerPayoutBankAccount(partyId, body));
367
+ }
368
+ catch (e) {
369
+ return errorResult(e.message);
370
+ }
371
+ });
372
+ server.registerTool("create_payout_route", {
373
+ title: "Create a payout route",
374
+ description: "Bind an ACTIVE beneficiary bank account to an account and a deposit asset (finance " +
375
+ "POST /v1/accounts/{accountId}/payout-routes). The route starts AWAITING_OWNERSHIP_PROOF; " +
376
+ "complete_payout_ownership_proof activates it. Dry-run by default.",
377
+ inputSchema: {
378
+ accountId: z.string(),
379
+ payoutBankAccountId: z.string(),
380
+ chain: z.enum(["AVALANCHE", "BASE", "ETHEREUM", "POLYGON", "SOLANA"]),
381
+ asset: z.string().min(1).describe("Deposit asset name, e.g. USDC"),
382
+ confirm: confirmField,
383
+ },
384
+ annotations: WRITE_ANNOTATIONS,
385
+ }, async ({ accountId, payoutBankAccountId, chain, asset, confirm }) => {
386
+ const body = { payoutBankAccountId, depositAsset: { chain, name: asset } };
387
+ const gate = evaluateWriteGate(confirm, env);
388
+ if (!gate.armed) {
389
+ return jsonResult(buildDryRun("create_payout_route", "POST", "finance", `/accounts/${accountId}/payout-routes`, body, gate));
390
+ }
391
+ try {
392
+ return executionResult(gate, await client.createPayoutRoute(accountId, body));
393
+ }
394
+ catch (e) {
395
+ return errorResult(e.message);
396
+ }
397
+ });
398
+ server.registerTool("prepare_payout_ownership_proof", {
399
+ title: "Prepare route ownership proof",
400
+ description: "Get the message the route's funding wallet must sign (finance POST " +
401
+ "/v1/accounts/{accountId}/payout-routes/{routeId}/ownership-proof/prepare). Takes no " +
402
+ "body: the server derives the wallet and chain from the route. The signature itself " +
403
+ "is produced by the wallet owner, never by this server. Dry-run by default.",
404
+ inputSchema: {
405
+ accountId: z.string(),
406
+ routeId: z.string(),
407
+ confirm: confirmField,
408
+ },
409
+ annotations: WRITE_ANNOTATIONS,
410
+ }, async ({ accountId, routeId, confirm }) => {
411
+ const gate = evaluateWriteGate(confirm, env);
412
+ if (!gate.armed) {
413
+ return jsonResult(buildDryRun("prepare_payout_ownership_proof", "POST", "finance", `/accounts/${accountId}/payout-routes/${routeId}/ownership-proof/prepare`, {}, gate));
414
+ }
415
+ try {
416
+ return executionResult(gate, await client.preparePayoutOwnershipProof(accountId, routeId));
417
+ }
418
+ catch (e) {
419
+ return errorResult(e.message);
420
+ }
421
+ });
422
+ server.registerTool("complete_payout_ownership_proof", {
423
+ title: "Complete route ownership proof",
424
+ description: "Submit the signed ownership-proof message; on success the route becomes ACTIVE " +
425
+ "(finance POST /v1/accounts/{accountId}/payout-routes/{routeId}/ownership-proof/complete). " +
426
+ "Dry-run by default.",
427
+ inputSchema: {
428
+ accountId: z.string(),
429
+ routeId: z.string(),
430
+ message: z.string().min(1),
431
+ signature: z.string().min(1),
432
+ confirm: confirmField,
433
+ },
434
+ annotations: WRITE_ANNOTATIONS,
435
+ }, async ({ accountId, routeId, confirm, ...body }) => {
436
+ const gate = evaluateWriteGate(confirm, env);
437
+ if (!gate.armed) {
438
+ return jsonResult(buildDryRun("complete_payout_ownership_proof", "POST", "finance", `/accounts/${accountId}/payout-routes/${routeId}/ownership-proof/complete`, body, gate));
439
+ }
440
+ try {
441
+ return executionResult(gate, await client.completePayoutOwnershipProof(accountId, routeId, body));
442
+ }
443
+ catch (e) {
444
+ return errorResult(e.message);
445
+ }
446
+ });
447
+ server.registerTool("request_payout", {
448
+ title: "Request a payout",
449
+ description: "Move crypto out of the account and settle fiat to the route's beneficiary bank " +
450
+ "account (finance POST /v1/accounts/{accountId}/payouts). Requires an ACTIVE payout " +
451
+ "route. This is money leaving the platform. Dry-run by default.",
452
+ inputSchema: {
453
+ accountId: z.string(),
454
+ payoutRouteId: z.string(),
455
+ cryptoAmount: z.number().positive(),
456
+ idempotencyKey: z
457
+ .string()
458
+ .optional()
459
+ .describe("UUID; generated when omitted"),
460
+ confirm: confirmField,
461
+ },
462
+ annotations: WRITE_ANNOTATIONS,
463
+ }, async ({ accountId, confirm, ...rest }) => {
464
+ const body = {
465
+ payoutRouteId: rest.payoutRouteId,
466
+ cryptoAmount: rest.cryptoAmount,
467
+ idempotencyKey: rest.idempotencyKey ?? crypto.randomUUID(),
468
+ };
469
+ const gate = evaluateWriteGate(confirm, env);
470
+ if (!gate.armed) {
471
+ const dryRun = buildDryRun("request_payout", "POST", "finance", `/accounts/${accountId}/payouts`, body, gate);
472
+ dryRun.note += " Payouts are outbound money movement; the route must be ACTIVE.";
473
+ return jsonResult(dryRun);
474
+ }
475
+ try {
476
+ return executionResult(gate, await client.requestPayout(accountId, body));
477
+ }
478
+ catch (e) {
479
+ return errorResult(e.message);
480
+ }
481
+ });
335
482
  }
package/dist/types.d.ts CHANGED
@@ -10,19 +10,27 @@ import type { FinanceComponents, FundflowComponents } from "@venlyfinance/sdk";
10
10
  import type { VenlyEnvironment } from "./constants.js";
11
11
  type FinanceSchemas = FinanceComponents["schemas"];
12
12
  type FundflowSchemas = FundflowComponents["schemas"];
13
- export type AddressInput = FinanceSchemas["Address"];
14
- export type Party = FinanceSchemas["Party"];
13
+ export type AddressInput = FinanceSchemas["AddressDto"];
14
+ export type Party = FinanceSchemas["PartyDto"];
15
15
  export type CreatePartyInput = FinanceSchemas["CreatePartyRequest"];
16
- export type Account = FinanceSchemas["Account"];
16
+ export type Account = FinanceSchemas["AccountListItemDto"];
17
17
  export type CreateAccountInput = FinanceSchemas["CreateAccountRequest"];
18
- export type Wallet = FinanceSchemas["Wallet"];
19
- export type VirtualBankAccount = FinanceSchemas["VirtualBankAccount"];
18
+ export type Wallet = FinanceSchemas["WalletBalanceDto"];
19
+ export type VirtualBankAccount = FinanceSchemas["VirtualBankAccountResponse"];
20
20
  export type CreateVirtualBankAccountInput = FinanceSchemas["CreateVirtualBankAccountRequest"];
21
- export type PaymentSession = FinanceSchemas["PaymentSession"];
22
- export type CreatePayInSessionRequest = FinanceSchemas["CreatePayInSessionRequest"];
23
- export type Transfer = FinanceSchemas["Transfer"];
21
+ export type PaymentSession = FinanceSchemas["PayInSessionDto"];
22
+ export type CreatePayInSessionRequest = FinanceSchemas["CreatePayInSessionInput"];
23
+ export type Transfer = FinanceSchemas["TransferRequestDto"];
24
24
  export type CurrentCreateFiatTransferInput = FinanceSchemas["CreateFiatTransferInput"];
25
25
  export type CreateCryptoTransferInput = FinanceSchemas["CreateCryptoTransferInput"];
26
+ export type Payout = FinanceSchemas["PayoutDto"];
27
+ export type CreatePayoutInput = FinanceSchemas["CreatePayoutRequest"];
28
+ export type PayoutRoute = FinanceSchemas["PayoutRouteDto"];
29
+ export type CreatePayoutRouteInput = FinanceSchemas["CreatePayoutRouteRequest"];
30
+ export type PayoutBankAccount = FinanceSchemas["PayoutBankAccountDto"];
31
+ export type RegisterPayoutBankAccountInput = FinanceSchemas["RegisterPayoutBankAccountRequest"];
32
+ export type PayoutOwnershipProof = FinanceSchemas["PayoutOwnershipProofDto"];
33
+ export type CompleteOwnershipProofInput = FinanceSchemas["CompletePayoutOwnershipProofRequest"];
26
34
  export type RampRequestDto = FundflowSchemas["RampRequestDto"];
27
35
  export type RampRequestListItem = FundflowSchemas["RampRequestListItem"];
28
36
  export type OptimisticLockingBody = FundflowSchemas["UpdateWithOptimisticLockingRequest"];
@@ -118,5 +126,18 @@ export interface VenlyClient {
118
126
  approveRampRequest(id: string, body: OptimisticLockingBody): Promise<RampRequestDto>;
119
127
  rejectRampRequest(id: string, body: OptimisticLockingBody): Promise<RampRequestDto>;
120
128
  createPayInSession(accountId: string, body: CreatePayInSessionRequest): Promise<PaymentSession>;
129
+ listPayouts(accountId: string, params?: {
130
+ page?: number;
131
+ size?: number;
132
+ status?: string;
133
+ }): Promise<Payout[]>;
134
+ getPayout(accountId: string, payoutId: string): Promise<Payout>;
135
+ requestPayout(accountId: string, body: CreatePayoutInput): Promise<Payout>;
136
+ listPayoutRoutes(accountId: string): Promise<PayoutRoute[]>;
137
+ createPayoutRoute(accountId: string, body: CreatePayoutRouteInput): Promise<PayoutRoute>;
138
+ preparePayoutOwnershipProof(accountId: string, routeId: string): Promise<PayoutOwnershipProof>;
139
+ completePayoutOwnershipProof(accountId: string, routeId: string, body: CompleteOwnershipProofInput): Promise<PayoutRoute>;
140
+ listPayoutBankAccounts(partyId: string): Promise<PayoutBankAccount[]>;
141
+ registerPayoutBankAccount(partyId: string, body: RegisterPayoutBankAccountInput): Promise<PayoutBankAccount>;
121
142
  }
122
143
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@venlyfinance/settlement-mcp",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "Venly Finance MCP: SDK-backed tools, resources and prompts for building international money products safely.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -27,7 +27,7 @@
27
27
  "node": ">=20"
28
28
  },
29
29
  "dependencies": {
30
- "@venlyfinance/sdk": "^0.2.0",
30
+ "@venlyfinance/sdk": "^0.4.0",
31
31
  "@modelcontextprotocol/sdk": "^1.30.0",
32
32
  "zod": "^3.23.8"
33
33
  },
@@ -60,4 +60,4 @@
60
60
  "eur",
61
61
  "viban"
62
62
  ]
63
- }
63
+ }