@finrock/mcp 0.0.0-stage → 0.1.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.
Files changed (51) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +214 -2
  3. package/dist/api/client.d.ts +50 -0
  4. package/dist/api/client.js +172 -0
  5. package/dist/api/tools.d.ts +17 -0
  6. package/dist/api/tools.js +300 -0
  7. package/dist/docs.d.ts +61 -0
  8. package/dist/docs.js +190 -0
  9. package/dist/guides.d.ts +19 -0
  10. package/dist/guides.js +66 -0
  11. package/dist/http.d.ts +13 -0
  12. package/dist/http.js +94 -0
  13. package/dist/index.d.ts +7 -0
  14. package/dist/index.js +7 -0
  15. package/dist/server.d.ts +17 -0
  16. package/dist/server.js +229 -0
  17. package/dist/setup.d.ts +10 -0
  18. package/dist/setup.js +123 -0
  19. package/dist/stdio.d.ts +2 -0
  20. package/dist/stdio.js +28 -0
  21. package/dist/webhook.d.ts +11 -0
  22. package/dist/webhook.js +39 -0
  23. package/guides/_client/nextjs.md +57 -0
  24. package/guides/_client/node.md +55 -0
  25. package/guides/_client/php.md +67 -0
  26. package/guides/_client/python.md +55 -0
  27. package/guides/accept-payments/nextjs.md +75 -0
  28. package/guides/accept-payments/node.md +49 -0
  29. package/guides/accept-payments/overview.md +30 -0
  30. package/guides/accept-payments/php.md +61 -0
  31. package/guides/accept-payments/python.md +59 -0
  32. package/guides/authentication/overview.md +60 -0
  33. package/guides/deposit-addresses/nextjs.md +50 -0
  34. package/guides/deposit-addresses/node.md +30 -0
  35. package/guides/deposit-addresses/overview.md +36 -0
  36. package/guides/deposit-addresses/php.md +35 -0
  37. package/guides/deposit-addresses/python.md +31 -0
  38. package/guides/payment-widget/html.md +26 -0
  39. package/guides/payment-widget/nextjs.md +41 -0
  40. package/guides/payment-widget/overview.md +32 -0
  41. package/guides/webhooks/nextjs.md +48 -0
  42. package/guides/webhooks/node.md +48 -0
  43. package/guides/webhooks/overview.md +34 -0
  44. package/guides/webhooks/php.md +46 -0
  45. package/guides/webhooks/python.md +55 -0
  46. package/guides/withdrawals/nextjs.md +35 -0
  47. package/guides/withdrawals/node.md +35 -0
  48. package/guides/withdrawals/overview.md +32 -0
  49. package/guides/withdrawals/php.md +37 -0
  50. package/guides/withdrawals/python.md +33 -0
  51. package/package.json +55 -4
