@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.
- package/LICENSE +7 -0
- package/README.md +214 -2
- package/dist/api/client.d.ts +50 -0
- package/dist/api/client.js +172 -0
- package/dist/api/tools.d.ts +17 -0
- package/dist/api/tools.js +300 -0
- package/dist/docs.d.ts +61 -0
- package/dist/docs.js +190 -0
- package/dist/guides.d.ts +19 -0
- package/dist/guides.js +66 -0
- package/dist/http.d.ts +13 -0
- package/dist/http.js +94 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/server.d.ts +17 -0
- package/dist/server.js +229 -0
- package/dist/setup.d.ts +10 -0
- package/dist/setup.js +123 -0
- package/dist/stdio.d.ts +2 -0
- package/dist/stdio.js +28 -0
- package/dist/webhook.d.ts +11 -0
- package/dist/webhook.js +39 -0
- package/guides/_client/nextjs.md +57 -0
- package/guides/_client/node.md +55 -0
- package/guides/_client/php.md +67 -0
- package/guides/_client/python.md +55 -0
- package/guides/accept-payments/nextjs.md +75 -0
- package/guides/accept-payments/node.md +49 -0
- package/guides/accept-payments/overview.md +30 -0
- package/guides/accept-payments/php.md +61 -0
- package/guides/accept-payments/python.md +59 -0
- package/guides/authentication/overview.md +60 -0
- package/guides/deposit-addresses/nextjs.md +50 -0
- package/guides/deposit-addresses/node.md +30 -0
- package/guides/deposit-addresses/overview.md +36 -0
- package/guides/deposit-addresses/php.md +35 -0
- package/guides/deposit-addresses/python.md +31 -0
- package/guides/payment-widget/html.md +26 -0
- package/guides/payment-widget/nextjs.md +41 -0
- package/guides/payment-widget/overview.md +32 -0
- package/guides/webhooks/nextjs.md +48 -0
- package/guides/webhooks/node.md +48 -0
- package/guides/webhooks/overview.md +34 -0
- package/guides/webhooks/php.md +46 -0
- package/guides/webhooks/python.md +55 -0
- package/guides/withdrawals/nextjs.md +35 -0
- package/guides/withdrawals/node.md +35 -0
- package/guides/withdrawals/overview.md +32 -0
- package/guides/withdrawals/php.md +37 -0
- package/guides/withdrawals/python.md +33 -0
- 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
|
-
#
|
|
1
|
+
# Finrock MCP server
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](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
|
+
[](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
|
+
[](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[];
|