@venlyfinance/settlement-mcp 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -0
- package/README.md +1 -1
- package/dist/constants.d.ts +3 -3
- package/dist/constants.js +7 -4
- package/dist/index.js +8 -1
- 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/tools/read-tools.js +8 -2
- package/dist/tools/write-tools.js +28 -10
- package/package.json +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
|
package/README.md
CHANGED
|
@@ -190,7 +190,7 @@ Override via env:
|
|
|
190
190
|
|
|
191
191
|
| Env var | Default (staging) |
|
|
192
192
|
|---|---|
|
|
193
|
-
| `VENLY_ENV` | `
|
|
193
|
+
| `VENLY_ENV` | Defaults to `mock` (since 0.3.0): an unconfigured server never points at real infrastructure. Set `staging` or `production` explicitly |
|
|
194
194
|
| `VENLY_FINANCE_BASE_URL` | `https://api-staging.venlyfinance.com/v1` |
|
|
195
195
|
| `VENLY_FUNDFLOW_BASE_URL` | `https://api-fundflow-staging.venly.io` |
|
|
196
196
|
| `VENLY_TOKEN_URL` | `https://login-staging.venly.io/auth/realms/VenlyFinance/protocol/openid-connect/token` (staging; use `login.venly.io` for production) |
|
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.3.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.3.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
|
}
|
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/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/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(),
|
|
@@ -13,6 +13,9 @@ import { normalizeLegacyFiatTransfer } from "../client/sdk-client.js";
|
|
|
13
13
|
function executionResult(gate, result) {
|
|
14
14
|
return jsonResult({
|
|
15
15
|
mode: gate.environment === "mock" ? "mock" : "live",
|
|
16
|
+
// Explicit on every mutation result: this call DID execute (against local
|
|
17
|
+
// fixtures in mock, against the real API when armed).
|
|
18
|
+
dryRun: false,
|
|
16
19
|
environment: gate.environment,
|
|
17
20
|
result,
|
|
18
21
|
});
|
|
@@ -154,8 +157,14 @@ export function registerWriteTools(server, client, env) {
|
|
|
154
157
|
description: "Create an account-to-account transfer using the current Finance OpenAPI fields. Dry-run by default outside explicit mock mode.",
|
|
155
158
|
inputSchema: {
|
|
156
159
|
senderAccountId: z.string(),
|
|
157
|
-
receiverAccountId: z
|
|
158
|
-
|
|
160
|
+
receiverAccountId: z
|
|
161
|
+
.string()
|
|
162
|
+
.optional()
|
|
163
|
+
.describe("Receiver's Venly account id. Exactly one of receiverAccountId / receiverExternalId is required."),
|
|
164
|
+
receiverExternalId: z
|
|
165
|
+
.string()
|
|
166
|
+
.optional()
|
|
167
|
+
.describe("Receiver's integrator-assigned externalId. Exactly one of receiverAccountId / receiverExternalId is required."),
|
|
159
168
|
currency: z.enum(["EUR", "GBP", "USD"]),
|
|
160
169
|
amount: z.number(),
|
|
161
170
|
description: z.string().optional(),
|
|
@@ -165,8 +174,8 @@ export function registerWriteTools(server, client, env) {
|
|
|
165
174
|
},
|
|
166
175
|
annotations: WRITE_ANNOTATIONS,
|
|
167
176
|
}, async ({ senderAccountId, confirm, ...input }) => {
|
|
168
|
-
if (!input.receiverAccountId
|
|
169
|
-
return errorResult("
|
|
177
|
+
if (!input.receiverAccountId === !input.receiverExternalId) {
|
|
178
|
+
return errorResult("Provide exactly one of receiverAccountId or receiverExternalId - a transfer needs one receiver, addressed one way.");
|
|
170
179
|
}
|
|
171
180
|
const body = {
|
|
172
181
|
...input,
|
|
@@ -188,8 +197,14 @@ export function registerWriteTools(server, client, env) {
|
|
|
188
197
|
description: "Create an account-to-account asset transfer using the current Finance OpenAPI fields. Dry-run by default outside explicit mock mode.",
|
|
189
198
|
inputSchema: {
|
|
190
199
|
senderAccountId: z.string(),
|
|
191
|
-
receiverAccountId: z
|
|
192
|
-
|
|
200
|
+
receiverAccountId: z
|
|
201
|
+
.string()
|
|
202
|
+
.optional()
|
|
203
|
+
.describe("Receiver's Venly account id. Exactly one of receiverAccountId / receiverExternalId is required."),
|
|
204
|
+
receiverExternalId: z
|
|
205
|
+
.string()
|
|
206
|
+
.optional()
|
|
207
|
+
.describe("Receiver's integrator-assigned externalId. Exactly one of receiverAccountId / receiverExternalId is required."),
|
|
193
208
|
chain: z.enum(["AVALANCHE", "BASE", "POLYGON"]),
|
|
194
209
|
asset: z.string().min(1),
|
|
195
210
|
amount: z.number(),
|
|
@@ -200,8 +215,8 @@ export function registerWriteTools(server, client, env) {
|
|
|
200
215
|
},
|
|
201
216
|
annotations: WRITE_ANNOTATIONS,
|
|
202
217
|
}, async ({ senderAccountId, confirm, ...input }) => {
|
|
203
|
-
if (!input.receiverAccountId
|
|
204
|
-
return errorResult("
|
|
218
|
+
if (!input.receiverAccountId === !input.receiverExternalId) {
|
|
219
|
+
return errorResult("Provide exactly one of receiverAccountId or receiverExternalId - a transfer needs one receiver, addressed one way.");
|
|
205
220
|
}
|
|
206
221
|
const body = {
|
|
207
222
|
...input,
|
|
@@ -219,8 +234,11 @@ export function registerWriteTools(server, client, env) {
|
|
|
219
234
|
}
|
|
220
235
|
});
|
|
221
236
|
server.registerTool("stage_transfer", {
|
|
222
|
-
title: "
|
|
223
|
-
description: "
|
|
237
|
+
title: "DEPRECATED - use create_fiat_transfer",
|
|
238
|
+
description: "DEPRECATED: legacy alias of create_fiat_transfer kept for 0.1.x compatibility; " +
|
|
239
|
+
"it will be removed in 0.4.0. Prefer create_fiat_transfer, whose inputs match the " +
|
|
240
|
+
"current OpenAPI contract directly. " +
|
|
241
|
+
"Stages a fiat-to-crypto transfer (finance POST /accounts/{senderAccountId}/transfers/fiat). " +
|
|
224
242
|
"Legacy fiatAmount/fiatCurrency inputs are normalized to the current OpenAPI fields; " +
|
|
225
243
|
"the dry-run shows the exact normalized request. DISARMED by default: returns that " +
|
|
226
244
|
"request without sending unless confirm:true AND VENLY_MCP_LIVE=1 AND credentials are present.",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@venlyfinance/settlement-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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
|
},
|