@venlyfinance/settlement-mcp 0.2.0 → 0.4.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 +48 -0
- package/README.md +21 -4
- package/dist/constants.d.ts +3 -3
- package/dist/constants.js +7 -4
- package/dist/frontend.d.ts +21 -0
- package/dist/frontend.js +163 -0
- package/dist/index.js +8 -1
- package/dist/prompts.js +9 -8
- package/dist/reconcile.d.ts +7 -0
- package/dist/reconcile.js +20 -2
- package/dist/resources.js +1 -1
- package/dist/safety.d.ts +2 -0
- package/dist/safety.js +1 -0
- package/dist/server.js +2 -0
- package/dist/staging-smoke.d.ts +2 -2
- package/dist/staging-smoke.js +3 -1
- package/dist/tools/read-tools.js +8 -2
- package/dist/tools/write-tools.js +23 -65
- package/package.json +2 -2
- package/skills/stage-and-confirm-transfer.md +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0 – 2026-08-04
|
|
4
|
+
|
|
5
|
+
Wording-is-the-safety-surface release. An outside integrator audit (Report 1,
|
|
6
|
+
2026-08-04) found the server's words disagreeing with its behavior in three
|
|
7
|
+
places; all fixed, plus the SDK under the mock now teaches the documented
|
|
8
|
+
lifecycle (see @venlyfinance/sdk 0.2.0).
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- **Default environment is `mock`** (was `staging`). A mock-first product must
|
|
13
|
+
not point at real infrastructure when unconfigured. Set `VENLY_ENV=staging`
|
|
14
|
+
or `production` explicitly for real calls; the safety resource and README
|
|
15
|
+
say so.
|
|
16
|
+
- **State-accurate startup banner.** Mock: "writes execute against local
|
|
17
|
+
fixtures - no network, no credentials, nothing real". Staging/production
|
|
18
|
+
disarmed: "mutations return dry-run previews". Armed: "confirmed writes hit
|
|
19
|
+
the live API". No more "DISARMED" next to a write that visibly executes.
|
|
20
|
+
- **Explicit `dryRun: true|false` on every mutation result** - agents no longer
|
|
21
|
+
infer persistence from `mode`.
|
|
22
|
+
- **`reconcile_by_reference_code` reads real remittance text**: matching is
|
|
23
|
+
case- and separator-insensitive, transactions match by containment
|
|
24
|
+
("invoice ref abc 123 ty" finds REF-ABC-123), and codes under 4 alphanumeric
|
|
25
|
+
characters are refused.
|
|
26
|
+
- **Receiver XOR enforced**: `create_fiat_transfer` / `create_crypto_transfer`
|
|
27
|
+
reject a transfer with zero or two receivers (exactly one of
|
|
28
|
+
`receiverAccountId` / `receiverExternalId`), stated in the tool schema.
|
|
29
|
+
- Requires `@venlyfinance/sdk` ^0.2.0: in mock mode, created parties/accounts
|
|
30
|
+
start verification-pending (`mock.advanceVerification`), transfers start
|
|
31
|
+
`PENDING` (`mock.advanceTransfer`), and request bodies are spec-validated.
|
|
32
|
+
|
|
33
|
+
### Deprecated
|
|
34
|
+
|
|
35
|
+
- **`stage_transfer`** is now plainly marked deprecated (legacy alias of
|
|
36
|
+
`create_fiat_transfer`); it will be removed in 0.4.0.
|
|
37
|
+
|
|
3
38
|
## 0.2.0 – 2026-08-03
|
|
4
39
|
|
|
5
40
|
The Settlement MCP becomes the SDK-backed **Venly Finance MCP** builder while retaining
|
|
@@ -52,3 +87,16 @@ its package name and compatibility binary throughout the 0.x line.
|
|
|
52
87
|
### Security
|
|
53
88
|
|
|
54
89
|
- Runtime dependency audits report zero findings after the MCP SDK/Hono upgrade.
|
|
90
|
+
|
|
91
|
+
## 0.4.0 – 2026-08-07
|
|
92
|
+
|
|
93
|
+
Frontend toolset: the judgment layer for interface assembly.
|
|
94
|
+
|
|
95
|
+
- `get_journey_blueprint` – screen inventory, required states, registry items and binding hooks for eight money-product journeys.
|
|
96
|
+
- `review_screen` – deterministic design audit (raw colours, hyphen-minus amounts, success styling on cancelled steps, masked review values, zebra striping, off-token shadows, gradients, colour-only state). Findings, not a score.
|
|
97
|
+
- `venly://frontend/agents` resource – composition rules plus the @venlyfinance shadcn-registry wiring (delivery of UI source rides the registry standard; these tools carry what a registry cannot).
|
|
98
|
+
- `build_international_account` prompt now assembles the interface from the registry and gates every screen on `review_screen`.
|
|
99
|
+
|
|
100
|
+
### Removed
|
|
101
|
+
|
|
102
|
+
- **`stage_transfer`**, as promised in 0.3.0's deprecation: use `create_fiat_transfer`, whose inputs match the OpenAPI contract directly. The skills pack is updated accordingly.
|
package/README.md
CHANGED
|
@@ -92,7 +92,7 @@ Builder writes: `create_party`, `create_account`,
|
|
|
92
92
|
`create_crypto_transfer`, `create_payment_session`.
|
|
93
93
|
|
|
94
94
|
Operator writes: `approve_ramp_request`, `reject_ramp_request`. The legacy
|
|
95
|
-
`stage_transfer`
|
|
95
|
+
`stage_transfer` alias was removed in 0.4.0 as deprecated in 0.3.0; use
|
|
96
96
|
`create_fiat_transfer` and its current OpenAPI field names.
|
|
97
97
|
|
|
98
98
|
Each is dry-run by default and returns the exact request it would send. See the
|
|
@@ -106,6 +106,23 @@ It documents the machine-to-machine rail. It never executes a payment, never
|
|
|
106
106
|
calls a facilitator, and never moves funds. Production x402 settlement needs a
|
|
107
107
|
facilitator decision and live rails.
|
|
108
108
|
|
|
109
|
+
## Frontend toolset (interface assembly)
|
|
110
|
+
|
|
111
|
+
Delivery of UI source rides the shadcn registry standard – add
|
|
112
|
+
`{ "registries": { "@venlyfinance": "https://raw.githubusercontent.com/Venly/venly-settlement-sdk/main/ui/r/{name}.json" } }`
|
|
113
|
+
to `components.json`, then `npx shadcn@latest add @venlyfinance/receive`. The MCP carries
|
|
114
|
+
what a registry cannot:
|
|
115
|
+
|
|
116
|
+
- `get_journey_blueprint` – screen inventory, required states, registry items and binding
|
|
117
|
+
hooks for eight money-product journeys.
|
|
118
|
+
- `review_screen` – deterministic design audit of a screen's source (raw colours,
|
|
119
|
+
hyphen-minus amounts, success styling on cancelled steps, masked review values, zebra
|
|
120
|
+
striping, off-token shadows, colour-only state). Findings, not a score.
|
|
121
|
+
- `venly://frontend/agents` – the composition rules an agent should read before building.
|
|
122
|
+
|
|
123
|
+
The `build_international_account` prompt assembles the interface from the registry and
|
|
124
|
+
gates every finished screen on `review_screen`. See [`ui/`](../ui/README.md) for the kit itself.
|
|
125
|
+
|
|
109
126
|
## Safety model (fail closed)
|
|
110
127
|
|
|
111
128
|
Outside explicit mock mode, read-only/dry-run is the default posture. A staging
|
|
@@ -190,7 +207,7 @@ Override via env:
|
|
|
190
207
|
|
|
191
208
|
| Env var | Default (staging) |
|
|
192
209
|
|---|---|
|
|
193
|
-
| `VENLY_ENV` | `
|
|
210
|
+
| `VENLY_ENV` | Defaults to `mock` (since 0.3.0): an unconfigured server never points at real infrastructure. Set `staging` or `production` explicitly |
|
|
194
211
|
| `VENLY_FINANCE_BASE_URL` | `https://api-staging.venlyfinance.com/v1` |
|
|
195
212
|
| `VENLY_FUNDFLOW_BASE_URL` | `https://api-fundflow-staging.venly.io` |
|
|
196
213
|
| `VENLY_TOKEN_URL` | `https://login-staging.venly.io/auth/realms/VenlyFinance/protocol/openid-connect/token` (staging; use `login.venly.io` for production) |
|
|
@@ -211,8 +228,8 @@ reads, and fail-closed write gate without mutating staging:
|
|
|
211
228
|
VENLY_CLIENT_ID=... VENLY_CLIENT_SECRET=... npm run smoke:staging
|
|
212
229
|
```
|
|
213
230
|
|
|
214
|
-
The command starts the MCP with `VENLY_ENV=staging`, lists the expected
|
|
215
|
-
|
|
231
|
+
The command starts the MCP with `VENLY_ENV=staging`, lists the expected 24 tools,
|
|
232
|
+
five resources, and builder prompt, then reads parties, accounts, and reference data.
|
|
216
233
|
It deliberately removes `VENLY_MCP_LIVE` and `VENLY_MCP_PRODUCTION` from the child
|
|
217
234
|
process before submitting one confirmed `create_party` request. Passing requires that
|
|
218
235
|
request to return `mode: dry-run`, `environment: staging`, and an unarmed gate. Output
|
package/dist/constants.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
/** Shared constants.
|
|
2
|
-
* touches
|
|
1
|
+
/** Shared constants. The default environment is MOCK so an unconfigured run
|
|
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
|
+
export declare const SERVER_VERSION = "0.4.0";
|
|
5
5
|
export declare const ENVIRONMENT_FLAG = "VENLY_ENV";
|
|
6
6
|
export type VenlyEnvironment = "mock" | "staging" | "production";
|
|
7
7
|
export declare function resolveVenlyEnvironment(env: Record<string, string | undefined>): VenlyEnvironment;
|
package/dist/constants.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
/** Shared constants.
|
|
2
|
-
* touches
|
|
1
|
+
/** Shared constants. The default environment is MOCK so an unconfigured run
|
|
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
|
+
export const SERVER_VERSION = "0.4.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
|
+
// real infrastructure when unconfigured. Set VENLY_ENV explicitly for
|
|
9
|
+
// staging or production.
|
|
10
|
+
const value = env[ENVIRONMENT_FLAG] ?? "mock";
|
|
8
11
|
if (value === "mock" || value === "staging" || value === "production") {
|
|
9
12
|
return value;
|
|
10
13
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Frontend toolset: what no generic registry can provide.
|
|
3
|
+
*
|
|
4
|
+
* Delivery of UI source belongs to the shadcn registry standard (the
|
|
5
|
+
* @venlyfinance registry under ui/r/ in this repo); these tools carry the
|
|
6
|
+
* judgment layer instead – journey blueprints (what screens and states a
|
|
7
|
+
* money product needs) and a deterministic design audit that pushes back
|
|
8
|
+
* on the classic agent-built-dashboard failure modes.
|
|
9
|
+
*/
|
|
10
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
11
|
+
export declare const REGISTRY_URL_TEMPLATE = "https://raw.githubusercontent.com/Venly/venly-settlement-sdk/main/ui/r/{name}.json";
|
|
12
|
+
interface Finding {
|
|
13
|
+
rule: string;
|
|
14
|
+
severity: "error" | "warn";
|
|
15
|
+
evidence: string;
|
|
16
|
+
fix: string;
|
|
17
|
+
}
|
|
18
|
+
/** Deterministic design audit. Text in, findings out - no model, no taste. */
|
|
19
|
+
export declare function reviewScreenSource(source: string): Finding[];
|
|
20
|
+
export declare function registerFrontendTools(server: McpServer): void;
|
|
21
|
+
export {};
|
package/dist/frontend.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const REGISTRY_URL_TEMPLATE = "https://raw.githubusercontent.com/Venly/venly-settlement-sdk/main/ui/r/{name}.json";
|
|
3
|
+
const JOURNEYS = {
|
|
4
|
+
"home-balances": `# Home / balances
|
|
5
|
+
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.`,
|
|
10
|
+
receive: `# Receive
|
|
11
|
+
Shell: content column - a warning callout, the field card, an advisory below.
|
|
12
|
+
Registry items: venly-tokens, field-list; block: receive.
|
|
13
|
+
Hooks: useVirtualBankAccounts (first active EUR account).
|
|
14
|
+
States that must exist: no virtual bank account yet (offer creation), details present, reference not yet assigned ("Not assigned yet" + Required pill - never "(not required)").
|
|
15
|
+
Rules that must hold: the payment reference is enforced as mandatory (amber Required pill, warning above the fields); per-field copy names the field it copied and only confirms on a successful write; rows never vanish - render the "(not required)" variant.`,
|
|
16
|
+
send: `# Send
|
|
17
|
+
Shell: full page; form clamped ~600px; review step replaces the form.
|
|
18
|
+
Registry items: venly-tokens, arithmetic-ladder, timeline; block: send.
|
|
19
|
+
Hooks: useStagedTransfer (the machine IS the flow), useFeeQuote when fees apply.
|
|
20
|
+
States that must exist: draft (validation issues listed), staged review, submitting, pending (polling), completed, failed (reason shown, terminal).
|
|
21
|
+
Rules that must hold: money movement is stage-then-confirm - the review renders the exact staged request as an arithmetic ladder (working before the answer, uncertainty attached to the number); the commit button restates the amount and never carries a countdown; values are never masked on review; execution is single-shot on an idempotency key pinned at staging.`,
|
|
22
|
+
activity: `# Activity
|
|
23
|
+
Shell: full-width table + side panel.
|
|
24
|
+
Registry items: venly-tokens, data-table, status-pill, side-panel, timeline; block: activity.
|
|
25
|
+
Hooks: useTransfers (and useRampRequests where ramps are in scope).
|
|
26
|
+
States that must exist: loading, empty ledger, rows with pending/failed pills, open detail panel that stays in sync with refetches.
|
|
27
|
+
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
|
+
"onboarding-status": `# Onboarding / verification status
|
|
29
|
+
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.`,
|
|
34
|
+
reconciliation: `# Reconciliation
|
|
35
|
+
Shell: split pane (roughly one-third list, two-thirds evidence) - not a drawer.
|
|
36
|
+
Registry items: venly-tokens, data-table, side-panel, status-pill, field-list.
|
|
37
|
+
Hooks: reconcile_by_reference_code (MCP composite) or useVirtualBankAccounts + useTransfers joined on referenceCode.
|
|
38
|
+
States that must exist: matched, unmatched with candidate expectations, partial/many-to-one with a live shortfall figure, resolved.
|
|
39
|
+
Rules that must hold: show per-signal match rationale (which fields agree), never a bare score; keep zero-counts visible - an empty exception queue is information; keyboard row-stepping for review throughput.`,
|
|
40
|
+
"proof-of-segregation": `# Proof of segregation
|
|
41
|
+
Shell: content column, single card.
|
|
42
|
+
Registry items: venly-tokens, field-list, balance-card.
|
|
43
|
+
Hooks: useWallets, useAccount; on-chain balance beside the ledger figure.
|
|
44
|
+
States that must exist: reconciled (figures agree, timestamped), reconciling, source unavailable (say so - never render a stale figure as current).
|
|
45
|
+
Rules that must hold: the wallet address renders monospace with copy; the on-chain figure and ledger figure sit side by side with their as-of times; discrepancies are stated, not smoothed.`,
|
|
46
|
+
approvals: `# Approvals
|
|
47
|
+
Shell: full-width queue + side panel tailored to the approver.
|
|
48
|
+
Registry items: venly-tokens, data-table, status-pill, side-panel, timeline.
|
|
49
|
+
Hooks: useRampRequests, useFourEyesApproval (capability decides what renders), useRampLifecycle.
|
|
50
|
+
States that must exist: queue with awaiting-approval items, detail with the decision context beside the figures, applied, stale-version (someone acted first - refetch and re-decide), creator-view (cannot approve own request - render the rule, not a disabled mystery button).
|
|
51
|
+
Rules that must hold: the optimistic-locking version travels with every decision; a 409 means re-decide against fresh state, never auto-retry; reject requires a reason; the creator sees why they cannot approve.`,
|
|
52
|
+
};
|
|
53
|
+
const JOURNEY_KEYS = Object.keys(JOURNEYS);
|
|
54
|
+
/** Deterministic design audit. Text in, findings out - no model, no taste. */
|
|
55
|
+
export function reviewScreenSource(source) {
|
|
56
|
+
const findings = [];
|
|
57
|
+
const push = (rule, severity, evidence, fix) => findings.push({ rule, severity, evidence: evidence.slice(0, 120), fix });
|
|
58
|
+
for (const match of source.matchAll(/#[0-9a-fA-F]{3,8}\b|rgba?\([^)]*\)/g)) {
|
|
59
|
+
push("raw-colour", "error", match[0], "Read colours from the venly-tokens custom properties; a reskin must be tokens.css and nothing else.");
|
|
60
|
+
}
|
|
61
|
+
for (const match of source.matchAll(/-\d[\d,]*\.\d{2}\s*(?:[A-Z]{3}|€|\$|£)/g)) {
|
|
62
|
+
push("hyphen-minus-amount", "error", match[0], "Use the true minus sign − before negative amounts (the Money primitive does this).");
|
|
63
|
+
}
|
|
64
|
+
// Only a RENDERED cancelled state counts (a quoted/JSX label or a state
|
|
65
|
+
// value), never the verb "cancel" in prose or a token file's comment; and
|
|
66
|
+
// only the ✓ glyph counts as the violation, never the mere NAME of a
|
|
67
|
+
// success token nearby.
|
|
68
|
+
for (const match of source.matchAll(/(?:["'>`]|\bstate\s*[:=]\s*["'])\s*cancell?ed\b/gi)) {
|
|
69
|
+
const idx = match.index ?? 0;
|
|
70
|
+
const around = source.slice(Math.max(0, idx - 150), idx + 150);
|
|
71
|
+
if (/✓/.test(around)) {
|
|
72
|
+
push("success-on-cancelled", "error", around.trim().slice(0, 80), "A cancelled or failed terminal step must never carry a success check - grey ↺ or red ✕.");
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (/review|confirm/i.test(source) && /[•*]{3,}/.test(source)) {
|
|
77
|
+
push("masked-review-value", "error", source.match(/[•*]{3,}/)[0], "Never mask values on a review screen; its only job is legibility of what is about to happen.");
|
|
78
|
+
}
|
|
79
|
+
if (/nth-child\(\s*(?:even|odd|2n)/.test(source)) {
|
|
80
|
+
push("zebra-striping", "warn", "nth-child(even/odd) background", "No finance reference uses zebra striping - separate rows with hairlines and spacing.");
|
|
81
|
+
}
|
|
82
|
+
if (/box-shadow/.test(source) && !/var\(--shadow-overlay\)/.test(source)) {
|
|
83
|
+
push("shadow-outside-overlay", "warn", source.match(/box-shadow[^;"}]*/)?.[0] ?? "box-shadow", "Elevation is only for overlays, and only via the --shadow-overlay token; the base layer is flat.");
|
|
84
|
+
}
|
|
85
|
+
if (/linear-gradient|radial-gradient/.test(source)) {
|
|
86
|
+
push("gradient-surface", "warn", source.match(/\w+-gradient\([^)]*\)/)?.[0] ?? "gradient", "Gradient balance heroes read as template, not product; surfaces are flat neutrals with one accent.");
|
|
87
|
+
}
|
|
88
|
+
if (/(?:status|state)/i.test(source) && /var\(--state-/.test(source)) {
|
|
89
|
+
if (!/[✓✕↺⚠●○]|aria-hidden/.test(source)) {
|
|
90
|
+
push("colour-only-state", "warn", "state colours present without any glyph", "Pair every state hue with a glyph or word so status survives greyscale.");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return findings;
|
|
94
|
+
}
|
|
95
|
+
const AGENTS_TEXT = `# Composition rules for coding agents building on the Venly UI registry
|
|
96
|
+
|
|
97
|
+
Delivery: add the registry once to components.json -
|
|
98
|
+
{ "registries": { "@venlyfinance": "${REGISTRY_URL_TEMPLATE}" } }
|
|
99
|
+
then install blocks with the shadcn CLI, e.g. \`npx shadcn@latest add @venlyfinance/receive\`.
|
|
100
|
+
Each block auto-installs its components, the venly-tokens file and the
|
|
101
|
+
@venlyfinance/react data layer. Import tokens.css once at the app root.
|
|
102
|
+
|
|
103
|
+
1. Never hand-roll API calls, auth, retries, or transfer state - every read
|
|
104
|
+
is a hook, every regulated lifecycle is a flow machine from
|
|
105
|
+
@venlyfinance/react. Wrap the tree once in <VenlyProvider environment="mock">.
|
|
106
|
+
2. Mock mode is the default for any demo or first build: zero credentials,
|
|
107
|
+
zero network. Never place a clientSecret in browser code - the provider
|
|
108
|
+
throws; use proxyClientOptions() against your own backend for production.
|
|
109
|
+
3. Money movement is stage-then-confirm: render the review, restate the
|
|
110
|
+
amount on the commit button, execute once.
|
|
111
|
+
4. Approval UIs render the rule, not the error: use the capability object;
|
|
112
|
+
on "stale-version" refetch and let the operator re-decide.
|
|
113
|
+
5. Theme by editing the installed venly-tokens css file and nothing else.
|
|
114
|
+
6. Before declaring a screen done, run the review_screen tool on its source
|
|
115
|
+
and fix every error-severity finding. Consult get_journey_blueprint
|
|
116
|
+
before designing a screen the registry has no block for.
|
|
117
|
+
`;
|
|
118
|
+
export function registerFrontendTools(server) {
|
|
119
|
+
server.registerTool("get_journey_blueprint", {
|
|
120
|
+
title: "Get a journey blueprint",
|
|
121
|
+
description: "Screen inventory, required states, registry items and binding hooks for one money-product journey. Consult before designing any screen.",
|
|
122
|
+
inputSchema: {
|
|
123
|
+
journey: z.enum(JOURNEY_KEYS).describe("Which journey to blueprint"),
|
|
124
|
+
},
|
|
125
|
+
}, async ({ journey }) => ({
|
|
126
|
+
content: [{ type: "text", text: JOURNEYS[journey] }],
|
|
127
|
+
}));
|
|
128
|
+
server.registerTool("review_screen", {
|
|
129
|
+
title: "Design-audit a screen",
|
|
130
|
+
description: "Deterministic audit of component/markup source against the kit's design contract: raw colours, hyphen-minus amounts, success styling on cancelled steps, masked review values, zebra striping, off-token shadows, gradients, colour-only state. Returns findings, not a score.",
|
|
131
|
+
inputSchema: {
|
|
132
|
+
source: z.string().min(1).describe("The component/markup/CSS source to audit"),
|
|
133
|
+
},
|
|
134
|
+
}, async ({ source }) => {
|
|
135
|
+
const findings = reviewScreenSource(source);
|
|
136
|
+
return {
|
|
137
|
+
content: [
|
|
138
|
+
{
|
|
139
|
+
type: "text",
|
|
140
|
+
text: JSON.stringify({
|
|
141
|
+
findings,
|
|
142
|
+
summary: findings.length === 0
|
|
143
|
+
? "No contract violations detected."
|
|
144
|
+
: `${findings.filter((f) => f.severity === "error").length} error(s), ${findings.filter((f) => f.severity === "warn").length} warning(s). Fix every error before declaring the screen done.`,
|
|
145
|
+
}, null, 2),
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
};
|
|
149
|
+
});
|
|
150
|
+
server.registerResource("frontend-agents", "venly://frontend/agents", {
|
|
151
|
+
title: "UI composition rules for coding agents",
|
|
152
|
+
description: "How to assemble a money-product frontend from the @venlyfinance registry and react package.",
|
|
153
|
+
mimeType: "text/markdown",
|
|
154
|
+
}, async () => ({
|
|
155
|
+
contents: [
|
|
156
|
+
{
|
|
157
|
+
uri: "venly://frontend/agents",
|
|
158
|
+
mimeType: "text/markdown",
|
|
159
|
+
text: AGENTS_TEXT,
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
}));
|
|
163
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -17,8 +17,15 @@ async function main() {
|
|
|
17
17
|
const transport = new StdioServerTransport();
|
|
18
18
|
await server.connect(transport);
|
|
19
19
|
// Log to stderr only (stdout is the MCP channel). No credentials here.
|
|
20
|
+
// The banner states what writes actually do in THIS environment - wording is
|
|
21
|
+
// the agent's safety surface, so it must match observed behavior exactly.
|
|
20
22
|
const armed = process.env[LIVE_FLAG] === "1";
|
|
21
|
-
|
|
23
|
+
const writeState = client.environment === "mock"
|
|
24
|
+
? "writes execute against local fixtures - no network, no credentials, nothing real"
|
|
25
|
+
: armed
|
|
26
|
+
? "writes ARMED (VENLY_MCP_LIVE=1): confirmed writes hit the live API"
|
|
27
|
+
: "writes DISARMED: mutations return dry-run previews (arming needs confirm:true + VENLY_MCP_LIVE=1 + credentials)";
|
|
28
|
+
process.stderr.write(`venly-finance-mcp started in ${client.environment}. ${writeState}.\n`);
|
|
22
29
|
}
|
|
23
30
|
main().catch((err) => {
|
|
24
31
|
process.stderr.write(`Fatal: ${err.message}\n`);
|
package/dist/prompts.js
CHANGED
|
@@ -24,16 +24,17 @@ export function registerBuilderPrompts(server) {
|
|
|
24
24
|
|
|
25
25
|
Use this operating brief:
|
|
26
26
|
|
|
27
|
-
1. Read venly://capabilities, venly://safety
|
|
27
|
+
1. Read venly://capabilities, venly://safety, venly://workflows/international-account and venly://frontend/agents before writing code.
|
|
28
28
|
2. Start in explicit mock mode with VENLY_ENV=mock. Keep all simulated states visibly labelled Mock.
|
|
29
29
|
3. Use @venlyfinance/sdk in server-side code. Never put Venly credentials or access tokens in browser code.
|
|
30
|
-
4.
|
|
31
|
-
5.
|
|
32
|
-
6.
|
|
33
|
-
7.
|
|
34
|
-
8.
|
|
35
|
-
9.
|
|
36
|
-
10.
|
|
30
|
+
4. Assemble the interface instead of inventing it: register the @venlyfinance registry in components.json (URL template in venly://frontend/agents), install the receive/send/activity blocks with the shadcn CLI, wrap the tree in <VenlyProvider environment="mock"> from @venlyfinance/react, and import the installed venly-tokens css once at the app root. Consult get_journey_blueprint before designing any screen the registry has no block for, and run every finished screen through review_screen, fixing all error-severity findings.
|
|
31
|
+
5. Build the customer experience around atomic Finance capabilities: party, account, auto-provisioned wallet and balances, EUR receiving account, transfer and status/reconciliation.
|
|
32
|
+
6. Do not claim that creating a party completes KYC/KYB. Display verification and pending states honestly.
|
|
33
|
+
7. Venly supplies financial infrastructure through regulated partners. Do not describe the application or its customer as a licensed bank unless separately verified.
|
|
34
|
+
8. EUR/SEPA virtual bank accounts are documented. Validate ${targetGeography ?? "the requested geography"} and any broader currency/coverage requirement instead of inferring support.
|
|
35
|
+
9. Card issuing is not exposed by the current Finance contract; do not invent a card feature.
|
|
36
|
+
10. Require an explicit user decision before switching to staging, adding credentials or arming writes. Dry-run staging mutations before confirmation.
|
|
37
|
+
11. Produce a concise README showing mock setup, the unchanged SDK business logic and the explicit staging transition.
|
|
37
38
|
|
|
38
39
|
Success means a credible money-product experience backed by real Venly contract shapes – not a generic dashboard and not a claim that the MCP itself generated a regulated bank.`,
|
|
39
40
|
},
|
package/dist/reconcile.d.ts
CHANGED
|
@@ -20,4 +20,11 @@ export interface ReconcileResult {
|
|
|
20
20
|
currency: string | null;
|
|
21
21
|
note: string;
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Normalize a payment reference the way bank remittance text must be read:
|
|
25
|
+
* uppercase, alphanumerics only. Payer banks freely re-case, strip or pad
|
|
26
|
+
* separators, so "ref-abc-123", "REF ABC 123" and "invoice REFABC123 thanks"
|
|
27
|
+
* must all find REF-ABC-123.
|
|
28
|
+
*/
|
|
29
|
+
export declare function normalizeReference(text: string): string;
|
|
23
30
|
export declare function reconcileByReferenceCode(referenceCode: string, virtualBankAccounts: VirtualBankAccount[], transactions: ObservedBankTransaction[]): ReconcileResult;
|
package/dist/reconcile.js
CHANGED
|
@@ -6,16 +6,34 @@
|
|
|
6
6
|
* vIBANs via the VenlyClient and passes them in alongside the operator- or
|
|
7
7
|
* bank-feed-supplied transactions.
|
|
8
8
|
*/
|
|
9
|
+
/**
|
|
10
|
+
* Normalize a payment reference the way bank remittance text must be read:
|
|
11
|
+
* uppercase, alphanumerics only. Payer banks freely re-case, strip or pad
|
|
12
|
+
* separators, so "ref-abc-123", "REF ABC 123" and "invoice REFABC123 thanks"
|
|
13
|
+
* must all find REF-ABC-123.
|
|
14
|
+
*/
|
|
15
|
+
export function normalizeReference(text) {
|
|
16
|
+
return text.toUpperCase().replace(/[^A-Z0-9]/g, "");
|
|
17
|
+
}
|
|
9
18
|
export function reconcileByReferenceCode(referenceCode, virtualBankAccounts, transactions) {
|
|
10
19
|
const target = referenceCode.trim();
|
|
11
20
|
if (!target) {
|
|
12
21
|
throw new Error("referenceCode must not be blank");
|
|
13
22
|
}
|
|
14
|
-
const
|
|
23
|
+
const normalizedTarget = normalizeReference(target);
|
|
24
|
+
if (normalizedTarget.length < 4) {
|
|
25
|
+
throw new Error(`referenceCode "${target}" is too short after normalization ` +
|
|
26
|
+
`("${normalizedTarget}"): at least 4 alphanumeric characters are required ` +
|
|
27
|
+
"to match safely against free-form remittance text.");
|
|
28
|
+
}
|
|
29
|
+
// The vIBAN side is Venly-issued, so it matches exactly (after normalization).
|
|
30
|
+
const vban = virtualBankAccounts.find((v) => normalizeReference(v.referenceCode ?? "") === normalizedTarget) ?? null;
|
|
15
31
|
if (vban && !(vban.id ?? "").trim()) {
|
|
16
32
|
throw new Error("matching vIBAN is missing an id");
|
|
17
33
|
}
|
|
18
|
-
|
|
34
|
+
// The transaction side is free-form remittance text typed by a payer, so a
|
|
35
|
+
// containment test on the normalized text is the honest match.
|
|
36
|
+
const matchedTransactions = transactions.filter((t) => normalizeReference(t.referenceCode ?? "").includes(normalizedTarget));
|
|
19
37
|
const totalAmount = matchedTransactions.reduce((sum, t) => sum + (Number.isFinite(t.amount) ? t.amount : 0), 0);
|
|
20
38
|
const currency = vban?.currency ?? matchedTransactions[0]?.currency ?? null;
|
|
21
39
|
const matched = vban !== null && matchedTransactions.length > 0;
|
package/dist/resources.js
CHANGED
|
@@ -33,7 +33,7 @@ Current boundaries:
|
|
|
33
33
|
description: "Environment, write, compliance and secret-handling rules.",
|
|
34
34
|
text: `# Venly Finance MCP safety
|
|
35
35
|
|
|
36
|
-
- Set VENLY_ENV explicitly to mock, staging or production. An absent value
|
|
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.
|
|
37
37
|
- Mock mode uses synthetic SDK fixtures, no credentials and no network. Every mutation result is labelled mode=mock.
|
|
38
38
|
- Staging writes require confirm=true, VENLY_MCP_LIVE=1 and VENLY_CLIENT_ID/VENLY_CLIENT_SECRET.
|
|
39
39
|
- Production requires every staging gate plus VENLY_MCP_PRODUCTION=1.
|
package/dist/safety.d.ts
CHANGED
|
@@ -26,6 +26,8 @@ export declare function credentialsPresent(env: EnvLike): boolean;
|
|
|
26
26
|
export declare function evaluateWriteGate(confirm: boolean, env: EnvLike): GateDecision;
|
|
27
27
|
export interface DryRunRequest {
|
|
28
28
|
mode: "dry-run";
|
|
29
|
+
/** Explicit on every mutation result: nothing was persisted. */
|
|
30
|
+
dryRun: true;
|
|
29
31
|
environment: VenlyEnvironment;
|
|
30
32
|
tool: string;
|
|
31
33
|
method: "GET" | "POST" | "PUT" | "DELETE";
|
package/dist/safety.js
CHANGED
package/dist/server.js
CHANGED
|
@@ -10,6 +10,7 @@ import { registerWriteTools } from "./tools/write-tools.js";
|
|
|
10
10
|
import { registerX402Tools } from "./tools/x402-tools.js";
|
|
11
11
|
import { registerBuilderResources } from "./resources.js";
|
|
12
12
|
import { registerBuilderPrompts } from "./prompts.js";
|
|
13
|
+
import { registerFrontendTools } from "./frontend.js";
|
|
13
14
|
export function createServer(options) {
|
|
14
15
|
const env = options.env ?? process.env;
|
|
15
16
|
// The write gate auto-arms every mutation in mock mode on the assumption
|
|
@@ -29,6 +30,7 @@ export function createServer(options) {
|
|
|
29
30
|
registerReadTools(server, options.client);
|
|
30
31
|
registerWriteTools(server, options.client, env);
|
|
31
32
|
registerX402Tools(server);
|
|
33
|
+
registerFrontendTools(server);
|
|
32
34
|
registerBuilderResources(server);
|
|
33
35
|
registerBuilderPrompts(server);
|
|
34
36
|
return server;
|
package/dist/staging-smoke.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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", "
|
|
2
|
-
export declare const EXPECTED_RESOURCE_URIS: readonly ["venly://capabilities", "venly://safety", "venly://workflows/international-account", "venly://workflows/mock-to-staging"];
|
|
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"];
|
|
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 {
|
|
5
5
|
tools: string[];
|
package/dist/staging-smoke.js
CHANGED
|
@@ -22,17 +22,19 @@ export const EXPECTED_TOOLS = [
|
|
|
22
22
|
"create_virtual_bank_account",
|
|
23
23
|
"create_fiat_transfer",
|
|
24
24
|
"create_crypto_transfer",
|
|
25
|
-
"stage_transfer",
|
|
26
25
|
"approve_ramp_request",
|
|
27
26
|
"reject_ramp_request",
|
|
28
27
|
"create_payment_session",
|
|
29
28
|
"quote_x402_payment",
|
|
29
|
+
"get_journey_blueprint",
|
|
30
|
+
"review_screen",
|
|
30
31
|
];
|
|
31
32
|
export const EXPECTED_RESOURCE_URIS = [
|
|
32
33
|
"venly://capabilities",
|
|
33
34
|
"venly://safety",
|
|
34
35
|
"venly://workflows/international-account",
|
|
35
36
|
"venly://workflows/mock-to-staging",
|
|
37
|
+
"venly://frontend/agents",
|
|
36
38
|
];
|
|
37
39
|
export const EXPECTED_PROMPTS = ["build_international_account"];
|
|
38
40
|
function assertExactMembers(label, expected, actual) {
|
package/dist/tools/read-tools.js
CHANGED
|
@@ -144,14 +144,20 @@ export function registerReadTools(server, client) {
|
|
|
144
144
|
description: "Match observed incoming bank transactions on an account's EUR vIBANs to " +
|
|
145
145
|
"the vIBAN whose referenceCode they carry. Fetches the account's vIBANs " +
|
|
146
146
|
"(finance GET .../virtual-bank-accounts) and matches against the supplied " +
|
|
147
|
-
"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 " +
|
|
148
152
|
"transactions, and total amount.",
|
|
149
153
|
inputSchema: {
|
|
150
154
|
accountId: z.string().describe("Account UUID whose vIBANs to reconcile against"),
|
|
151
155
|
referenceCode: z.string().describe("The reference code to reconcile"),
|
|
152
156
|
transactions: z
|
|
153
157
|
.array(z.object({
|
|
154
|
-
referenceCode: z
|
|
158
|
+
referenceCode: z
|
|
159
|
+
.string()
|
|
160
|
+
.describe("Remittance text as received - free-form is fine; matching normalizes it"),
|
|
155
161
|
amount: z.number(),
|
|
156
162
|
currency: z.string(),
|
|
157
163
|
remitterName: z.string().optional(),
|
|
@@ -9,10 +9,12 @@
|
|
|
9
9
|
import { z } from "zod";
|
|
10
10
|
import { buildDryRun, evaluateWriteGate } from "../safety.js";
|
|
11
11
|
import { errorResult, jsonResult } from "../results.js";
|
|
12
|
-
import { normalizeLegacyFiatTransfer } from "../client/sdk-client.js";
|
|
13
12
|
function executionResult(gate, result) {
|
|
14
13
|
return jsonResult({
|
|
15
14
|
mode: gate.environment === "mock" ? "mock" : "live",
|
|
15
|
+
// Explicit on every mutation result: this call DID execute (against local
|
|
16
|
+
// fixtures in mock, against the real API when armed).
|
|
17
|
+
dryRun: false,
|
|
16
18
|
environment: gate.environment,
|
|
17
19
|
result,
|
|
18
20
|
});
|
|
@@ -154,8 +156,14 @@ export function registerWriteTools(server, client, env) {
|
|
|
154
156
|
description: "Create an account-to-account transfer using the current Finance OpenAPI fields. Dry-run by default outside explicit mock mode.",
|
|
155
157
|
inputSchema: {
|
|
156
158
|
senderAccountId: z.string(),
|
|
157
|
-
receiverAccountId: z
|
|
158
|
-
|
|
159
|
+
receiverAccountId: z
|
|
160
|
+
.string()
|
|
161
|
+
.optional()
|
|
162
|
+
.describe("Receiver's Venly account id. Exactly one of receiverAccountId / receiverExternalId is required."),
|
|
163
|
+
receiverExternalId: z
|
|
164
|
+
.string()
|
|
165
|
+
.optional()
|
|
166
|
+
.describe("Receiver's integrator-assigned externalId. Exactly one of receiverAccountId / receiverExternalId is required."),
|
|
159
167
|
currency: z.enum(["EUR", "GBP", "USD"]),
|
|
160
168
|
amount: z.number(),
|
|
161
169
|
description: z.string().optional(),
|
|
@@ -165,8 +173,8 @@ export function registerWriteTools(server, client, env) {
|
|
|
165
173
|
},
|
|
166
174
|
annotations: WRITE_ANNOTATIONS,
|
|
167
175
|
}, async ({ senderAccountId, confirm, ...input }) => {
|
|
168
|
-
if (!input.receiverAccountId
|
|
169
|
-
return errorResult("
|
|
176
|
+
if (!input.receiverAccountId === !input.receiverExternalId) {
|
|
177
|
+
return errorResult("Provide exactly one of receiverAccountId or receiverExternalId - a transfer needs one receiver, addressed one way.");
|
|
170
178
|
}
|
|
171
179
|
const body = {
|
|
172
180
|
...input,
|
|
@@ -188,8 +196,14 @@ export function registerWriteTools(server, client, env) {
|
|
|
188
196
|
description: "Create an account-to-account asset transfer using the current Finance OpenAPI fields. Dry-run by default outside explicit mock mode.",
|
|
189
197
|
inputSchema: {
|
|
190
198
|
senderAccountId: z.string(),
|
|
191
|
-
receiverAccountId: z
|
|
192
|
-
|
|
199
|
+
receiverAccountId: z
|
|
200
|
+
.string()
|
|
201
|
+
.optional()
|
|
202
|
+
.describe("Receiver's Venly account id. Exactly one of receiverAccountId / receiverExternalId is required."),
|
|
203
|
+
receiverExternalId: z
|
|
204
|
+
.string()
|
|
205
|
+
.optional()
|
|
206
|
+
.describe("Receiver's integrator-assigned externalId. Exactly one of receiverAccountId / receiverExternalId is required."),
|
|
193
207
|
chain: z.enum(["AVALANCHE", "BASE", "POLYGON"]),
|
|
194
208
|
asset: z.string().min(1),
|
|
195
209
|
amount: z.number(),
|
|
@@ -200,8 +214,8 @@ export function registerWriteTools(server, client, env) {
|
|
|
200
214
|
},
|
|
201
215
|
annotations: WRITE_ANNOTATIONS,
|
|
202
216
|
}, async ({ senderAccountId, confirm, ...input }) => {
|
|
203
|
-
if (!input.receiverAccountId
|
|
204
|
-
return errorResult("
|
|
217
|
+
if (!input.receiverAccountId === !input.receiverExternalId) {
|
|
218
|
+
return errorResult("Provide exactly one of receiverAccountId or receiverExternalId - a transfer needs one receiver, addressed one way.");
|
|
205
219
|
}
|
|
206
220
|
const body = {
|
|
207
221
|
...input,
|
|
@@ -218,62 +232,6 @@ export function registerWriteTools(server, client, env) {
|
|
|
218
232
|
return errorResult(e.message);
|
|
219
233
|
}
|
|
220
234
|
});
|
|
221
|
-
server.registerTool("stage_transfer", {
|
|
222
|
-
title: "Stage a fiat transfer (dry-run by default)",
|
|
223
|
-
description: "Stage a fiat-to-crypto transfer (finance POST /accounts/{senderAccountId}/transfers/fiat). " +
|
|
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.",
|
|
227
|
-
inputSchema: {
|
|
228
|
-
senderAccountId: z.string().describe("Account initiating the transfer"),
|
|
229
|
-
receiverAccountId: z.string(),
|
|
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\""),
|
|
234
|
-
fiatCurrency: z.string().describe("e.g. EUR"),
|
|
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."),
|
|
240
|
-
description: z.string().optional(),
|
|
241
|
-
merchantReference: z.string().optional(),
|
|
242
|
-
confirm: confirmField,
|
|
243
|
-
},
|
|
244
|
-
annotations: WRITE_ANNOTATIONS,
|
|
245
|
-
}, async ({ senderAccountId, confirm, ...rest }) => {
|
|
246
|
-
const gate = evaluateWriteGate(confirm, env);
|
|
247
|
-
const legacyInput = {
|
|
248
|
-
receiverAccountId: rest.receiverAccountId,
|
|
249
|
-
fiatAmount: rest.fiatAmount,
|
|
250
|
-
fiatCurrency: rest.fiatCurrency,
|
|
251
|
-
cryptocurrency: rest.cryptocurrency,
|
|
252
|
-
description: rest.description,
|
|
253
|
-
merchantReference: rest.merchantReference,
|
|
254
|
-
idempotencyKey: crypto.randomUUID(),
|
|
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
|
-
}
|
|
266
|
-
if (!gate.armed) {
|
|
267
|
-
return jsonResult(buildDryRun("stage_transfer", "POST", "finance", `/accounts/${senderAccountId}/transfers/fiat`, body, gate));
|
|
268
|
-
}
|
|
269
|
-
try {
|
|
270
|
-
const result = await client.createFiatTransfer(senderAccountId, legacyInput);
|
|
271
|
-
return executionResult(gate, result);
|
|
272
|
-
}
|
|
273
|
-
catch (e) {
|
|
274
|
-
return errorResult(e.message);
|
|
275
|
-
}
|
|
276
|
-
});
|
|
277
235
|
server.registerTool("approve_ramp_request", {
|
|
278
236
|
title: "Approve a ramp request (dry-run by default)",
|
|
279
237
|
description: "Approve a ramp request through four-eyes (fundflow POST /v1/ramp-requests/{id}/approve). " +
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@venlyfinance/settlement-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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.
|
|
30
|
+
"@venlyfinance/sdk": "^0.2.0",
|
|
31
31
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
32
32
|
"zod": "^3.23.8"
|
|
33
33
|
},
|
|
@@ -11,13 +11,13 @@ fiat to crypto.
|
|
|
11
11
|
## Tools
|
|
12
12
|
|
|
13
13
|
- `get_account` (read)
|
|
14
|
-
- `
|
|
14
|
+
- `create_fiat_transfer` (write, disarmed by default)
|
|
15
15
|
- `get_transfer` (read)
|
|
16
16
|
|
|
17
17
|
## Steps
|
|
18
18
|
|
|
19
19
|
1. `get_account` for the `senderAccountId` to confirm it is active.
|
|
20
|
-
2. Stage the transfer: call `
|
|
20
|
+
2. Stage the transfer: call `create_fiat_transfer` with `senderAccountId`,
|
|
21
21
|
`receiverAccountId`, `fiatAmount` (decimal string), `fiatCurrency`, and
|
|
22
22
|
optionally `cryptocurrency`, `description`, `merchantReference`. Omit
|
|
23
23
|
`confirm` (or set it false).
|