package/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright (c) Finrock. All rights reserved.
2
+
3
+ Use of this software, including the @finrock/mcp package and the hosted server at
4
+ https://mcp.finrock.io, is subject to the Finrock Terms of Service:
5
+ https://finrock.io/terms-of-service
6
+
7
+ See also the Finrock Privacy Policy: https://finrock.io/privacy-policy
package/README.md CHANGED
@@ -1,3 +1,215 @@
1
- # Temporary Holding Version
1
+ # Finrock MCP server
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![Install in VS Code](https://img.shields.io/badge/VS_Code-Install_Finrock-0098FF?logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=finrock&config=%7B%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fmcp.finrock.io%22%7D)
4
+ [![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install_Finrock-24bfa5?logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=finrock&config=%7B%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fmcp.finrock.io%22%7D&quality=insiders)
5
+ [![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.png)](https://cursor.com/en/install-mcp?name=finrock&config=eyJ1cmwiOiJodHRwczovL21jcC5maW5yb2NrLmlvIn0=)
6
+
7
+ An [MCP](https://modelcontextprotocol.io) server that lets AI coding agents (Claude, Codex,
8
+ Cursor, VS Code Copilot and others) add [Finrock](https://finrock.io) crypto payments to any
9
+ application.
10
+
11
+ The hosted server at **`https://mcp.finrock.io`** (the older `https://mcp.finrock.io/mcp` also works) needs no API key. It reads the live docs at
12
+ [apidocs.finrock.io](https://apidocs.finrock.io) and returns tested integration code, so an agent
13
+ can go from "accept USDT at checkout" to a working, secure implementation in your codebase. It
14
+ never touches anyone's Finrock account: API calls are signed with your private key, which stays
15
+ on your servers.
16
+
17
+ ## Tools
18
+
19
+ | Tool | What it does |
20
+ |---|---|
21
+ | `list_integration_guides` | The ready-made recipes and supported stacks |
22
+ | `get_integration_guide` | Steps plus code for a goal in a stack (`node`, `nextjs`, `python`, `php`) |
23
+ | `search_docs` | Full-text search over the live Finrock docs |
24
+ | `read_doc` | One docs page as Markdown, with request and response examples |
25
+ | `list_docs` | Every docs page, grouped by section |
26
+ | `verify_webhook_signature` | Check a captured webhook body and `x-signature` against Finrock's key |
27
+ | `setup_account_access` | Walk through connecting a Finrock account with keys kept in the OS keychain |
28
+ | `list_supported_assets` | Supported coins and tokens with the exact asset name to use in API calls |
29
+ | `get_asset_price` | Spot price of a crypto in a fiat currency |
30
+
31
+ There is also a prompt, `integrate_finrock`, that walks an agent through the whole integration.
32
+
33
+ Integration goals: `accept-payments` (hosted invoices), `payment-widget`, `deposit-addresses`,
34
+ `webhooks`, `withdrawals` and `authentication`.
35
+
36
+ ## Connect your agent
37
+
38
+ **Claude Code**
39
+
40
+ ```bash
41
+ claude mcp add --transport http finrock https://mcp.finrock.io
42
+ ```
43
+
44
+ Or install it as a plugin, which keeps the server up to date:
45
+
46
+ ```bash
47
+ claude plugin marketplace add gofinrock/mcp
48
+ claude plugin install finrock@finrock
49
+ ```
50
+
51
+ **Claude.ai / Claude Desktop**: Settings → Connectors → Add custom connector →
52
+ `https://mcp.finrock.io`.
53
+
54
+ **Cursor** (`~/.cursor/mcp.json` or `.cursor/mcp.json`)
55
+
56
+ ```json
57
+ { "mcpServers": { "finrock": { "url": "https://mcp.finrock.io" } } }
58
+ ```
59
+
60
+ **VS Code** (`.vscode/mcp.json`)
61
+
62
+ ```json
63
+ { "servers": { "finrock": { "type": "http", "url": "https://mcp.finrock.io" } } }
64
+ ```
65
+
66
+ **Codex** (`~/.codex/config.toml`)
67
+
68
+ ```toml
69
+ [mcp_servers.finrock]
70
+ url = "https://mcp.finrock.io"
71
+ ```
72
+
73
+ Any client that only supports stdio can run the same server locally once the package is
74
+ published: `npx -y @finrock/mcp`.
75
+
76
+ Then ask your agent something like: *"Use Finrock to add a crypto checkout to this app."*
77
+
78
+ ## Manage a Finrock account (local package)
79
+
80
+ The hosted server never sees anyone's credentials. To let an agent work with a real Finrock
81
+ account (create invoices and deposit addresses, check balances and transactions, and
82
+ optionally send payouts), run the package locally with your own keys. Requests are signed on
83
+ your machine and the private key never leaves it.
84
+
85
+ ```bash
86
+ claude mcp add finrock \
87
+ -e FINROCK_API_KEY=<API Key UUID> \
88
+ -e FINROCK_PRIVATE_KEY_PATH=/path/to/finrock_private.pem \
89
+ -- npx -y @finrock/mcp
90
+ ```
91
+
92
+ Or in any client's JSON config:
93
+
94
+ ```json
95
+ {
96
+ "mcpServers": {
97
+ "finrock": {
98
+ "command": "npx",
99
+ "args": ["-y", "@finrock/mcp"],
100
+ "env": {
101
+ "FINROCK_API_KEY": "<API Key UUID>",
102
+ "FINROCK_PRIVATE_KEY_PATH": "/path/to/finrock_private.pem"
103
+ }
104
+ }
105
+ }
106
+ }
107
+ ```
108
+
109
+ ### Keep the keys in your keychain
110
+
111
+ Instead of putting the keys in the config, `FINROCK_API_KEY_COMMAND` and
112
+ `FINROCK_PRIVATE_KEY_COMMAND` can name a command whose output is the secret: the macOS Keychain,
113
+ Linux Secret Service, a Windows DPAPI-encrypted file, or a password manager CLI such as
114
+ 1Password's `op read`. The package runs each command once at startup and keeps the result only in
115
+ memory. Ask your agent to *"set up my Finrock account"* and it calls `setup_account_access`, which
116
+ gives the exact commands for your OS and client. For example, on macOS:
117
+
118
+ ```bash
119
+ claude mcp add finrock --scope user \
120
+ -e FINROCK_API_KEY_COMMAND='/usr/bin/security find-generic-password -s finrock-api-key -w' \
121
+ -e FINROCK_PRIVATE_KEY_COMMAND='/usr/bin/security find-generic-password -s finrock-private-key -w' \
122
+ -- npx -y @finrock/mcp
123
+ ```
124
+
125
+ Never paste either key into a chat with an agent.
126
+
127
+ Until `@finrock/mcp` is published to npm, run it from a clone instead: `npm ci && npm run build`,
128
+ then use `node /path/to/mcp/dist/stdio.js` as the command.
129
+
130
+ Create the API user and key pair as described in the `authentication` guide (or the
131
+ [Quick Start](https://apidocs.finrock.io/quickstart)). Use a dedicated API user for the agent and
132
+ restrict it to your IP.
133
+
134
+ ### Access levels
135
+
136
+ | Level | Tools | Enabled |
137
+ |---|---|---|
138
+ | Read | `list_wallets`, `list_addresses`, `validate_address`, `list_invoices`, `list_transactions`, `estimate_fee`, `get_network_fee_rates`, `list_gas_tanks`, `get_gas_tank_balance`, `get_prepaid_fee`, `list_exchange_accounts`, `get_exchange_balance`, `get_aml_status`, `list_frozen_addresses` | Always |
139
+ | Create | `create_invoice`, `create_wallet`, `create_address`, `update_address`, `generate_xrp_destination_tag` | Unless `FINROCK_READ_ONLY=true` |
140
+ | Sensitive (moves funds or changes security) | `create_transaction`, `bump_fee`, `sweep_address`, `update_wallet`, `configure_aml`, `unfreeze_address`, `stake_trx`, `unstake_trx`, `release_unstaked_trx` | Only with `FINROCK_ENABLE_SENSITIVE_TOOLS=true` |
141
+
142
+ Disabled tools are not shown to the agent at all. `create_transaction` requires a
143
+ `sequence_id`, so a retried call returns the original transaction instead of paying twice.
144
+
145
+ | Variable | Purpose |
146
+ |---|---|
147
+ | `FINROCK_API_KEY` | API Key UUID of the API user |
148
+ | `FINROCK_PRIVATE_KEY_PATH` or `FINROCK_PRIVATE_KEY` | RSA private key (path, or PEM text with `\n` escapes allowed) |
149
+ | `FINROCK_API_KEY_COMMAND` | Command that prints the API Key UUID, used when `FINROCK_API_KEY` is unset |
150
+ | `FINROCK_PRIVATE_KEY_COMMAND` | Command that prints the PEM (`\n` escapes allowed), used when neither of the above is set |
151
+ | `FINROCK_READ_ONLY` | `true` to expose read tools only |
152
+ | `FINROCK_ENABLE_SENSITIVE_TOOLS` | `true` to expose tools that move funds or change AML settings |
153
+ | `FINROCK_API_URL` | Override the API base URL (default `https://api.finrock.io`) |
154
+
155
+ ## Development
156
+
157
+ ```bash
158
+ npm ci
159
+ npm test # unit tests plus an end-to-end MCP client test over HTTP
160
+ npm run typecheck
161
+ npm run dev # HTTP server on :3000 in watch mode
162
+ ```
163
+
164
+ Try it interactively with the MCP Inspector:
165
+
166
+ ```bash
167
+ npx @modelcontextprotocol/inspector # then connect to http://localhost:3000
168
+ ```
169
+
170
+ Integration guides are plain Markdown in [`guides/`](guides): `overview.md` per goal (with
171
+ frontmatter), one file per stack, and the request-signing client per stack in `guides/_client/`.
172
+ Edit them without touching TypeScript; tests check that every goal renders for every stack.
173
+
174
+ ## Directory listings
175
+
176
+ The repo carries the metadata each directory reads:
177
+
178
+ | File | Used by |
179
+ |---|---|
180
+ | `server.json` | [Official MCP Registry](https://registry.modelcontextprotocol.io) as `io.finrock/mcp`, which feeds the VS Code / GitHub MCP gallery and other downstream registries |
181
+ | `package.json` `mcpName` | Proves to the MCP Registry that `@finrock/mcp` on npm belongs to `io.finrock/mcp` |
182
+ | `.claude-plugin/marketplace.json`, `plugins/finrock/` | Claude Code plugin marketplace (`claude plugin marketplace add gofinrock/mcp`) and the Claude plugin directory |
183
+ | `glama.json` | [Glama](https://glama.ai/mcp/servers) maintainer claim |
184
+
185
+ `npm test` checks that the name and version in `server.json`, `package.json`, the plugin and the
186
+ server agree, so bump all of them together.
187
+
188
+ **Publishing a release to the MCP Registry.** The `io.finrock/` namespace is proven with a DNS TXT
189
+ record on `finrock.io` (one-time setup):
190
+
191
+ ```bash
192
+ openssl genpkey -algorithm Ed25519 -out mcp-registry-key.pem # keep this out of the repo
193
+ PUBLIC_KEY="$(openssl pkey -in mcp-registry-key.pem -pubout -outform DER | tail -c 32 | base64)"
194
+ echo "finrock.io. IN TXT \"v=MCPv1; k=ed25519; p=${PUBLIC_KEY}\"" # add this record to finrock.io
195
+ ```
196
+
197
+ **Publishing to npm.** Bump the version everywhere (see above), commit to `main`, then push a
198
+ matching tag: `git tag v0.1.1 && git push origin v0.1.1`. The `Release` workflow checks the tag
199
+ against `package.json`, runs the tests and publishes `@finrock/mcp` with npm trusted publishing, so
200
+ no npm token is stored in the repo. One-time setup on npmjs.com: package **Settings → Trusted
201
+ Publisher → GitHub Actions**, organization `gofinrock`, repository `mcp`, workflow `release.yml`.
202
+
203
+ Then for each release (after the npm publish, because the registry checks that the npm version
204
+ exists):
205
+
206
+ ```bash
207
+ brew install mcp-publisher # or download it from github.com/modelcontextprotocol/registry/releases
208
+ PRIVATE_KEY="$(openssl pkey -in mcp-registry-key.pem -noout -text | grep -A3 "priv:" | tail -n +2 | tr -d ' :\n')"
209
+ mcp-publisher login dns --domain finrock.io --private-key "${PRIVATE_KEY}"
210
+ mcp-publisher publish
211
+ ```
212
+
213
+ ## Roadmap
214
+
215
+ - npm release of `@finrock/mcp`.
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Signed client for the Finrock REST API. Each request carries the API key and a
3
+ * fresh RS256 JWT (unique nonce, exp <= iat + 30s, SHA-256 of the exact body).
4
+ * Credentials only ever exist in the process that runs this client, i.e. on the
5
+ * developer's own machine; the hosted server never constructs one.
6
+ */
7
+ export declare const DEFAULT_API_URL = "https://api.finrock.io";
8
+ export interface FinrockClientOptions {
9
+ apiKey: string;
10
+ /** PEM text of the RSA private key whose public half is uploaded to Finrock. */
11
+ privateKey: string;
12
+ baseUrl?: string;
13
+ fetch?: typeof fetch;
14
+ timeoutMs?: number;
15
+ }
16
+ export type Query = Record<string, string | number | boolean | undefined>;
17
+ export declare class FinrockApiError extends Error {
18
+ readonly status: number;
19
+ readonly method: string;
20
+ readonly path: string;
21
+ readonly body: string;
22
+ readonly retryAfter?: string | undefined;
23
+ constructor(status: number, method: string, path: string, body: string, retryAfter?: string | undefined);
24
+ }
25
+ export declare class FinrockClient {
26
+ readonly baseUrl: string;
27
+ private readonly apiKey;
28
+ private readonly key;
29
+ private readonly fetchImpl;
30
+ private readonly timeoutMs;
31
+ constructor(opts: FinrockClientOptions);
32
+ /**
33
+ * Build a client from FINROCK_API_KEY plus FINROCK_PRIVATE_KEY (PEM, `\n` escapes
34
+ * allowed) or FINROCK_PRIVATE_KEY_PATH. Either secret can instead come from the stdout of
35
+ * FINROCK_API_KEY_COMMAND / FINROCK_PRIVATE_KEY_COMMAND, so it can live in the OS keychain
36
+ * or a secret manager rather than in the client's config. Returns undefined when no key is configured.
37
+ */
38
+ static fromEnv(env?: NodeJS.ProcessEnv): FinrockClient | undefined;
39
+ get<T = unknown>(path: string, query?: Query): Promise<T>;
40
+ post<T = unknown>(path: string, body?: unknown): Promise<T>;
41
+ patch<T = unknown>(path: string, body: unknown): Promise<T>;
42
+ /** Unauthenticated GET for Finrock's public endpoints (assets, prices). */
43
+ getPublic<T = unknown>(path: string, query?: Query): Promise<T>;
44
+ request<T>(method: string, pathWithQuery: string, body?: unknown): Promise<T>;
45
+ signJwt(uri: string, rawBody: string, now?: number): string;
46
+ }
47
+ export declare function publicGet<T>(baseUrl: string, pathWithQuery: string, fetchImpl?: typeof fetch, timeoutMs?: number): Promise<T>;
48
+ export declare function withQuery(path: string, query?: Query): string;
49
+ /** An error's message plus its cause: Node's fetch reports DNS, TLS and proxy failures as a bare "fetch failed". */
50
+ export declare function errorMessage(err: unknown): string;
@@ -0,0 +1,172 @@
1
+ import { createHash, createPrivateKey, randomUUID, sign } from "node:crypto";
2
+ import { execSync } from "node:child_process";
3
+ import { readFileSync } from "node:fs";
4
+ /**
5
+ * Signed client for the Finrock REST API. Each request carries the API key and a
6
+ * fresh RS256 JWT (unique nonce, exp <= iat + 30s, SHA-256 of the exact body).
7
+ * Credentials only ever exist in the process that runs this client, i.e. on the
8
+ * developer's own machine; the hosted server never constructs one.
9
+ */
10
+ export const DEFAULT_API_URL = "https://api.finrock.io";
11
+ export class FinrockApiError extends Error {
12
+ status;
13
+ method;
14
+ path;
15
+ body;
16
+ retryAfter;
17
+ constructor(status, method, path, body, retryAfter) {
18
+ super(`Finrock ${method} ${path} failed with HTTP ${status}${body ? `: ${body}` : ""}`);
19
+ this.status = status;
20
+ this.method = method;
21
+ this.path = path;
22
+ this.body = body;
23
+ this.retryAfter = retryAfter;
24
+ }
25
+ }
26
+ export class FinrockClient {
27
+ baseUrl;
28
+ apiKey;
29
+ key;
30
+ fetchImpl;
31
+ timeoutMs;
32
+ constructor(opts) {
33
+ if (!opts.apiKey)
34
+ throw new Error("A Finrock API key is required");
35
+ this.apiKey = opts.apiKey;
36
+ this.key = createPrivateKey(opts.privateKey);
37
+ if (this.key.asymmetricKeyType !== "rsa")
38
+ throw new Error("The Finrock private key must be an RSA key");
39
+ this.baseUrl = (opts.baseUrl ?? DEFAULT_API_URL).replace(/\/+$/, "");
40
+ this.fetchImpl = opts.fetch ?? fetch;
41
+ this.timeoutMs = opts.timeoutMs ?? 30_000;
42
+ }
43
+ /**
44
+ * Build a client from FINROCK_API_KEY plus FINROCK_PRIVATE_KEY (PEM, `\n` escapes
45
+ * allowed) or FINROCK_PRIVATE_KEY_PATH. Either secret can instead come from the stdout of
46
+ * FINROCK_API_KEY_COMMAND / FINROCK_PRIVATE_KEY_COMMAND, so it can live in the OS keychain
47
+ * or a secret manager rather than in the client's config. Returns undefined when no key is configured.
48
+ */
49
+ static fromEnv(env = process.env) {
50
+ const apiKey = (env.FINROCK_API_KEY || secretFromCommand("FINROCK_API_KEY_COMMAND", env))?.trim();
51
+ if (!apiKey)
52
+ return undefined;
53
+ let pem = env.FINROCK_PRIVATE_KEY;
54
+ if (!pem && env.FINROCK_PRIVATE_KEY_PATH)
55
+ pem = readFileSync(env.FINROCK_PRIVATE_KEY_PATH, "utf8");
56
+ if (!pem)
57
+ pem = secretFromCommand("FINROCK_PRIVATE_KEY_COMMAND", env);
58
+ if (!pem) {
59
+ throw new Error("A Finrock API key is configured but no private key: set FINROCK_PRIVATE_KEY, FINROCK_PRIVATE_KEY_PATH or FINROCK_PRIVATE_KEY_COMMAND");
60
+ }
61
+ return new FinrockClient({ apiKey, privateKey: pem.replace(/\\n/g, "\n"), baseUrl: env.FINROCK_API_URL });
62
+ }
63
+ get(path, query) {
64
+ return this.request("GET", withQuery(path, query));
65
+ }
66
+ post(path, body = {}) {
67
+ return this.request("POST", path, body);
68
+ }
69
+ patch(path, body) {
70
+ return this.request("PATCH", path, body);
71
+ }
72
+ /** Unauthenticated GET for Finrock's public endpoints (assets, prices). */
73
+ async getPublic(path, query) {
74
+ return publicGet(this.baseUrl, withQuery(path, query), this.fetchImpl, this.timeoutMs);
75
+ }
76
+ async request(method, pathWithQuery, body) {
77
+ // Serialise once: the bytes hashed into the JWT must be the bytes sent.
78
+ const raw = body === undefined ? "" : JSON.stringify(body);
79
+ const headers = {
80
+ authorization: `Bearer ${this.signJwt(pathWithQuery, raw)}`,
81
+ "x-api-key": this.apiKey,
82
+ accept: "application/json",
83
+ };
84
+ if (raw)
85
+ headers["content-type"] = "application/json";
86
+ const res = await this.fetchImpl(this.baseUrl + pathWithQuery, {
87
+ method,
88
+ headers,
89
+ body: raw || undefined,
90
+ signal: AbortSignal.timeout(this.timeoutMs),
91
+ });
92
+ return parseResponse(res, method, pathWithQuery);
93
+ }
94
+ signJwt(uri, rawBody, now = Math.floor(Date.now() / 1000)) {
95
+ const header = { alg: "RS256", typ: "JWT" };
96
+ const bodyHash = createHash("sha256").update(rawBody).digest("hex");
97
+ const payload = {
98
+ uri,
99
+ nonce: randomUUID(),
100
+ iat: now,
101
+ exp: now + 25,
102
+ sub: this.apiKey,
103
+ bodyHash,
104
+ // api.finrock.io currently reads the lower-case claim; send both so either spelling verifies.
105
+ bodyhash: bodyHash,
106
+ };
107
+ const input = `${b64url(JSON.stringify(header))}.${b64url(JSON.stringify(payload))}`;
108
+ return `${input}.${b64url(sign("sha256", Buffer.from(input), this.key))}`;
109
+ }
110
+ }
111
+ export async function publicGet(baseUrl, pathWithQuery, fetchImpl = fetch, timeoutMs = 30_000) {
112
+ const res = await fetchImpl(baseUrl.replace(/\/+$/, "") + pathWithQuery, {
113
+ headers: { accept: "application/json" },
114
+ signal: AbortSignal.timeout(timeoutMs),
115
+ });
116
+ return parseResponse(res, "GET", pathWithQuery);
117
+ }
118
+ async function parseResponse(res, method, path) {
119
+ const text = await res.text();
120
+ if (!res.ok) {
121
+ const retry = res.headers.get("x-rate-limit-reset") ?? res.headers.get("retry-after") ?? undefined;
122
+ throw new FinrockApiError(res.status, method, path, text.slice(0, 2000), retry);
123
+ }
124
+ if (!text)
125
+ return null;
126
+ try {
127
+ return JSON.parse(text);
128
+ }
129
+ catch {
130
+ return text;
131
+ }
132
+ }
133
+ export function withQuery(path, query) {
134
+ if (!query)
135
+ return path;
136
+ const params = new URLSearchParams();
137
+ for (const [k, v] of Object.entries(query))
138
+ if (v !== undefined && v !== "")
139
+ params.set(k, String(v));
140
+ const qs = params.toString();
141
+ return qs ? `${path}?${qs}` : path;
142
+ }
143
+ /** An error's message plus its cause: Node's fetch reports DNS, TLS and proxy failures as a bare "fetch failed". */
144
+ export function errorMessage(err) {
145
+ const msg = err.message;
146
+ const cause = err.cause;
147
+ return cause?.message ? `${msg} (${cause.code ? `${cause.code}: ` : ""}${cause.message})` : msg;
148
+ }
149
+ const b64url = (b) => Buffer.from(b).toString("base64url");
150
+ /**
151
+ * Run the shell command in env[name] and return its trimmed stdout. Its stdout is never
152
+ * logged (stdio MCP uses this process's stdout); stderr passes through so keychain or
153
+ * secret-manager prompts and errors stay visible in the client's MCP log.
154
+ */
155
+ function secretFromCommand(name, env) {
156
+ const cmd = env[name]?.trim();
157
+ if (!cmd)
158
+ return undefined;
159
+ let out;
160
+ try {
161
+ out = execSync(cmd, { encoding: "utf8", env, stdio: ["ignore", "pipe", "inherit"], timeout: 60_000 });
162
+ }
163
+ catch (err) {
164
+ const e = err;
165
+ const why = e.signal ? `was stopped (${e.signal})` : `exited with status ${e.status ?? "unknown"}`;
166
+ throw new Error(`${name} ${why}`);
167
+ }
168
+ const value = out.trim();
169
+ if (!value)
170
+ throw new Error(`${name} printed nothing`);
171
+ return value;
172
+ }
@@ -0,0 +1,17 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { type FinrockClient } from "./client.js";
3
+ /**
4
+ * Account tools, registered only when the server runs with the user's own
5
+ * credentials (the local stdio package). Three tiers:
6
+ * read always on
7
+ * write creates records that move no money (invoices, wallets, addresses); off with FINROCK_READ_ONLY
8
+ * sensitive moves funds or changes security settings; on only with FINROCK_ENABLE_SENSITIVE_TOOLS
9
+ */
10
+ export type Tier = "read" | "write" | "sensitive";
11
+ export interface ApiToolOptions {
12
+ readOnly?: boolean;
13
+ enableSensitive?: boolean;
14
+ }
15
+ export declare function apiToolMode(env?: NodeJS.ProcessEnv): ApiToolOptions;
16
+ export declare function describeMode(opts: ApiToolOptions): string;
17
+ export declare function registerApiTools(server: McpServer, api: FinrockClient, opts?: ApiToolOptions): string[];