@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/dist/types.js CHANGED
@@ -1,17 +1,9 @@
1
1
  /**
2
- * Domain types + the injectable VenlyClient interface.
2
+ * Generated API contracts + the injectable VenlyClient interface.
3
3
  *
4
- * These shapes are a minimal projection of the published OpenAPI specs
5
- * vendored in this repository under `specs/` finance.yaml (servers:
6
- * https://api.venlyfinance.com/v1) and fundflow.yaml (servers:
7
- * https://api-fundflow.venly.io). Only the fields the tools actually read or
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.1.1",
4
- "description": "Venly Settlement MCP server. Human-gated operator surface over the Venly Finance and Fundflow APIs, plus an x402 machine-to-machine quote stub. Read-only by default, write tools fail closed.",
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": ">=18"
27
+ "node": ">=20"
23
28
  },
24
29
  "dependencies": {
25
- "@modelcontextprotocol/sdk": "^1.29.0",
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/timdierckxsens/venly-sdk.git",
42
+ "url": "git+https://github.com/Venly/venly-settlement-sdk.git",
37
43
  "directory": "settlement-mcp"
38
44
  },
39
- "homepage": "https://github.com/timdierckxsens/venly-sdk/tree/main/settlement-mcp#readme",
45
+ "homepage": "https://github.com/Venly/venly-settlement-sdk/tree/main/settlement-mcp#readme",
40
46
  "bugs": {
41
- "url": "https://github.com/timdierckxsens/venly-sdk/issues"
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
- }