@venlyfinance/settlement-mcp 0.6.0 → 0.7.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/AGENTS.md +2 -2
- package/CHANGELOG.md +21 -0
- package/README.md +25 -4
- package/dist/constants.d.ts +2 -1
- package/dist/constants.js +2 -1
- package/dist/frontend.d.ts +4 -0
- package/dist/frontend.js +376 -3
- package/dist/index.js +11 -0
- package/dist/server.js +3 -1
- package/dist/staging-smoke.d.ts +1 -1
- package/dist/staging-smoke.js +1 -0
- package/dist/verify-cli.d.ts +24 -0
- package/dist/verify-cli.js +371 -0
- package/package.json +2 -2
package/AGENTS.md
CHANGED
|
@@ -9,7 +9,7 @@ You are building a financial product UI on `@venlyfinance/react`. These rules ex
|
|
|
9
9
|
3. **Never place `clientSecret` in browser code.** The provider throws if you try. For browser apps use `proxyClientOptions()` against your own backend route.
|
|
10
10
|
4. **Money movement is stage-then-confirm.** Render a review step showing `state.staged` (the exact request) before calling `confirm()`. Never wire a form submit directly to execution.
|
|
11
11
|
5. **Approval UIs render the rule, not the error.** Use `capability` from `useFourEyesApproval`: when `reason` is `"actor-is-creator"`, say that a second person must approve – do not show buttons that will be refused. On failure `"stale-version"`, refetch and let the operator re-decide; never auto-retry an approval.
|
|
12
|
-
6. **Gate
|
|
12
|
+
6. **Gate both the screen and its runtime contract, and wire both into CI.** Run `npx @venlyfinance/settlement-mcp review "src/**/*.tsx"` for the design contract and `npx @venlyfinance/settlement-mcp verify "src/**/*.{ts,tsx}"` for package/provider/hook/proxy composition. Each fails (exit 1) on any error-severity finding. A deliberate, justified exception carries `venly-allow:<rule-id>` on the offending line or the line above.
|
|
13
13
|
|
|
14
14
|
## Rendering money states
|
|
15
15
|
|
|
@@ -32,4 +32,4 @@ The UI kit's auth and team blocks therefore render against two adapter interface
|
|
|
32
32
|
## Demo choreography (mock mode)
|
|
33
33
|
|
|
34
34
|
`useVenlyMock()` exposes the store controls. A credible end-to-end demo:
|
|
35
|
-
create party → `advanceVerification(id)` → create account → virtual bank account (note its `referenceCode`) → stage + confirm a transfer → `advanceTransfer(id)` → show the ledger. Inject failures with `failNext("CONFLICT")` to show the stale-version approval path – error states are part of the product.
|
|
35
|
+
create party → `advanceVerification(id)` → create account → virtual bank account (note its `referenceCode`) → **fund it: `simulations.inbound.credit(vbaId, 500)`** (a new account holds nothing, as in production; an unfunded transfer is refused with `402 insufficient-funds`) → stage + confirm a transfer → `advanceTransfer(id)` → show the ledger, and `simulations.ledger.verify()` to prove it balances. Inject failures with `failNext("CONFLICT")` to show the stale-version approval path – error states are part of the product.
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.7.0 – 2026-08-19
|
|
4
|
+
|
|
5
|
+
The MCP now pushes a machine-checkable runtime contract instead of relying on
|
|
6
|
+
agents to discover composition guidance through optional pull surfaces.
|
|
7
|
+
|
|
8
|
+
- The initialize response carries server instructions that establish
|
|
9
|
+
`@venlyfinance/react` hooks and flow machines as the browser data plane,
|
|
10
|
+
`@venlyfinance/sdk` as the server data plane, and the shadcn registry as the
|
|
11
|
+
UI installation path.
|
|
12
|
+
- Every `get_journey_blueprint` response keeps its prose blueprint and adds the
|
|
13
|
+
same `runtime_contract` as fenced JSON and `structuredContent`: exact registry
|
|
14
|
+
dependencies, qualified hook imports, provider configuration, forbidden
|
|
15
|
+
hand-rolled patterns, install commands, and completion gates.
|
|
16
|
+
- New deterministic `verify` CLI and `verify_runtime_contract` MCP tool with
|
|
17
|
+
auto-detected `direct-sdk` and `backend-proxy` profiles. Exit codes match the
|
|
18
|
+
`review` CLI (0 clean, 1 on errors, 2 on usage/no-match), and
|
|
19
|
+
`venly-allow:<rule-id>` suppressions work on verifier findings.
|
|
20
|
+
- Money-route-without-SDK, in-memory-money-store, and direct-profile missing
|
|
21
|
+
React checks remain warning-only pending a false-positive-boundary ruling.
|
|
22
|
+
- Requires `@venlyfinance/sdk` ^0.5.0.
|
|
23
|
+
|
|
3
24
|
## 0.3.0 – 2026-08-04
|
|
4
25
|
|
|
5
26
|
Wording-is-the-safety-surface release. An outside integrator audit (2026-08-04) found the server's words disagreeing with its behavior in three
|
package/README.md
CHANGED
|
@@ -13,9 +13,9 @@ Start in explicit mock mode with no credentials or network. Move the same SDK
|
|
|
13
13
|
business logic to staging only after reviewing capabilities, compliance state and
|
|
14
14
|
the normalized write requests. Staging and production writes fail closed.
|
|
15
15
|
|
|
16
|
-
The Venly Finance builder surface documented here is the v0.
|
|
16
|
+
The Venly Finance builder surface documented here is the v0.7.0 release line.
|
|
17
17
|
|
|
18
|
-
Building with a coding agent? Start from [AGENTS.md](AGENTS.md) - the MCP also serves it as the
|
|
18
|
+
Building with a coding agent? Start from [AGENTS.md](AGENTS.md) - the MCP also serves it as the `venly://frontend/agents` resource and pushes the core runtime doctrine in its initialize response.
|
|
19
19
|
|
|
20
20
|
## What it is
|
|
21
21
|
|
|
@@ -124,8 +124,12 @@ Delivery of UI source rides the shadcn registry standard – add
|
|
|
124
124
|
to `components.json`, then `npx shadcn@latest add @venlyfinance/receive`. The MCP carries
|
|
125
125
|
what a registry cannot:
|
|
126
126
|
|
|
127
|
-
- `get_journey_blueprint` – screen inventory
|
|
128
|
-
|
|
127
|
+
- `get_journey_blueprint` – screen inventory and required states for eleven
|
|
128
|
+
money-product journeys, plus a machine-readable `runtime_contract` containing
|
|
129
|
+
exact package versions, qualified hooks, provider setup, forbidden patterns,
|
|
130
|
+
install commands and completion checks.
|
|
131
|
+
- `verify_runtime_contract` – deterministic runtime-contract checks over supplied
|
|
132
|
+
source and package metadata, using the same profiles and rules as the CLI.
|
|
129
133
|
- `review_screen` – deterministic design audit of a screen's source (raw colours,
|
|
130
134
|
hyphen-minus amounts, success styling on cancelled steps, masked review values,
|
|
131
135
|
invented timing/custody copy, crypto codes inside `Intl.NumberFormat` currency
|
|
@@ -157,6 +161,22 @@ Scope the glob to component source, never to token or theme files: the
|
|
|
157
161
|
css file is the one legitimate home of raw colour values (theming stays a
|
|
158
162
|
one-file edit). `"src/**/*.tsx"` is the right default.
|
|
159
163
|
|
|
164
|
+
### Runtime-contract CLI
|
|
165
|
+
|
|
166
|
+
Gate the data-plane composition alongside the screen audit:
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
npx @venlyfinance/settlement-mcp verify "src/**/*.{ts,tsx}"
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
The verifier auto-detects `direct-sdk` (browser provider + hooks) or
|
|
173
|
+
`backend-proxy` (browser proxy options + SDK-backed server routes). Override
|
|
174
|
+
with `--profile direct-sdk` or `--profile backend-proxy`. Exit codes are `0`
|
|
175
|
+
when no error findings exist, `1` when any error exists, and `2` for usage or
|
|
176
|
+
no-match failures; warnings print but do not fail. The same
|
|
177
|
+
`venly-allow:<rule-id>` token suppresses a deliberate finding on its line or
|
|
178
|
+
the line above.
|
|
179
|
+
|
|
160
180
|
## Safety model (fail closed)
|
|
161
181
|
|
|
162
182
|
Outside explicit mock mode, read-only/dry-run is the default posture. A staging
|
|
@@ -323,6 +343,7 @@ settlement-mcp/
|
|
|
323
343
|
resources.ts capability, safety and workflow resources
|
|
324
344
|
prompts.ts build_international_account prompt
|
|
325
345
|
results.ts text + structured output and error redaction
|
|
346
|
+
verify-cli.ts runtime-contract profiles, checks and CLI
|
|
326
347
|
staging-smoke.ts safe discovery/read/dry-run staging verification
|
|
327
348
|
client/
|
|
328
349
|
sdk-client.ts Adapter over @venlyfinance/sdk
|
package/dist/constants.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/** Shared constants. The default environment is MOCK so an unconfigured run
|
|
2
2
|
* never touches real infrastructure; staging/production are explicit. */
|
|
3
3
|
export declare const SERVER_NAME = "venly-finance-mcp-server";
|
|
4
|
-
export declare const SERVER_VERSION = "0.
|
|
4
|
+
export declare const SERVER_VERSION = "0.7.0";
|
|
5
|
+
export declare const INSTRUCTIONS = "Venly Finance build advisor. This server is a build-time advisor, not your app's data plane. The data plane is the published packages: every read is a hook and every regulated lifecycle a flow machine from `@venlyfinance/react`, inside `<VenlyProvider environment=\"mock\">` \u2013 zero credentials, zero network; server-side code uses `@venlyfinance/sdk`. Hand-rolled fetch layers, in-memory money stores, or route handlers that re-implement transfers, balances, or approvals are off-contract and fail review. UI installs from the @venlyfinance shadcn registry: `npx shadcn@latest add @venlyfinance/balances @venlyfinance/send \u2026` (auto-installs the npm packages). Before scaffolding, read `venly://frontend/agents` \u2013 it is the composition doctrine (AGENTS.md). Consult `get_journey_blueprint` per screen; gate finished screens with `review_screen` and `npx @venlyfinance/settlement-mcp review \"src/**/*.tsx\"`.";
|
|
5
6
|
export declare const ENVIRONMENT_FLAG = "VENLY_ENV";
|
|
6
7
|
export type VenlyEnvironment = "mock" | "qa" | "staging" | "production";
|
|
7
8
|
export declare function resolveVenlyEnvironment(env: Record<string, string | undefined>): VenlyEnvironment;
|
package/dist/constants.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/** Shared constants. The default environment is MOCK so an unconfigured run
|
|
2
2
|
* never touches real infrastructure; staging/production are explicit. */
|
|
3
3
|
export const SERVER_NAME = "venly-finance-mcp-server";
|
|
4
|
-
export const SERVER_VERSION = "0.
|
|
4
|
+
export const SERVER_VERSION = "0.7.0";
|
|
5
|
+
export const INSTRUCTIONS = `Venly Finance build advisor. This server is a build-time advisor, not your app's data plane. The data plane is the published packages: every read is a hook and every regulated lifecycle a flow machine from \`@venlyfinance/react\`, inside \`<VenlyProvider environment="mock">\` – zero credentials, zero network; server-side code uses \`@venlyfinance/sdk\`. Hand-rolled fetch layers, in-memory money stores, or route handlers that re-implement transfers, balances, or approvals are off-contract and fail review. UI installs from the @venlyfinance shadcn registry: \`npx shadcn@latest add @venlyfinance/balances @venlyfinance/send …\` (auto-installs the npm packages). Before scaffolding, read \`venly://frontend/agents\` – it is the composition doctrine (AGENTS.md). Consult \`get_journey_blueprint\` per screen; gate finished screens with \`review_screen\` and \`npx @venlyfinance/settlement-mcp review "src/**/*.tsx"\`.`;
|
|
5
6
|
export const ENVIRONMENT_FLAG = "VENLY_ENV";
|
|
6
7
|
export function resolveVenlyEnvironment(env) {
|
|
7
8
|
// Default is MOCK (since 0.3.0): the mock-first product must not point at
|
package/dist/frontend.d.ts
CHANGED
|
@@ -21,6 +21,10 @@ declare const JOURNEYS: {
|
|
|
21
21
|
readonly reconciliation: "# Reconciliation\nShell: split pane (roughly one-third list, two-thirds evidence) - not a drawer.\nRegistry items: venly-tokens, data-table, side-panel, status-pill, field-list.\nHooks: reconcile_by_reference_code (MCP composite) or useVirtualBankAccounts + useTransfers joined on referenceCode.\nStates that must exist: matched, unmatched with candidate expectations, partial/many-to-one with a live shortfall figure, resolved.\nRules 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.";
|
|
22
22
|
readonly "proof-of-segregation": "# Proof of segregation\nShell: content column, single card.\nRegistry items: venly-tokens, field-list, balance-card.\nHooks: useWallets, useAccount; on-chain balance beside the ledger figure.\nStates that must exist: reconciled (figures agree, timestamped), reconciling, source unavailable (say so - never render a stale figure as current).\nRules 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.";
|
|
23
23
|
readonly approvals: "# Approvals\nShell: full-width queue + side panel tailored to the approver.\nRegistry items: venly-tokens, data-table, status-pill, side-panel, timeline.\nHooks: useRampRequests, useFourEyesApproval (capability decides what renders), useRampLifecycle.\nStates 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).\nRules 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.";
|
|
24
|
+
readonly "console-review-queue": "# Console review queue (the operator worklist)\nShell: left nav rail + thin top bar, full-width content, and a page-edge\nenvironment banner naming mock mode. Not a consumer surface - density rules\napply.\nRegistry items: venly-tokens, data-table, status-pill, money, list-error. The\nregistry has no console block yet: compose these primitives.\nHooks: useAccounts, useParties. The queue's own state is DERIVED on every\nrender - never stored, never cached as a status.\nBinding: sections are ACTORS, not statuses - your move, waiting on the customer,\nwaiting on a provider, then a collapsed closed section, so the reviewer's own\nworklist is the top band by construction.\nStates that must exist: loading, your move, waiting on the customer, waiting on a provider, closed, empty queue, filtered to nothing, list error.\nRules that must hold: the whose-move value is a pure function of enum values on\nthe row - no clock reading, no threshold, no configuration and no default, and a\ncombination the mapping does not cover renders NO value plus an explicit\nnot-recognised line, which is a bug report rather than a guess; never a target\ntime, a breach colour or an overdue state, because the API publishes no targets\nand an invented one is the same defect as an invented fee; an age column is\nlabelled for what it actually measures - a created-at delta is \"Age\", and only a\nduration the API itself computes may be called time in state; empty sections are\nstill drawn as a zero header row, because nothing-to-do is information; loading\nis a skeleton that preserves column geometry exactly, never prose; a row click\nopens a side panel and never navigates; one status pill per row - the whose-move\nvalue is plain text, since two pills read as two states.";
|
|
25
|
+
readonly "console-decision-detail": "# Console decision detail (evidence, ceremony, audit trail)\nShell: side panel about 30% wide over the queue - no scrim, the table stays\nvisible clipped at the panel edge and the source row stays tinted. Escalate to a\n35/65 split only when the evidence outgrows the panel. Evidence goes on the\nLEFT: this is a judging task, not an authoring one.\nRegistry items: venly-tokens, side-panel, timeline, field-list, status-pill,\nmoney, data-table.\nHooks: useAccount, useParty, useWallets, useTransfers,\nuseVirtualBankAccounts, useVenlyMock (the trail reads the mock's event log).\nTwo timeline columns, not one feed: the decision chain (who decided what, when,\nin which seat) beside money movement on the same subject. Different actors,\ndifferent audiences; merging them is what makes an audit trail unreadable.\nStates that must exist: loading, evidence present, evidence unavailable, decision owed, decision applied, stale decision, terminal decision, frozen, empty trail.\nRules that must hold: every evidence row is either a real field path or a\nlabelled omission, and an omission is a FIRST-CLASS type in the component's\nprops, so a placeholder cannot be rendered where a gap belongs;\nomission copy states only what is verified and never implies a result, a\npending state, or a clean one - and never mentions the API contract, which is\ndeveloper diagnostics rather than operator language; a field the API cannot\ncarry is captured anyway when the work needs it, and rendered with a visible\nbadge saying it is a console note rather than API state; every decision carries the\noptimistic-locking version, and a conflict means refetch and re-decide against\nfresh state, never auto-retry; every transition the console causes leaves a\ntimeline node with actor, role and a timezone-qualified absolute timestamp, so a\nstatus change with no node is a bug; a store resync is a system line, not a\ndecision node; the panel footer carries row-stepping key chips so the reviewer\nmoves row to row without closing.";
|
|
26
|
+
readonly "console-pricing-config": "# Console pricing configuration\nShell: in-shell content column. A config screen, not a queue: no whose-move\nvalue and no aging.\nRegistry items: venly-tokens, data-table, arithmetic-ladder, field-list.\nHooks: useCompanyFees.\nBinding: the fee data the packages actually serve is a VOLUME-TIER model - tier\nname, ramp direction, minimum and maximum volume, percentage, version - and it\nis the same model the shipped withdrawal quote consumes, so this screen shows\nwhere a real quote comes from. A second, richer per-rail configuration model\nexists on an internal plane and is NOT served here; it renders as a labelled\nomission, never as an empty form.\nStates that must exist: loading, tiers present, no tiers, configuration unavailable, worked example, save failed.\nRules that must hold: a worked arithmetic ladder is mandatory on the tier\nsection - a sample amount times the tier percentage, with the operator glyphs in\na left gutter - because a pricing screen that shows only stored numbers teaches\nnothing; the ladder renders ONLY over data that exists, never over the omitted\nsection; the tier\na sample amount falls into is highlighted in the table so the row and the ladder\nare visibly the same fact; a single-member enum renders as a disabled\nsingle-value field that says so, not a select pretending at choice; a date\nwindow that has not opened reads scheduled, never active; forms are single\ncolumn with the field width capped, label above input, helper text between them\nand the error below.";
|
|
27
|
+
readonly "console-simulator": "# Sandbox simulator (play the counterparty)\nShell: its own chrome - a scrimmed right-hand drawer on a distinct surface with\na persistent sandbox label, reachable from ONE fixed affordance in the top bar.\nIt is the only scrimmed drawer in the console, so the surface change alone\nsignals the register change.\nRegistry items: venly-tokens, field-list, status-pill, money.\nHooks: useVenlyMock. Every control maps to exactly one call on the mock's\nsimulations namespace - no control without a call, and no call renamed.\nBinding: inbound credits, provider progression and screening verdicts are things\nOTHER parties do, so they live here rather than in the operator's workflow.\nStates that must exist: drawer closed, drawer open, sharing, not sharing, credit landed, verdict returned, payout advanced, books balanced, books do not balance, reset.\nRules that must hold: controls are phrased as events that happen to you, in the\nthird person, while operator controls elsewhere are imperative decisions - a\ncontrol phrased in the wrong voice is in the wrong surface; a simulated\ntransition emits the SAME event the real path emits, and the trail attributes it\nto the simulator plainly rather than to an operator; the drawer is reachable\nonly from the top bar, never from a queue row or a decision panel, because those\npaths make another party's action look like the operator's; the ledger check gets\na visible surface: it is the one control here that asserts something true, namely\nthat the simulated books balance; the channel footer states the adapter, session\nand peer count, and says IN WORDS when the surface is not actually sharing - the\ndefault channel shares nothing and cross-context sharing is same-origin only, so\nwithout that line a two-context demo can prove nothing while looking correct.";
|
|
24
28
|
};
|
|
25
29
|
type JourneyKey = keyof typeof JOURNEYS;
|
|
26
30
|
interface Finding {
|
package/dist/frontend.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { verifyRuntimeContract } from "./verify-cli.js";
|
|
2
3
|
export const REGISTRY_URL_TEMPLATE = "https://raw.githubusercontent.com/Venly/venly-settlement-sdk/main/ui/r/{name}.json";
|
|
3
4
|
const JOURNEYS = {
|
|
4
5
|
auth: `# Auth (sign-in, 2FA, sign-up)
|
|
@@ -78,8 +79,331 @@ Registry items: venly-tokens, data-table, status-pill, side-panel, timeline.
|
|
|
78
79
|
Hooks: useRampRequests, useFourEyesApproval (capability decides what renders), useRampLifecycle.
|
|
79
80
|
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).
|
|
80
81
|
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.`,
|
|
82
|
+
"console-review-queue": `# Console review queue (the operator worklist)
|
|
83
|
+
Shell: left nav rail + thin top bar, full-width content, and a page-edge
|
|
84
|
+
environment banner naming mock mode. Not a consumer surface - density rules
|
|
85
|
+
apply.
|
|
86
|
+
Registry items: venly-tokens, data-table, status-pill, money, list-error. The
|
|
87
|
+
registry has no console block yet: compose these primitives.
|
|
88
|
+
Hooks: useAccounts, useParties. The queue's own state is DERIVED on every
|
|
89
|
+
render - never stored, never cached as a status.
|
|
90
|
+
Binding: sections are ACTORS, not statuses - your move, waiting on the customer,
|
|
91
|
+
waiting on a provider, then a collapsed closed section, so the reviewer's own
|
|
92
|
+
worklist is the top band by construction.
|
|
93
|
+
States that must exist: loading, your move, waiting on the customer, waiting on a provider, closed, empty queue, filtered to nothing, list error.
|
|
94
|
+
Rules that must hold: the whose-move value is a pure function of enum values on
|
|
95
|
+
the row - no clock reading, no threshold, no configuration and no default, and a
|
|
96
|
+
combination the mapping does not cover renders NO value plus an explicit
|
|
97
|
+
not-recognised line, which is a bug report rather than a guess; never a target
|
|
98
|
+
time, a breach colour or an overdue state, because the API publishes no targets
|
|
99
|
+
and an invented one is the same defect as an invented fee; an age column is
|
|
100
|
+
labelled for what it actually measures - a created-at delta is "Age", and only a
|
|
101
|
+
duration the API itself computes may be called time in state; empty sections are
|
|
102
|
+
still drawn as a zero header row, because nothing-to-do is information; loading
|
|
103
|
+
is a skeleton that preserves column geometry exactly, never prose; a row click
|
|
104
|
+
opens a side panel and never navigates; one status pill per row - the whose-move
|
|
105
|
+
value is plain text, since two pills read as two states.`,
|
|
106
|
+
"console-decision-detail": `# Console decision detail (evidence, ceremony, audit trail)
|
|
107
|
+
Shell: side panel about 30% wide over the queue - no scrim, the table stays
|
|
108
|
+
visible clipped at the panel edge and the source row stays tinted. Escalate to a
|
|
109
|
+
35/65 split only when the evidence outgrows the panel. Evidence goes on the
|
|
110
|
+
LEFT: this is a judging task, not an authoring one.
|
|
111
|
+
Registry items: venly-tokens, side-panel, timeline, field-list, status-pill,
|
|
112
|
+
money, data-table.
|
|
113
|
+
Hooks: useAccount, useParty, useWallets, useTransfers,
|
|
114
|
+
useVirtualBankAccounts, useVenlyMock (the trail reads the mock's event log).
|
|
115
|
+
Two timeline columns, not one feed: the decision chain (who decided what, when,
|
|
116
|
+
in which seat) beside money movement on the same subject. Different actors,
|
|
117
|
+
different audiences; merging them is what makes an audit trail unreadable.
|
|
118
|
+
States that must exist: loading, evidence present, evidence unavailable, decision owed, decision applied, stale decision, terminal decision, frozen, empty trail.
|
|
119
|
+
Rules that must hold: every evidence row is either a real field path or a
|
|
120
|
+
labelled omission, and an omission is a FIRST-CLASS type in the component's
|
|
121
|
+
props, so a placeholder cannot be rendered where a gap belongs;
|
|
122
|
+
omission copy states only what is verified and never implies a result, a
|
|
123
|
+
pending state, or a clean one - and never mentions the API contract, which is
|
|
124
|
+
developer diagnostics rather than operator language; a field the API cannot
|
|
125
|
+
carry is captured anyway when the work needs it, and rendered with a visible
|
|
126
|
+
badge saying it is a console note rather than API state; every decision carries the
|
|
127
|
+
optimistic-locking version, and a conflict means refetch and re-decide against
|
|
128
|
+
fresh state, never auto-retry; every transition the console causes leaves a
|
|
129
|
+
timeline node with actor, role and a timezone-qualified absolute timestamp, so a
|
|
130
|
+
status change with no node is a bug; a store resync is a system line, not a
|
|
131
|
+
decision node; the panel footer carries row-stepping key chips so the reviewer
|
|
132
|
+
moves row to row without closing.`,
|
|
133
|
+
"console-pricing-config": `# Console pricing configuration
|
|
134
|
+
Shell: in-shell content column. A config screen, not a queue: no whose-move
|
|
135
|
+
value and no aging.
|
|
136
|
+
Registry items: venly-tokens, data-table, arithmetic-ladder, field-list.
|
|
137
|
+
Hooks: useCompanyFees.
|
|
138
|
+
Binding: the fee data the packages actually serve is a VOLUME-TIER model - tier
|
|
139
|
+
name, ramp direction, minimum and maximum volume, percentage, version - and it
|
|
140
|
+
is the same model the shipped withdrawal quote consumes, so this screen shows
|
|
141
|
+
where a real quote comes from. A second, richer per-rail configuration model
|
|
142
|
+
exists on an internal plane and is NOT served here; it renders as a labelled
|
|
143
|
+
omission, never as an empty form.
|
|
144
|
+
States that must exist: loading, tiers present, no tiers, configuration unavailable, worked example, save failed.
|
|
145
|
+
Rules that must hold: a worked arithmetic ladder is mandatory on the tier
|
|
146
|
+
section - a sample amount times the tier percentage, with the operator glyphs in
|
|
147
|
+
a left gutter - because a pricing screen that shows only stored numbers teaches
|
|
148
|
+
nothing; the ladder renders ONLY over data that exists, never over the omitted
|
|
149
|
+
section; the tier
|
|
150
|
+
a sample amount falls into is highlighted in the table so the row and the ladder
|
|
151
|
+
are visibly the same fact; a single-member enum renders as a disabled
|
|
152
|
+
single-value field that says so, not a select pretending at choice; a date
|
|
153
|
+
window that has not opened reads scheduled, never active; forms are single
|
|
154
|
+
column with the field width capped, label above input, helper text between them
|
|
155
|
+
and the error below.`,
|
|
156
|
+
"console-simulator": `# Sandbox simulator (play the counterparty)
|
|
157
|
+
Shell: its own chrome - a scrimmed right-hand drawer on a distinct surface with
|
|
158
|
+
a persistent sandbox label, reachable from ONE fixed affordance in the top bar.
|
|
159
|
+
It is the only scrimmed drawer in the console, so the surface change alone
|
|
160
|
+
signals the register change.
|
|
161
|
+
Registry items: venly-tokens, field-list, status-pill, money.
|
|
162
|
+
Hooks: useVenlyMock. Every control maps to exactly one call on the mock's
|
|
163
|
+
simulations namespace - no control without a call, and no call renamed.
|
|
164
|
+
Binding: inbound credits, provider progression and screening verdicts are things
|
|
165
|
+
OTHER parties do, so they live here rather than in the operator's workflow.
|
|
166
|
+
States that must exist: drawer closed, drawer open, sharing, not sharing, credit landed, verdict returned, payout advanced, books balanced, books do not balance, reset.
|
|
167
|
+
Rules that must hold: controls are phrased as events that happen to you, in the
|
|
168
|
+
third person, while operator controls elsewhere are imperative decisions - a
|
|
169
|
+
control phrased in the wrong voice is in the wrong surface; a simulated
|
|
170
|
+
transition emits the SAME event the real path emits, and the trail attributes it
|
|
171
|
+
to the simulator plainly rather than to an operator; the drawer is reachable
|
|
172
|
+
only from the top bar, never from a queue row or a decision panel, because those
|
|
173
|
+
paths make another party's action look like the operator's; the ledger check gets
|
|
174
|
+
a visible surface: it is the one control here that asserts something true, namely
|
|
175
|
+
that the simulated books balance; the channel footer states the adapter, session
|
|
176
|
+
and peer count, and says IN WORDS when the surface is not actually sharing - the
|
|
177
|
+
default channel shares nothing and cross-context sharing is same-origin only, so
|
|
178
|
+
without that line a two-context demo can prove nothing while looking correct.`,
|
|
81
179
|
};
|
|
82
180
|
const JOURNEY_KEYS = Object.keys(JOURNEYS);
|
|
181
|
+
const RUNTIME_PACKAGES_BY_BLOCK = {
|
|
182
|
+
activity: {
|
|
183
|
+
"@venlyfinance/react": "^0.4.0",
|
|
184
|
+
"@venlyfinance/sdk": "^0.5.0",
|
|
185
|
+
"@tanstack/react-query": "^5.0.0",
|
|
186
|
+
},
|
|
187
|
+
auth: { "@radix-ui/react-one-time-password-field": "^0.1.16" },
|
|
188
|
+
balances: {
|
|
189
|
+
"@venlyfinance/react": "^0.4.0",
|
|
190
|
+
"@venlyfinance/sdk": "^0.5.0",
|
|
191
|
+
"@tanstack/react-query": "^5.0.0",
|
|
192
|
+
},
|
|
193
|
+
"bank-accounts": {
|
|
194
|
+
"@venlyfinance/react": "^0.4.0",
|
|
195
|
+
"@venlyfinance/sdk": "^0.5.0",
|
|
196
|
+
"@tanstack/react-query": "^5.0.0",
|
|
197
|
+
},
|
|
198
|
+
onboarding: {
|
|
199
|
+
"@venlyfinance/react": "^0.4.0",
|
|
200
|
+
"@venlyfinance/sdk": "^0.5.0",
|
|
201
|
+
"@tanstack/react-query": "^5.0.0",
|
|
202
|
+
},
|
|
203
|
+
receive: {
|
|
204
|
+
"@venlyfinance/react": "^0.4.0",
|
|
205
|
+
"@venlyfinance/sdk": "^0.5.0",
|
|
206
|
+
"@tanstack/react-query": "^5.0.0",
|
|
207
|
+
},
|
|
208
|
+
reconciliation: {
|
|
209
|
+
"@venlyfinance/react": "^0.4.0",
|
|
210
|
+
"@venlyfinance/sdk": "^0.5.0",
|
|
211
|
+
"@tanstack/react-query": "^5.0.0",
|
|
212
|
+
},
|
|
213
|
+
send: {
|
|
214
|
+
"@venlyfinance/react": "^0.4.0",
|
|
215
|
+
"@venlyfinance/sdk": "^0.5.0",
|
|
216
|
+
"@tanstack/react-query": "^5.0.0",
|
|
217
|
+
},
|
|
218
|
+
team: { "@radix-ui/react-dialog": "^1.1.23" },
|
|
219
|
+
withdraw: {
|
|
220
|
+
"@venlyfinance/react": "^0.4.0",
|
|
221
|
+
"@venlyfinance/sdk": "^0.5.0",
|
|
222
|
+
"@tanstack/react-query": "^5.0.0",
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
/**
|
|
226
|
+
* The package set any hook-using screen needs, for journeys the registry has no
|
|
227
|
+
* composite block for yet - the console screens are built from primitives
|
|
228
|
+
* (data-table, side-panel, timeline …), and a primitive registry item declares
|
|
229
|
+
* no npm dependencies, so deriving `requiredPackages` from blocks alone would
|
|
230
|
+
* tell an agent that a surface living entirely on hooks needs no packages.
|
|
231
|
+
*
|
|
232
|
+
* The sdk range is the one the console screens themselves need: they render the
|
|
233
|
+
* mock's channel state and balances that move on a transfer, and both arrived in
|
|
234
|
+
* 0.6.0. Composite block registry items stamp their own range from
|
|
235
|
+
* ui/package.json, which is older; a console screen built against that range
|
|
236
|
+
* would describe states it cannot reach.
|
|
237
|
+
*/
|
|
238
|
+
const DATA_PLANE_PACKAGES = {
|
|
239
|
+
"@venlyfinance/react": "^0.4.0",
|
|
240
|
+
"@venlyfinance/sdk": "^0.6.0",
|
|
241
|
+
"@tanstack/react-query": "^5.0.0",
|
|
242
|
+
};
|
|
243
|
+
const JOURNEY_RUNTIME = {
|
|
244
|
+
auth: {
|
|
245
|
+
blocks: ["auth"],
|
|
246
|
+
hooks: [],
|
|
247
|
+
demoBindings: [{ import: "createMockAuthAdapter", from: "registry:block/auth" }],
|
|
248
|
+
},
|
|
249
|
+
team: {
|
|
250
|
+
blocks: ["team"],
|
|
251
|
+
hooks: [],
|
|
252
|
+
demoBindings: [{ import: "createMockTeamAdapter", from: "registry:block/team" }],
|
|
253
|
+
},
|
|
254
|
+
"home-balances": { blocks: ["balances"], hooks: ["useAccounts", "useWallets"] },
|
|
255
|
+
receive: { blocks: ["receive"], hooks: ["useVirtualBankAccounts"] },
|
|
256
|
+
send: { blocks: ["send"], hooks: ["useStagedTransfer", "useFeeQuote"] },
|
|
257
|
+
activity: { blocks: ["activity"], hooks: ["useTransfers", "useRampRequests"] },
|
|
258
|
+
"onboarding-status": {
|
|
259
|
+
blocks: ["onboarding"],
|
|
260
|
+
hooks: ["useCreateParty", "useCreateAccount", "useParty", "useAccount"],
|
|
261
|
+
},
|
|
262
|
+
"withdraw-bank-accounts": {
|
|
263
|
+
blocks: ["bank-accounts", "withdraw"],
|
|
264
|
+
hooks: [
|
|
265
|
+
"useCompanyBankAccounts",
|
|
266
|
+
"useBankAccountConfig",
|
|
267
|
+
"useCreateCompanyBankAccount",
|
|
268
|
+
"useRampRequests",
|
|
269
|
+
"useRampRequest",
|
|
270
|
+
"useCreateRampRequest",
|
|
271
|
+
"useFeeQuote",
|
|
272
|
+
"useRampPairs",
|
|
273
|
+
"useReferenceData",
|
|
274
|
+
"useFourEyesApproval",
|
|
275
|
+
"useInitiateRamp",
|
|
276
|
+
"describeRampStatus",
|
|
277
|
+
],
|
|
278
|
+
},
|
|
279
|
+
reconciliation: {
|
|
280
|
+
blocks: ["reconciliation"],
|
|
281
|
+
hooks: ["useVirtualBankAccounts", "useTransfers"],
|
|
282
|
+
},
|
|
283
|
+
"proof-of-segregation": { blocks: ["balances"], hooks: ["useWallets", "useAccount"] },
|
|
284
|
+
approvals: {
|
|
285
|
+
blocks: ["withdraw"],
|
|
286
|
+
hooks: ["useRampRequests", "useFourEyesApproval", "useRampLifecycle"],
|
|
287
|
+
},
|
|
288
|
+
"console-review-queue": {
|
|
289
|
+
blocks: [],
|
|
290
|
+
registryItems: ["venly-tokens", "data-table", "status-pill", "money", "list-error"],
|
|
291
|
+
dataPlane: true,
|
|
292
|
+
hooks: ["useAccounts", "useParties"],
|
|
293
|
+
extraForbidden: [
|
|
294
|
+
"a whose-move or needs-attention value computed from anything other than enum values on the row",
|
|
295
|
+
"a target time, breach threshold or overdue state (the API publishes no targets)",
|
|
296
|
+
"labelling a created-at delta \"time in state\" rather than \"Age\" (only an API-computed duration may use that phrase)",
|
|
297
|
+
],
|
|
298
|
+
},
|
|
299
|
+
"console-decision-detail": {
|
|
300
|
+
blocks: [],
|
|
301
|
+
registryItems: [
|
|
302
|
+
"venly-tokens",
|
|
303
|
+
"side-panel",
|
|
304
|
+
"timeline",
|
|
305
|
+
"field-list",
|
|
306
|
+
"status-pill",
|
|
307
|
+
"money",
|
|
308
|
+
"data-table",
|
|
309
|
+
],
|
|
310
|
+
dataPlane: true,
|
|
311
|
+
hooks: [
|
|
312
|
+
"useAccount",
|
|
313
|
+
"useParty",
|
|
314
|
+
"useWallets",
|
|
315
|
+
"useTransfers",
|
|
316
|
+
"useVirtualBankAccounts",
|
|
317
|
+
"useVenlyMock",
|
|
318
|
+
],
|
|
319
|
+
extraForbidden: [
|
|
320
|
+
"a rendered placeholder where an unavailable field belongs (omission is a prop type, not a string)",
|
|
321
|
+
"a captured field the API cannot carry, rendered without the console-note badge",
|
|
322
|
+
"a status change that leaves no timeline node with actor, role and timezone-qualified stamp",
|
|
323
|
+
"auto-retry on a version conflict (refetch and let the operator re-decide)",
|
|
324
|
+
],
|
|
325
|
+
},
|
|
326
|
+
"console-pricing-config": {
|
|
327
|
+
blocks: [],
|
|
328
|
+
registryItems: ["venly-tokens", "data-table", "arithmetic-ladder", "field-list"],
|
|
329
|
+
dataPlane: true,
|
|
330
|
+
hooks: ["useCompanyFees"],
|
|
331
|
+
extraForbidden: [
|
|
332
|
+
"an arithmetic ladder over figures the API does not serve",
|
|
333
|
+
"a single-member enum rendered as a select",
|
|
334
|
+
],
|
|
335
|
+
},
|
|
336
|
+
"console-simulator": {
|
|
337
|
+
blocks: [],
|
|
338
|
+
registryItems: ["venly-tokens", "field-list", "status-pill", "money"],
|
|
339
|
+
dataPlane: true,
|
|
340
|
+
hooks: ["useVenlyMock"],
|
|
341
|
+
extraForbidden: [
|
|
342
|
+
"counterparty or provider simulation rendered inside operator chrome",
|
|
343
|
+
"a simulator control reachable from a queue row or a decision panel",
|
|
344
|
+
"a simulator control phrased as an imperative operator decision",
|
|
345
|
+
"a cross-context demo that does not state its channel adapter and peer count",
|
|
346
|
+
],
|
|
347
|
+
},
|
|
348
|
+
};
|
|
349
|
+
const RUNTIME_CONTRACT_SCHEMA = z.object({
|
|
350
|
+
runtimeMode: z.enum(["mock", "staging", "production"]),
|
|
351
|
+
requiredPackages: z.record(z.string()),
|
|
352
|
+
requiredHooks: z.array(z.object({ import: z.string(), from: z.string() })),
|
|
353
|
+
demoBindings: z.array(z.object({ import: z.string(), from: z.string() })).optional(),
|
|
354
|
+
provider: z.object({
|
|
355
|
+
import: z.string(),
|
|
356
|
+
from: z.string(),
|
|
357
|
+
props: z.object({ environment: z.literal("mock") }),
|
|
358
|
+
}),
|
|
359
|
+
forbiddenPatterns: z.array(z.string()),
|
|
360
|
+
install: z.array(z.string()),
|
|
361
|
+
completionChecks: z.array(z.string()),
|
|
362
|
+
});
|
|
363
|
+
function runtimeContractForJourney(journey) {
|
|
364
|
+
const definition = JOURNEY_RUNTIME[journey];
|
|
365
|
+
// Base first, blocks last: a composite block's own stamped dependencies are
|
|
366
|
+
// what the registry will actually install, so they win where the two differ.
|
|
367
|
+
const requiredPackages = definition.dataPlane
|
|
368
|
+
? { ...DATA_PLANE_PACKAGES }
|
|
369
|
+
: {};
|
|
370
|
+
for (const block of definition.blocks) {
|
|
371
|
+
Object.assign(requiredPackages, RUNTIME_PACKAGES_BY_BLOCK[block]);
|
|
372
|
+
}
|
|
373
|
+
const installItems = definition.blocks.length
|
|
374
|
+
? definition.blocks.map((block) => `@venlyfinance/${block}`)
|
|
375
|
+
: (definition.registryItems ?? []).map((item) => `@venlyfinance/${item}`);
|
|
376
|
+
return {
|
|
377
|
+
runtimeMode: "mock",
|
|
378
|
+
requiredPackages,
|
|
379
|
+
requiredHooks: definition.hooks.map((name) => ({
|
|
380
|
+
import: name,
|
|
381
|
+
from: "@venlyfinance/react",
|
|
382
|
+
})),
|
|
383
|
+
...(definition.demoBindings ? { demoBindings: definition.demoBindings } : {}),
|
|
384
|
+
provider: {
|
|
385
|
+
import: "VenlyProvider",
|
|
386
|
+
from: "@venlyfinance/react",
|
|
387
|
+
props: { environment: "mock" },
|
|
388
|
+
},
|
|
389
|
+
forbiddenPatterns: [
|
|
390
|
+
"in-memory store of transfer/balance/approval state",
|
|
391
|
+
"fetch()/axios to self-owned money routes that do not wrap @venlyfinance/sdk",
|
|
392
|
+
"useEffect polling loops for transfer status (useStagedTransfer/useRampLifecycle exist)",
|
|
393
|
+
"clientSecret in browser code (provider throws; use proxyClientOptions())",
|
|
394
|
+
...(definition.extraForbidden ?? []),
|
|
395
|
+
],
|
|
396
|
+
install: [
|
|
397
|
+
"npx shadcn@latest init -y -b radix -p nova",
|
|
398
|
+
'add { "registries": { "@venlyfinance": "https://raw.githubusercontent.com/Venly/venly-settlement-sdk/main/ui/r/{name}.json" } } to components.json',
|
|
399
|
+
`npx shadcn@latest add ${installItems.join(" ")} -y -o`,
|
|
400
|
+
],
|
|
401
|
+
completionChecks: [
|
|
402
|
+
'npx @venlyfinance/settlement-mcp review "src/**/*.tsx" exits 0',
|
|
403
|
+
'npx @venlyfinance/settlement-mcp verify "src/**/*.{ts,tsx}" exits 0',
|
|
404
|
+
],
|
|
405
|
+
};
|
|
406
|
+
}
|
|
83
407
|
// ---------------------------------------------------------------------------
|
|
84
408
|
// Shared mechanics. Three cross-cutting behaviours every rule participates in:
|
|
85
409
|
//
|
|
@@ -442,9 +766,58 @@ export function registerFrontendTools(server) {
|
|
|
442
766
|
inputSchema: {
|
|
443
767
|
journey: z.enum(JOURNEY_KEYS).describe("Which journey to blueprint"),
|
|
444
768
|
},
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
769
|
+
outputSchema: {
|
|
770
|
+
runtime_contract: RUNTIME_CONTRACT_SCHEMA,
|
|
771
|
+
},
|
|
772
|
+
}, async ({ journey }) => {
|
|
773
|
+
const structuredContent = {
|
|
774
|
+
runtime_contract: runtimeContractForJourney(journey),
|
|
775
|
+
};
|
|
776
|
+
return {
|
|
777
|
+
content: [
|
|
778
|
+
{ type: "text", text: JOURNEYS[journey] },
|
|
779
|
+
{
|
|
780
|
+
type: "text",
|
|
781
|
+
text: `\`\`\`json\n${JSON.stringify(structuredContent, null, 2)}\n\`\`\``,
|
|
782
|
+
},
|
|
783
|
+
],
|
|
784
|
+
structuredContent,
|
|
785
|
+
};
|
|
786
|
+
});
|
|
787
|
+
server.registerTool("verify_runtime_contract", {
|
|
788
|
+
title: "Verify an app's Venly runtime contract",
|
|
789
|
+
description: "Deterministically checks supplied app source and package.json against the direct-sdk or backend-proxy runtime contract. The same rules power the verify CLI.",
|
|
790
|
+
inputSchema: {
|
|
791
|
+
files: z
|
|
792
|
+
.array(z.object({ path: z.string().min(1), source: z.string() }))
|
|
793
|
+
.min(1),
|
|
794
|
+
packageJson: z.string().describe("The app's package.json contents"),
|
|
795
|
+
profile: z.enum(["direct-sdk", "backend-proxy"]).optional(),
|
|
796
|
+
},
|
|
797
|
+
}, async ({ files, packageJson, profile }) => {
|
|
798
|
+
let parsedPackageJson;
|
|
799
|
+
try {
|
|
800
|
+
parsedPackageJson = JSON.parse(packageJson);
|
|
801
|
+
}
|
|
802
|
+
catch (error) {
|
|
803
|
+
const message = `Invalid packageJson: ${error.message}`;
|
|
804
|
+
return {
|
|
805
|
+
content: [{ type: "text", text: message }],
|
|
806
|
+
structuredContent: { error: message },
|
|
807
|
+
isError: true,
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
const result = verifyRuntimeContract({ files, packageJson: parsedPackageJson, profile });
|
|
811
|
+
const structuredContent = {
|
|
812
|
+
profile: result.profile,
|
|
813
|
+
findings: result.findings,
|
|
814
|
+
summary: result.summary,
|
|
815
|
+
};
|
|
816
|
+
return {
|
|
817
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
818
|
+
structuredContent,
|
|
819
|
+
};
|
|
820
|
+
});
|
|
448
821
|
server.registerTool("review_screen", {
|
|
449
822
|
title: "Design-audit a screen",
|
|
450
823
|
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, invented timing/custody copy, crypto codes inside Intl currency formatting, required fields rendered optional, parity and round-number fixtures, zebra striping, off-token shadows, gradients, colour-only state. Pass the journey key to also check the source against that journey's required blueprint states. Suppress a deliberate exception with venly-allow:<rule-id> on the offending line or the line above. Returns findings, not a score.",
|
package/dist/index.js
CHANGED
|
@@ -42,6 +42,17 @@ if (argv[0] === "review") {
|
|
|
42
42
|
process.exit(2);
|
|
43
43
|
});
|
|
44
44
|
}
|
|
45
|
+
else if (argv[0] === "verify") {
|
|
46
|
+
import("./verify-cli.js")
|
|
47
|
+
.then(({ runVerifyCli }) => runVerifyCli(argv.slice(1)))
|
|
48
|
+
.then((code) => {
|
|
49
|
+
process.exitCode = code;
|
|
50
|
+
})
|
|
51
|
+
.catch((err) => {
|
|
52
|
+
process.stderr.write(`Fatal: ${err.message}\n`);
|
|
53
|
+
process.exit(2);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
45
56
|
else {
|
|
46
57
|
main().catch((err) => {
|
|
47
58
|
process.stderr.write(`Fatal: ${err.message}\n`);
|
package/dist/server.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* mock client and no network.
|
|
5
5
|
*/
|
|
6
6
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
|
-
import { ENVIRONMENT_FLAG, SERVER_NAME, SERVER_VERSION, resolveVenlyEnvironment, } from "./constants.js";
|
|
7
|
+
import { ENVIRONMENT_FLAG, INSTRUCTIONS, SERVER_NAME, SERVER_VERSION, resolveVenlyEnvironment, } from "./constants.js";
|
|
8
8
|
import { registerReadTools } from "./tools/read-tools.js";
|
|
9
9
|
import { registerWriteTools } from "./tools/write-tools.js";
|
|
10
10
|
import { registerX402Tools } from "./tools/x402-tools.js";
|
|
@@ -26,6 +26,8 @@ export function createServer(options) {
|
|
|
26
26
|
const server = new McpServer({
|
|
27
27
|
name: SERVER_NAME,
|
|
28
28
|
version: SERVER_VERSION,
|
|
29
|
+
}, {
|
|
30
|
+
instructions: INSTRUCTIONS,
|
|
29
31
|
});
|
|
30
32
|
registerReadTools(server, options.client);
|
|
31
33
|
registerWriteTools(server, options.client, env);
|
package/dist/staging-smoke.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const EXPECTED_TOOLS: readonly ["list_ramp_requests", "get_ramp_request", "list_accounts", "get_account", "list_wallets", "list_virtual_bank_accounts", "get_virtual_bank_account", "reconcile_by_reference_code", "list_transfers", "get_transfer", "list_parties", "get_party", "get_reference_data", "create_party", "create_account", "create_virtual_bank_account", "create_fiat_transfer", "create_crypto_transfer", "approve_ramp_request", "reject_ramp_request", "create_payment_session", "list_payouts", "get_payout", "list_payout_routes", "list_payout_bank_accounts", "register_payout_bank_account", "create_payout_route", "prepare_payout_ownership_proof", "complete_payout_ownership_proof", "request_payout", "quote_x402_payment", "get_journey_blueprint", "review_screen"];
|
|
1
|
+
export declare const EXPECTED_TOOLS: readonly ["list_ramp_requests", "get_ramp_request", "list_accounts", "get_account", "list_wallets", "list_virtual_bank_accounts", "get_virtual_bank_account", "reconcile_by_reference_code", "list_transfers", "get_transfer", "list_parties", "get_party", "get_reference_data", "create_party", "create_account", "create_virtual_bank_account", "create_fiat_transfer", "create_crypto_transfer", "approve_ramp_request", "reject_ramp_request", "create_payment_session", "list_payouts", "get_payout", "list_payout_routes", "list_payout_bank_accounts", "register_payout_bank_account", "create_payout_route", "prepare_payout_ownership_proof", "complete_payout_ownership_proof", "request_payout", "quote_x402_payment", "get_journey_blueprint", "verify_runtime_contract", "review_screen"];
|
|
2
2
|
export declare const EXPECTED_RESOURCE_URIS: readonly ["venly://capabilities", "venly://safety", "venly://workflows/international-account", "venly://workflows/mock-to-staging", "venly://frontend/agents"];
|
|
3
3
|
export declare const EXPECTED_PROMPTS: readonly ["build_international_account"];
|
|
4
4
|
export interface DiscoveryNames {
|
package/dist/staging-smoke.js
CHANGED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export type VerifyProfile = "direct-sdk" | "backend-proxy";
|
|
2
|
+
export interface VerifySourceFile {
|
|
3
|
+
path: string;
|
|
4
|
+
source: string;
|
|
5
|
+
}
|
|
6
|
+
export interface VerifyFinding {
|
|
7
|
+
rule: string;
|
|
8
|
+
severity: "error" | "warn";
|
|
9
|
+
path: string;
|
|
10
|
+
evidence: string;
|
|
11
|
+
fix: string;
|
|
12
|
+
line?: number;
|
|
13
|
+
}
|
|
14
|
+
export interface VerifyResult {
|
|
15
|
+
profile: VerifyProfile;
|
|
16
|
+
findings: VerifyFinding[];
|
|
17
|
+
summary: string;
|
|
18
|
+
}
|
|
19
|
+
export declare function verifyRuntimeContract(options: {
|
|
20
|
+
files: VerifySourceFile[];
|
|
21
|
+
packageJson: Record<string, unknown>;
|
|
22
|
+
profile?: VerifyProfile;
|
|
23
|
+
}): VerifyResult;
|
|
24
|
+
export declare function runVerifyCli(args: string[], out?: NodeJS.WritableStream, err?: NodeJS.WritableStream): Promise<0 | 1 | 2>;
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
// `verify` subcommand: deterministic runtime-contract checks for generated apps.
|
|
2
|
+
//
|
|
3
|
+
// Exit codes: 0 clean (warnings allowed) · 1 at least one error · 2 usage or
|
|
4
|
+
// no-match. The three unresolved false-positive boundaries intentionally warn:
|
|
5
|
+
// missing React in direct-sdk, app-owned money routes, and in-memory stores.
|
|
6
|
+
import { existsSync, readFileSync, readdirSync, statSync, } from "node:fs";
|
|
7
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
8
|
+
const BLUEPRINT_HOOKS = new Set([
|
|
9
|
+
"useAccount",
|
|
10
|
+
"useAccounts",
|
|
11
|
+
"useBankAccountConfig",
|
|
12
|
+
"useCompanyBankAccounts",
|
|
13
|
+
"useCreateAccount",
|
|
14
|
+
"useCreateCompanyBankAccount",
|
|
15
|
+
"useCreateParty",
|
|
16
|
+
"useCreateRampRequest",
|
|
17
|
+
"useFeeQuote",
|
|
18
|
+
"useFourEyesApproval",
|
|
19
|
+
"useInitiateRamp",
|
|
20
|
+
"useParty",
|
|
21
|
+
"useRampLifecycle",
|
|
22
|
+
"useRampPairs",
|
|
23
|
+
"useRampRequest",
|
|
24
|
+
"useRampRequests",
|
|
25
|
+
"useReferenceData",
|
|
26
|
+
"useStagedTransfer",
|
|
27
|
+
"useTransfers",
|
|
28
|
+
"useVirtualBankAccounts",
|
|
29
|
+
"useWallets",
|
|
30
|
+
]);
|
|
31
|
+
function lineFor(source, index) {
|
|
32
|
+
return source.slice(0, index).split("\n").length;
|
|
33
|
+
}
|
|
34
|
+
function suppressed(source, rule, line) {
|
|
35
|
+
const token = `venly-allow:${rule}`;
|
|
36
|
+
if (line === undefined)
|
|
37
|
+
return source.includes(token);
|
|
38
|
+
const lines = source.split("\n");
|
|
39
|
+
return Boolean(lines[line - 1]?.includes(token) || lines[line - 2]?.includes(token));
|
|
40
|
+
}
|
|
41
|
+
function dependencies(packageJson) {
|
|
42
|
+
return Object.assign({}, packageJson.dependencies ?? {}, packageJson.devDependencies ?? {}, packageJson.peerDependencies ?? {});
|
|
43
|
+
}
|
|
44
|
+
function importsFrom(source, packageName) {
|
|
45
|
+
const names = [];
|
|
46
|
+
const expression = new RegExp(`import\\s*\\{([^}]*)\\}\\s*from\\s*["']${packageName.replaceAll("/", "\\/")}["']`, "g");
|
|
47
|
+
for (const match of source.matchAll(expression)) {
|
|
48
|
+
for (const item of (match[1] ?? "").split(",")) {
|
|
49
|
+
const name = item.trim().split(/\s+as\s+/)[0]?.trim();
|
|
50
|
+
if (name)
|
|
51
|
+
names.push(name);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return names;
|
|
55
|
+
}
|
|
56
|
+
function isServerFile(file) {
|
|
57
|
+
return (/(?:^|\/)(?:server|backend|api)(?:\/|\.)/i.test(file.path) ||
|
|
58
|
+
/(?:^|\/)route\.[cm]?[jt]sx?$/i.test(file.path) ||
|
|
59
|
+
/[.]server\.[cm]?[jt]sx?$/i.test(file.path) ||
|
|
60
|
+
/^\s*["']use server["'];?/m.test(file.source));
|
|
61
|
+
}
|
|
62
|
+
function isMoneyRoute(file) {
|
|
63
|
+
const routeHandler = /(?:^|\/)route\.[cm]?[jt]sx?$/i.test(file.path) ||
|
|
64
|
+
/export\s+(?:async\s+)?function\s+(?:GET|POST|PUT|PATCH|DELETE)\b/.test(file.source);
|
|
65
|
+
const moneySignal = /(?:transfers?|payouts?|balances?|ramps?|rampRequests|virtual[-_/ ]bank)/i.test(`${file.path}\n${file.source}`);
|
|
66
|
+
return routeHandler && moneySignal;
|
|
67
|
+
}
|
|
68
|
+
function autoDetectProfile(files) {
|
|
69
|
+
return files.some((file) => file.source.includes("proxyClientOptions") || isMoneyRoute(file))
|
|
70
|
+
? "backend-proxy"
|
|
71
|
+
: "direct-sdk";
|
|
72
|
+
}
|
|
73
|
+
export function verifyRuntimeContract(options) {
|
|
74
|
+
const { files, packageJson } = options;
|
|
75
|
+
const profile = options.profile ?? autoDetectProfile(files);
|
|
76
|
+
const deps = dependencies(packageJson);
|
|
77
|
+
const findings = [];
|
|
78
|
+
function addProjectFinding(finding) {
|
|
79
|
+
const source = files.map((file) => file.source).join("\n");
|
|
80
|
+
if (!suppressed(source, finding.rule)) {
|
|
81
|
+
findings.push({ ...finding, path: "package.json" });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function addSourceFinding(file, finding) {
|
|
85
|
+
if (!suppressed(file.source, finding.rule, finding.line)) {
|
|
86
|
+
findings.push({ ...finding, path: file.path });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (!("@venlyfinance/react" in deps) && !("@venlyfinance/sdk" in deps)) {
|
|
90
|
+
addProjectFinding({
|
|
91
|
+
rule: "venly-package-missing",
|
|
92
|
+
severity: "error",
|
|
93
|
+
evidence: "package.json declares zero @venlyfinance runtime packages",
|
|
94
|
+
fix: "Install the registry block for the journey or add the required @venlyfinance package.",
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
if (profile === "direct-sdk") {
|
|
98
|
+
if (!("@venlyfinance/react" in deps)) {
|
|
99
|
+
addProjectFinding({
|
|
100
|
+
rule: "react-package-missing",
|
|
101
|
+
severity: "warn",
|
|
102
|
+
evidence: "direct-sdk profile has no @venlyfinance/react dependency",
|
|
103
|
+
fix: "Install the journey's registry block, or suppress this warning for an intentionally headless integration.",
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
const providerImported = files.some((file) => importsFrom(file.source, "@venlyfinance/react").includes("VenlyProvider"));
|
|
107
|
+
const mockProviderRendered = files.some((file) => /<VenlyProvider\b[^>]*\benvironment\s*=\s*(?:["']mock["']|\{[^}]+\})/s.test(file.source));
|
|
108
|
+
if (!providerImported || !mockProviderRendered) {
|
|
109
|
+
addProjectFinding({
|
|
110
|
+
rule: "provider-missing",
|
|
111
|
+
severity: "error",
|
|
112
|
+
evidence: "no VenlyProvider import and mock/environment expression wrapper were found",
|
|
113
|
+
fix: 'Import VenlyProvider from @venlyfinance/react and wrap the tree with environment="mock".',
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
const importedHooks = files.flatMap((file) => importsFrom(file.source, "@venlyfinance/react").filter((name) => BLUEPRINT_HOOKS.has(name)));
|
|
117
|
+
if (importedHooks.length === 0) {
|
|
118
|
+
addProjectFinding({
|
|
119
|
+
rule: "blueprint-hook-missing",
|
|
120
|
+
severity: "error",
|
|
121
|
+
evidence: "no journey-blueprint hook is imported from @venlyfinance/react",
|
|
122
|
+
fix: "Use the qualified hooks named by get_journey_blueprint instead of rebuilding the data layer.",
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
for (const file of files) {
|
|
126
|
+
if (!/(?:@venlyfinance\/react|["']react["']|["']use client["'])/.test(file.source))
|
|
127
|
+
continue;
|
|
128
|
+
for (const match of file.source.matchAll(/\bclientSecret\b/g)) {
|
|
129
|
+
addSourceFinding(file, {
|
|
130
|
+
rule: "browser-client-secret",
|
|
131
|
+
severity: "error",
|
|
132
|
+
line: lineFor(file.source, match.index ?? 0),
|
|
133
|
+
evidence: "clientSecret appears in browser/React source",
|
|
134
|
+
fix: "Keep secrets server-side; use proxyClientOptions() for browser traffic.",
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
if (!("@venlyfinance/sdk" in deps)) {
|
|
141
|
+
addProjectFinding({
|
|
142
|
+
rule: "sdk-package-missing",
|
|
143
|
+
severity: "error",
|
|
144
|
+
evidence: "backend-proxy profile has no @venlyfinance/sdk dependency",
|
|
145
|
+
fix: "Add @venlyfinance/sdk and make money routes wrap the official client.",
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
for (const file of files) {
|
|
149
|
+
if (isMoneyRoute(file) && !/from\s*["']@venlyfinance\/sdk["']/.test(file.source)) {
|
|
150
|
+
addSourceFinding(file, {
|
|
151
|
+
rule: "money-route-without-sdk",
|
|
152
|
+
severity: "warn",
|
|
153
|
+
line: 1,
|
|
154
|
+
evidence: "money-route heuristic matched but no @venlyfinance/sdk import was found",
|
|
155
|
+
fix: "Wrap this route with @venlyfinance/sdk, or suppress if it is an unrelated consumer-owned ledger.",
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
if (!isServerFile(file)) {
|
|
159
|
+
for (const match of file.source.matchAll(/\bclientSecret\b/g)) {
|
|
160
|
+
addSourceFinding(file, {
|
|
161
|
+
rule: "client-secret-outside-server",
|
|
162
|
+
severity: "error",
|
|
163
|
+
line: lineFor(file.source, match.index ?? 0),
|
|
164
|
+
evidence: "clientSecret appears outside a server-only file",
|
|
165
|
+
fix: "Move the secret into a server route or server-only module.",
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const proxyImported = files.some((file) => importsFrom(file.source, "@venlyfinance/react").includes("proxyClientOptions"));
|
|
171
|
+
if (!proxyImported) {
|
|
172
|
+
addProjectFinding({
|
|
173
|
+
rule: "proxy-client-options-missing",
|
|
174
|
+
severity: "warn",
|
|
175
|
+
evidence: "backend-proxy profile has no browser-side proxyClientOptions import",
|
|
176
|
+
fix: "Use proxyClientOptions() in the browser provider, or suppress for a server-only consumer.",
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
for (const file of files) {
|
|
181
|
+
if (/\buseEffect\b/.test(file.source) &&
|
|
182
|
+
/\bset(?:Interval|Timeout)\b/.test(file.source) &&
|
|
183
|
+
/(?:status|state)/i.test(file.source) &&
|
|
184
|
+
/\b(?:transfer|ramp)/i.test(file.source)) {
|
|
185
|
+
const match = /\buseEffect\b/.exec(file.source);
|
|
186
|
+
addSourceFinding(file, {
|
|
187
|
+
rule: "status-polling",
|
|
188
|
+
severity: "warn",
|
|
189
|
+
line: lineFor(file.source, match?.index ?? 0),
|
|
190
|
+
evidence: "useEffect timer polling appears beside transfer/ramp state",
|
|
191
|
+
fix: "Use useStagedTransfer or useRampLifecycle for lifecycle polling.",
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
const store = /(?:^|\n)\s*export\s+(?:const|let)\s+(transfers|balances|payouts|rampRequests)\s*=\s*(?:\[|new\s+(?:Map|Set)\b)/g;
|
|
195
|
+
for (const match of file.source.matchAll(store)) {
|
|
196
|
+
addSourceFinding(file, {
|
|
197
|
+
rule: "in-memory-money-store",
|
|
198
|
+
severity: "warn",
|
|
199
|
+
line: lineFor(file.source, match.index ?? 0),
|
|
200
|
+
evidence: `module exports mutable in-memory money state named ${match[1]}`,
|
|
201
|
+
fix: "Use @venlyfinance/react hooks/flows or the SDK; suppress only for a deliberate fixture.",
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
findings.sort((a, b) => a.path.localeCompare(b.path) ||
|
|
206
|
+
(a.line ?? 0) - (b.line ?? 0) ||
|
|
207
|
+
a.rule.localeCompare(b.rule));
|
|
208
|
+
const errors = findings.filter((finding) => finding.severity === "error").length;
|
|
209
|
+
const warnings = findings.length - errors;
|
|
210
|
+
return {
|
|
211
|
+
profile,
|
|
212
|
+
findings,
|
|
213
|
+
summary: `${errors} error(s), ${warnings} warning(s) across ${files.length} file(s)`,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
const SKIP_DIRS = new Set(["node_modules", ".git", "dist"]);
|
|
217
|
+
function braceExpand(pattern) {
|
|
218
|
+
const match = /\{([^{}]*)\}/.exec(pattern);
|
|
219
|
+
if (!match)
|
|
220
|
+
return [pattern];
|
|
221
|
+
const before = pattern.slice(0, match.index);
|
|
222
|
+
const after = pattern.slice(match.index + match[0].length);
|
|
223
|
+
return (match[1] ?? "").split(",").flatMap((option) => braceExpand(before + option + after));
|
|
224
|
+
}
|
|
225
|
+
function patternToRegExp(pattern) {
|
|
226
|
+
const escaped = pattern.replace(/[.+^$()|[\]\\?]/g, "\\$&");
|
|
227
|
+
const translated = escaped
|
|
228
|
+
.replace(/\*\*\//g, "\u0000")
|
|
229
|
+
.replace(/\*\*/g, "\u0001")
|
|
230
|
+
.replace(/\*/g, "[^/]*")
|
|
231
|
+
.replace(/\u0000/g, "(?:.*/)?")
|
|
232
|
+
.replace(/\u0001/g, ".*");
|
|
233
|
+
return new RegExp(`^${translated}$`);
|
|
234
|
+
}
|
|
235
|
+
function walk(dir, into) {
|
|
236
|
+
let entries;
|
|
237
|
+
try {
|
|
238
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
for (const entry of entries) {
|
|
244
|
+
if (entry.isDirectory()) {
|
|
245
|
+
if (!SKIP_DIRS.has(entry.name))
|
|
246
|
+
walk(join(dir, entry.name), into);
|
|
247
|
+
}
|
|
248
|
+
else if (entry.isFile()) {
|
|
249
|
+
into.push(join(dir, entry.name));
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
function expandPattern(pattern, cwd) {
|
|
254
|
+
const results = [];
|
|
255
|
+
for (const variant of braceExpand(pattern)) {
|
|
256
|
+
const segments = variant.split("/");
|
|
257
|
+
const firstWild = segments.findIndex((segment) => segment.includes("*"));
|
|
258
|
+
if (firstWild === -1) {
|
|
259
|
+
if (existsSync(join(cwd, variant)) && statSync(join(cwd, variant)).isFile())
|
|
260
|
+
results.push(variant);
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
const staticPrefix = segments.slice(0, firstWild).join("/");
|
|
264
|
+
const root = staticPrefix ? join(cwd, staticPrefix) : cwd;
|
|
265
|
+
const files = [];
|
|
266
|
+
walk(root, files);
|
|
267
|
+
const matcher = patternToRegExp(variant);
|
|
268
|
+
for (const file of files) {
|
|
269
|
+
const rel = relative(cwd, file).split("\\").join("/");
|
|
270
|
+
if (matcher.test(rel))
|
|
271
|
+
results.push(rel);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return [...new Set(results)].sort();
|
|
275
|
+
}
|
|
276
|
+
function expandPatterns(patterns, cwd) {
|
|
277
|
+
return [...new Set(patterns.flatMap((pattern) => expandPattern(pattern, cwd)))];
|
|
278
|
+
}
|
|
279
|
+
function findPackageJson(file, cwd) {
|
|
280
|
+
let dir = dirname(resolve(cwd, file));
|
|
281
|
+
while (true) {
|
|
282
|
+
const candidate = join(dir, "package.json");
|
|
283
|
+
if (existsSync(candidate))
|
|
284
|
+
return candidate;
|
|
285
|
+
const parent = dirname(dir);
|
|
286
|
+
if (parent === dir)
|
|
287
|
+
return undefined;
|
|
288
|
+
dir = parent;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
function parseArgs(args) {
|
|
292
|
+
const patterns = [];
|
|
293
|
+
let profile;
|
|
294
|
+
for (let index = 0; index < args.length; index++) {
|
|
295
|
+
const arg = args[index] ?? "";
|
|
296
|
+
if (arg === "--profile") {
|
|
297
|
+
const value = args[++index];
|
|
298
|
+
if (value !== "direct-sdk" && value !== "backend-proxy") {
|
|
299
|
+
return { patterns, error: "--profile must be direct-sdk or backend-proxy" };
|
|
300
|
+
}
|
|
301
|
+
profile = value;
|
|
302
|
+
}
|
|
303
|
+
else if (arg.startsWith("--profile=")) {
|
|
304
|
+
const value = arg.slice("--profile=".length);
|
|
305
|
+
if (value !== "direct-sdk" && value !== "backend-proxy") {
|
|
306
|
+
return { patterns, error: "--profile must be direct-sdk or backend-proxy" };
|
|
307
|
+
}
|
|
308
|
+
profile = value;
|
|
309
|
+
}
|
|
310
|
+
else if (arg.startsWith("-")) {
|
|
311
|
+
return { patterns, error: `Unknown option: ${arg}` };
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
patterns.push(arg);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return { patterns, profile };
|
|
318
|
+
}
|
|
319
|
+
export async function runVerifyCli(args, out = process.stdout, err = process.stderr) {
|
|
320
|
+
const parsed = parseArgs(args);
|
|
321
|
+
if (parsed.error || parsed.patterns.length === 0) {
|
|
322
|
+
if (parsed.error)
|
|
323
|
+
err.write(`${parsed.error}\n`);
|
|
324
|
+
err.write('Usage: verify [--profile direct-sdk|backend-proxy] "<glob>" [more globs or files]\n' +
|
|
325
|
+
' e.g. verify "src/**/*.{ts,tsx}"\n' +
|
|
326
|
+
"Exits 1 on any error-severity finding, 2 when nothing matched.\n");
|
|
327
|
+
return 2;
|
|
328
|
+
}
|
|
329
|
+
const cwd = process.cwd();
|
|
330
|
+
const files = expandPatterns(parsed.patterns, cwd);
|
|
331
|
+
if (files.length === 0) {
|
|
332
|
+
err.write(`Nothing matched: ${parsed.patterns.join(" ")}\n`);
|
|
333
|
+
return 2;
|
|
334
|
+
}
|
|
335
|
+
const groups = new Map();
|
|
336
|
+
for (const file of files) {
|
|
337
|
+
const packagePath = findPackageJson(file, cwd) ?? join(cwd, "package.json");
|
|
338
|
+
groups.set(packagePath, [...(groups.get(packagePath) ?? []), file]);
|
|
339
|
+
}
|
|
340
|
+
let errors = 0;
|
|
341
|
+
let warnings = 0;
|
|
342
|
+
for (const [packagePath, groupFiles] of [...groups.entries()].sort()) {
|
|
343
|
+
let packageJson = {};
|
|
344
|
+
if (existsSync(packagePath)) {
|
|
345
|
+
try {
|
|
346
|
+
packageJson = JSON.parse(readFileSync(packagePath, "utf8"));
|
|
347
|
+
}
|
|
348
|
+
catch (error) {
|
|
349
|
+
err.write(`Invalid package.json at ${relative(cwd, packagePath)}: ${error.message}\n`);
|
|
350
|
+
return 2;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
const result = verifyRuntimeContract({
|
|
354
|
+
files: groupFiles.map((path) => ({ path, source: readFileSync(path, "utf8") })),
|
|
355
|
+
packageJson,
|
|
356
|
+
profile: parsed.profile,
|
|
357
|
+
});
|
|
358
|
+
out.write(`profile: ${result.profile}\n`);
|
|
359
|
+
for (const finding of result.findings) {
|
|
360
|
+
if (finding.severity === "error")
|
|
361
|
+
errors++;
|
|
362
|
+
else
|
|
363
|
+
warnings++;
|
|
364
|
+
const line = finding.line === undefined ? "" : `:${finding.line}`;
|
|
365
|
+
out.write(`${finding.path}${line} ${finding.severity} ${finding.rule} ${finding.evidence}\n`);
|
|
366
|
+
out.write(` fix: ${finding.fix}\n`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
out.write(`${errors} error(s), ${warnings} warning(s) across ${files.length} file(s)\n`);
|
|
370
|
+
return errors > 0 ? 1 : 0;
|
|
371
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@venlyfinance/settlement-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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": {
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"node": ">=20"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@venlyfinance/sdk": "^0.
|
|
32
|
+
"@venlyfinance/sdk": "^0.5.0",
|
|
33
33
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
34
34
|
"zod": "^3.23.8"
|
|
35
35
|
},
|