@venlyfinance/settlement-mcp 0.1.1 → 0.2.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 +54 -0
- package/README.md +98 -31
- package/dist/client/sdk-client.d.ts +67 -0
- package/dist/client/sdk-client.js +183 -0
- package/dist/constants.d.ts +6 -2
- package/dist/constants.js +11 -2
- package/dist/index.d.ts +5 -4
- package/dist/index.js +8 -7
- package/dist/prompts.d.ts +2 -0
- package/dist/prompts.js +43 -0
- package/dist/reconcile.js +6 -0
- package/dist/resources.d.ts +2 -0
- package/dist/resources.js +106 -0
- package/dist/results.d.ts +18 -0
- package/dist/results.js +25 -0
- package/dist/safety.d.ts +5 -1
- package/dist/safety.js +27 -4
- package/dist/server.js +15 -1
- package/dist/staging-smoke.d.ts +17 -0
- package/dist/staging-smoke.js +199 -0
- package/dist/tools/read-tools.js +83 -12
- package/dist/tools/write-tools.js +226 -20
- package/dist/types.d.ts +69 -127
- package/dist/types.js +5 -13
- package/package.json +18 -10
- package/scripts/staging-smoke.mjs +13 -0
- package/skills/build-international-account.md +26 -0
- package/skills/mock-to-staging.md +11 -0
- package/dist/client/http-client.d.ts +0 -58
- package/dist/client/http-client.js +0 -163
package/dist/types.js
CHANGED
|
@@ -1,17 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Generated API contracts + the injectable VenlyClient interface.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* echo are modeled. Fields are intentionally loose (optional) because this is a
|
|
9
|
-
* thin wrapper, not a full SDK.
|
|
10
|
-
*
|
|
11
|
-
* TRANSPORT NOTE: the bundled HttpVenlyClient is a deliberately minimal fetch
|
|
12
|
-
* transport (see client/http-client.ts). A future release replaces it with a
|
|
13
|
-
* thin adapter over `@venlyfinance/sdk` with no change to this interface;
|
|
14
|
-
* until then the minimal transport is what ships. When that lands, replace
|
|
15
|
-
* HttpVenlyClient with a thin adapter over it and delete the vendored transport.
|
|
4
|
+
* Finance and Fundflow resources and requests are aliases to the types exported
|
|
5
|
+
* by `@venlyfinance/sdk`. Only MCP-owned inputs and compatibility shapes are
|
|
6
|
+
* declared locally. This prevents the MCP from silently drifting away from the
|
|
7
|
+
* vendored OpenAPI specifications.
|
|
16
8
|
*/
|
|
17
9
|
export {};
|
package/package.json
CHANGED
|
@@ -1,28 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@venlyfinance/settlement-mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Venly
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Venly Finance MCP: SDK-backed tools, resources and prompts for building international money products safely.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
+
"venly-finance-mcp": "dist/index.js",
|
|
7
8
|
"venly-settlement-mcp": "dist/index.js"
|
|
8
9
|
},
|
|
9
10
|
"main": "dist/index.js",
|
|
10
11
|
"files": [
|
|
11
12
|
"dist",
|
|
13
|
+
"scripts",
|
|
12
14
|
"skills",
|
|
13
|
-
"README.md"
|
|
15
|
+
"README.md",
|
|
16
|
+
"CHANGELOG.md"
|
|
14
17
|
],
|
|
15
18
|
"scripts": {
|
|
16
|
-
"build": "tsc -p tsconfig.json",
|
|
19
|
+
"build": "rm -rf dist && tsc -p tsconfig.json",
|
|
17
20
|
"start": "node dist/index.js",
|
|
21
|
+
"smoke:staging": "npm run build && node scripts/staging-smoke.mjs",
|
|
18
22
|
"test": "node --import tsx --test test/*.test.ts",
|
|
19
|
-
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
23
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
24
|
+
"typecheck:test": "tsc -p tsconfig.test.json"
|
|
20
25
|
},
|
|
21
26
|
"engines": {
|
|
22
|
-
"node": ">=
|
|
27
|
+
"node": ">=20"
|
|
23
28
|
},
|
|
24
29
|
"dependencies": {
|
|
25
|
-
"@
|
|
30
|
+
"@venlyfinance/sdk": "^0.1.1",
|
|
31
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
26
32
|
"zod": "^3.23.8"
|
|
27
33
|
},
|
|
28
34
|
"devDependencies": {
|
|
@@ -33,12 +39,12 @@
|
|
|
33
39
|
"license": "MIT",
|
|
34
40
|
"repository": {
|
|
35
41
|
"type": "git",
|
|
36
|
-
"url": "git+https://github.com/
|
|
42
|
+
"url": "git+https://github.com/Venly/venly-settlement-sdk.git",
|
|
37
43
|
"directory": "settlement-mcp"
|
|
38
44
|
},
|
|
39
|
-
"homepage": "https://github.com/
|
|
45
|
+
"homepage": "https://github.com/Venly/venly-settlement-sdk/tree/main/settlement-mcp#readme",
|
|
40
46
|
"bugs": {
|
|
41
|
-
"url": "https://github.com/
|
|
47
|
+
"url": "https://github.com/Venly/venly-settlement-sdk/issues"
|
|
42
48
|
},
|
|
43
49
|
"keywords": [
|
|
44
50
|
"mcp",
|
|
@@ -46,6 +52,8 @@
|
|
|
46
52
|
"stablecoin",
|
|
47
53
|
"settlement",
|
|
48
54
|
"payments",
|
|
55
|
+
"finance-api",
|
|
56
|
+
"ai-agents",
|
|
49
57
|
"x402",
|
|
50
58
|
"venly",
|
|
51
59
|
"fintech",
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { sanitizeErrorMessage } from "../dist/results.js";
|
|
4
|
+
import { runStagingSmoke } from "../dist/staging-smoke.js";
|
|
5
|
+
|
|
6
|
+
try {
|
|
7
|
+
await runStagingSmoke();
|
|
8
|
+
console.log("\nSTAGING SMOKE PASSED: discovery and reads succeeded; writes stayed dry-run.");
|
|
9
|
+
} catch (error) {
|
|
10
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
11
|
+
console.error(`\nSTAGING SMOKE FAILED: ${sanitizeErrorMessage(message)}`);
|
|
12
|
+
process.exitCode = 1;
|
|
13
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Build an international account experience
|
|
2
|
+
|
|
3
|
+
Use this skill when a user asks an AI coding agent to build an international account,
|
|
4
|
+
stablecoin account or neobank-like customer experience with Venly Finance.
|
|
5
|
+
|
|
6
|
+
## Rules
|
|
7
|
+
|
|
8
|
+
1. Read `venly://capabilities`, `venly://safety` and
|
|
9
|
+
`venly://workflows/international-account` when MCP resources are supported.
|
|
10
|
+
2. Start with `VENLY_ENV=mock`; label every simulated state.
|
|
11
|
+
3. Use `@venlyfinance/sdk` only in server-side application code. Never expose Venly
|
|
12
|
+
credentials or access tokens to the browser.
|
|
13
|
+
4. Assemble atomic capabilities: party, account, auto-provisioned wallet/balances, EUR
|
|
14
|
+
receiving account, transfer, status and reconciliation.
|
|
15
|
+
5. Creating a party does not complete KYC/KYB. Show returned compliance states.
|
|
16
|
+
6. Venly provides infrastructure through regulated partners. Do not imply a bank
|
|
17
|
+
charter, deposit insurance or unsupported geographic/currency coverage.
|
|
18
|
+
7. Card issuing is not exposed by the current Finance OpenAPI contract.
|
|
19
|
+
8. Ask for an explicit decision before moving to staging or arming a write. Dry-run
|
|
20
|
+
live-environment mutations before confirmation.
|
|
21
|
+
|
|
22
|
+
## Outcome
|
|
23
|
+
|
|
24
|
+
Produce a credible customer-facing money-product experience and a README documenting
|
|
25
|
+
the mock setup and explicit staging transition. Do not build a generic infrastructure
|
|
26
|
+
dashboard or hide financial mutations inside one autonomous operation.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Move a Venly Finance build from mock to staging
|
|
2
|
+
|
|
3
|
+
1. Keep the working mock flow and its visible environment label.
|
|
4
|
+
2. Set `VENLY_ENV=staging` and provide client credentials through server-side secret
|
|
5
|
+
storage.
|
|
6
|
+
3. Confirm enabled custody model, chains, assets and regulated-partner coverage.
|
|
7
|
+
4. Use a documented KYC-verified staging account before provisioning a EUR vIBAN.
|
|
8
|
+
5. Run read-only smoke checks before setting `VENLY_MCP_LIVE=1`.
|
|
9
|
+
6. Dry-run each intended write, review the normalized request, then explicitly confirm.
|
|
10
|
+
7. Never fall back implicitly to mock when staging authentication or capability checks
|
|
11
|
+
fail.
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* HttpVenlyClient: a minimal fetch-based transport implementing VenlyClient.
|
|
3
|
-
*
|
|
4
|
-
* TRANSPORT NOTE
|
|
5
|
-
* --------------
|
|
6
|
-
* Minimal by design: OAuth2 client credentials, lazy token fetch, staging
|
|
7
|
-
* defaults. A future release replaces this with a thin adapter over
|
|
8
|
-
* `@venlyfinance/sdk` (single-flight token refresh, automatic idempotency
|
|
9
|
-
* keys, retry/backoff, richer errors) with no change to the tool interface.
|
|
10
|
-
*
|
|
11
|
-
* Safety invariants honored here:
|
|
12
|
-
* - credentials are read from env ONLY, never logged, never returned in output.
|
|
13
|
-
* - no request is issued at construction time; tokens are fetched lazily.
|
|
14
|
-
* - this class does not know about the write gate. It only issues a live call
|
|
15
|
-
* when a write method is invoked, and write methods are invoked only after
|
|
16
|
-
* the gate in safety.ts is armed. Read-only by default is enforced upstream.
|
|
17
|
-
*/
|
|
18
|
-
import type { Account, CreateFiatTransferInput, CreatePayInSessionRequest, ListRampRequestsParams, OptimisticLockingBody, Party, PaymentSession, RampRequestDto, RampRequestListItem, Transfer, VenlyClient, VirtualBankAccount } from "../types.js";
|
|
19
|
-
export interface HttpVenlyClientConfig {
|
|
20
|
-
financeBaseUrl?: string;
|
|
21
|
-
fundflowBaseUrl?: string;
|
|
22
|
-
tokenUrl?: string;
|
|
23
|
-
clientId?: string;
|
|
24
|
-
clientSecret?: string;
|
|
25
|
-
/** Injectable for tests; defaults to global fetch. */
|
|
26
|
-
fetchImpl?: typeof fetch;
|
|
27
|
-
}
|
|
28
|
-
export declare class HttpVenlyClient implements VenlyClient {
|
|
29
|
-
private readonly financeBaseUrl;
|
|
30
|
-
private readonly fundflowBaseUrl;
|
|
31
|
-
private readonly tokenUrl;
|
|
32
|
-
private readonly clientId?;
|
|
33
|
-
private readonly clientSecret?;
|
|
34
|
-
private readonly fetchImpl;
|
|
35
|
-
private token;
|
|
36
|
-
constructor(config?: HttpVenlyClientConfig);
|
|
37
|
-
/** Build a client from environment variables. Never logs credentials. */
|
|
38
|
-
static fromEnv(env?: NodeJS.ProcessEnv): HttpVenlyClient;
|
|
39
|
-
private getAccessToken;
|
|
40
|
-
private request;
|
|
41
|
-
getAccount(accountId: string): Promise<Account>;
|
|
42
|
-
listVirtualBankAccounts(accountId: string): Promise<VirtualBankAccount[]>;
|
|
43
|
-
getTransfer(accountId: string, transferId: string): Promise<Transfer>;
|
|
44
|
-
listParties(params?: {
|
|
45
|
-
page?: number;
|
|
46
|
-
size?: number;
|
|
47
|
-
}): Promise<Party[]>;
|
|
48
|
-
listRampRequests(params?: ListRampRequestsParams): Promise<RampRequestListItem[]>;
|
|
49
|
-
getRampRequest(id: string): Promise<RampRequestDto>;
|
|
50
|
-
getSupportedChains(): Promise<unknown[]>;
|
|
51
|
-
getFiatCurrencies(): Promise<unknown[]>;
|
|
52
|
-
getCryptocurrencies(): Promise<unknown[]>;
|
|
53
|
-
getCompanyFees(): Promise<unknown>;
|
|
54
|
-
createFiatTransfer(senderAccountId: string, body: CreateFiatTransferInput): Promise<Transfer>;
|
|
55
|
-
approveRampRequest(id: string, body: OptimisticLockingBody): Promise<RampRequestDto>;
|
|
56
|
-
rejectRampRequest(id: string, body: OptimisticLockingBody): Promise<RampRequestDto>;
|
|
57
|
-
createPayInSession(accountId: string, body: CreatePayInSessionRequest): Promise<PaymentSession>;
|
|
58
|
-
}
|
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* HttpVenlyClient: a minimal fetch-based transport implementing VenlyClient.
|
|
3
|
-
*
|
|
4
|
-
* TRANSPORT NOTE
|
|
5
|
-
* --------------
|
|
6
|
-
* Minimal by design: OAuth2 client credentials, lazy token fetch, staging
|
|
7
|
-
* defaults. A future release replaces this with a thin adapter over
|
|
8
|
-
* `@venlyfinance/sdk` (single-flight token refresh, automatic idempotency
|
|
9
|
-
* keys, retry/backoff, richer errors) with no change to the tool interface.
|
|
10
|
-
*
|
|
11
|
-
* Safety invariants honored here:
|
|
12
|
-
* - credentials are read from env ONLY, never logged, never returned in output.
|
|
13
|
-
* - no request is issued at construction time; tokens are fetched lazily.
|
|
14
|
-
* - this class does not know about the write gate. It only issues a live call
|
|
15
|
-
* when a write method is invoked, and write methods are invoked only after
|
|
16
|
-
* the gate in safety.ts is armed. Read-only by default is enforced upstream.
|
|
17
|
-
*/
|
|
18
|
-
import { DEFAULT_FINANCE_BASE_URL, DEFAULT_FUNDFLOW_BASE_URL, DEFAULT_TOKEN_URL, } from "../constants.js";
|
|
19
|
-
/** Unwrap the Venly `{ success, result, pagination }` envelope. */
|
|
20
|
-
function unwrap(payload) {
|
|
21
|
-
if (payload && typeof payload === "object" && "result" in payload) {
|
|
22
|
-
return payload.result;
|
|
23
|
-
}
|
|
24
|
-
return payload;
|
|
25
|
-
}
|
|
26
|
-
export class HttpVenlyClient {
|
|
27
|
-
financeBaseUrl;
|
|
28
|
-
fundflowBaseUrl;
|
|
29
|
-
tokenUrl;
|
|
30
|
-
clientId;
|
|
31
|
-
clientSecret;
|
|
32
|
-
fetchImpl;
|
|
33
|
-
token = null;
|
|
34
|
-
constructor(config = {}) {
|
|
35
|
-
this.financeBaseUrl = config.financeBaseUrl ?? DEFAULT_FINANCE_BASE_URL;
|
|
36
|
-
this.fundflowBaseUrl = config.fundflowBaseUrl ?? DEFAULT_FUNDFLOW_BASE_URL;
|
|
37
|
-
this.tokenUrl = config.tokenUrl ?? DEFAULT_TOKEN_URL;
|
|
38
|
-
this.clientId = config.clientId;
|
|
39
|
-
this.clientSecret = config.clientSecret;
|
|
40
|
-
this.fetchImpl = config.fetchImpl ?? fetch;
|
|
41
|
-
}
|
|
42
|
-
/** Build a client from environment variables. Never logs credentials. */
|
|
43
|
-
static fromEnv(env = process.env) {
|
|
44
|
-
return new HttpVenlyClient({
|
|
45
|
-
financeBaseUrl: env.VENLY_FINANCE_BASE_URL,
|
|
46
|
-
fundflowBaseUrl: env.VENLY_FUNDFLOW_BASE_URL,
|
|
47
|
-
tokenUrl: env.VENLY_TOKEN_URL,
|
|
48
|
-
clientId: env.VENLY_CLIENT_ID,
|
|
49
|
-
clientSecret: env.VENLY_CLIENT_SECRET,
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
async getAccessToken() {
|
|
53
|
-
const now = Date.now();
|
|
54
|
-
if (this.token && this.token.expiresAt > now + 5_000) {
|
|
55
|
-
return this.token.accessToken;
|
|
56
|
-
}
|
|
57
|
-
if (!this.clientId || !this.clientSecret) {
|
|
58
|
-
// Do not include any credential material in the error.
|
|
59
|
-
throw new Error("Missing Venly credentials. Set VENLY_CLIENT_ID and VENLY_CLIENT_SECRET.");
|
|
60
|
-
}
|
|
61
|
-
const body = new URLSearchParams({
|
|
62
|
-
grant_type: "client_credentials",
|
|
63
|
-
client_id: this.clientId,
|
|
64
|
-
client_secret: this.clientSecret,
|
|
65
|
-
});
|
|
66
|
-
const res = await this.fetchImpl(this.tokenUrl, {
|
|
67
|
-
method: "POST",
|
|
68
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
69
|
-
body,
|
|
70
|
-
});
|
|
71
|
-
if (!res.ok) {
|
|
72
|
-
// Never echo the request body (contains the secret).
|
|
73
|
-
throw new Error(`Token request failed with status ${res.status}`);
|
|
74
|
-
}
|
|
75
|
-
const json = (await res.json());
|
|
76
|
-
const expiresInMs = (json.expires_in ?? 300) * 1000;
|
|
77
|
-
this.token = {
|
|
78
|
-
accessToken: json.access_token,
|
|
79
|
-
expiresAt: now + expiresInMs,
|
|
80
|
-
};
|
|
81
|
-
return json.access_token;
|
|
82
|
-
}
|
|
83
|
-
async request(base, method, path, opts = {}) {
|
|
84
|
-
const token = await this.getAccessToken();
|
|
85
|
-
const url = new URL(path.replace(/^\//, ""), base.endsWith("/") ? base : base + "/");
|
|
86
|
-
if (opts.query) {
|
|
87
|
-
for (const [k, v] of Object.entries(opts.query)) {
|
|
88
|
-
if (v !== undefined && v !== null)
|
|
89
|
-
url.searchParams.set(k, String(v));
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
const headers = {
|
|
93
|
-
Authorization: `Bearer ${token}`,
|
|
94
|
-
Accept: "application/json",
|
|
95
|
-
};
|
|
96
|
-
if (opts.body !== undefined) {
|
|
97
|
-
headers["Content-Type"] = "application/json";
|
|
98
|
-
// Idempotency for writes. Production SDK does this automatically.
|
|
99
|
-
headers["Idempotency-Key"] = crypto.randomUUID();
|
|
100
|
-
}
|
|
101
|
-
const res = await this.fetchImpl(url.toString(), {
|
|
102
|
-
method,
|
|
103
|
-
headers,
|
|
104
|
-
body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
|
|
105
|
-
});
|
|
106
|
-
if (!res.ok) {
|
|
107
|
-
throw new Error(`Venly API ${method} ${path} failed with status ${res.status}`);
|
|
108
|
-
}
|
|
109
|
-
const json = await res.json();
|
|
110
|
-
return unwrap(json);
|
|
111
|
-
}
|
|
112
|
-
// ----- READ (finance) -----
|
|
113
|
-
getAccount(accountId) {
|
|
114
|
-
return this.request(this.financeBaseUrl, "GET", `/accounts/${accountId}`);
|
|
115
|
-
}
|
|
116
|
-
listVirtualBankAccounts(accountId) {
|
|
117
|
-
return this.request(this.financeBaseUrl, "GET", `/accounts/${accountId}/virtual-bank-accounts`);
|
|
118
|
-
}
|
|
119
|
-
getTransfer(accountId, transferId) {
|
|
120
|
-
return this.request(this.financeBaseUrl, "GET", `/accounts/${accountId}/transfers/${transferId}`);
|
|
121
|
-
}
|
|
122
|
-
listParties(params = {}) {
|
|
123
|
-
return this.request(this.financeBaseUrl, "GET", "/parties", { query: params });
|
|
124
|
-
}
|
|
125
|
-
// ----- READ (fundflow) -----
|
|
126
|
-
listRampRequests(params = {}) {
|
|
127
|
-
return this.request(this.fundflowBaseUrl, "GET", "/v1/ramp-requests", {
|
|
128
|
-
query: params,
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
getRampRequest(id) {
|
|
132
|
-
return this.request(this.fundflowBaseUrl, "GET", `/v1/ramp-requests/${id}`);
|
|
133
|
-
}
|
|
134
|
-
getSupportedChains() {
|
|
135
|
-
return this.request(this.fundflowBaseUrl, "GET", "/v1/chains");
|
|
136
|
-
}
|
|
137
|
-
getFiatCurrencies() {
|
|
138
|
-
return this.request(this.fundflowBaseUrl, "GET", "/v1/fiat-currencies");
|
|
139
|
-
}
|
|
140
|
-
getCryptocurrencies() {
|
|
141
|
-
return this.request(this.fundflowBaseUrl, "GET", "/v1/crypto-currencies");
|
|
142
|
-
}
|
|
143
|
-
getCompanyFees() {
|
|
144
|
-
return this.request(this.fundflowBaseUrl, "GET", "/v1/fees");
|
|
145
|
-
}
|
|
146
|
-
// ----- WRITE -----
|
|
147
|
-
createFiatTransfer(senderAccountId, body) {
|
|
148
|
-
return this.request(this.financeBaseUrl, "POST", `/accounts/${senderAccountId}/transfers/fiat`, { body });
|
|
149
|
-
}
|
|
150
|
-
approveRampRequest(id, body) {
|
|
151
|
-
return this.request(this.fundflowBaseUrl, "POST", `/v1/ramp-requests/${id}/approve`, {
|
|
152
|
-
body,
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
rejectRampRequest(id, body) {
|
|
156
|
-
return this.request(this.fundflowBaseUrl, "POST", `/v1/ramp-requests/${id}/reject`, {
|
|
157
|
-
body,
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
createPayInSession(accountId, body) {
|
|
161
|
-
return this.request(this.financeBaseUrl, "POST", `/accounts/${accountId}/fiat-to-crypto/payment-sessions`, { body });
|
|
162
|
-
}
|
|
163
|
-
}
|