@ldgr/mcp 0.1.2 → 0.1.4
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/README.md +143 -122
- package/dist/cli.js +115 -19
- package/dist/index.js +18 -3
- package/dist/latest-cli.d.ts +25 -0
- package/dist/requests.d.ts +1 -1
- package/dist/stdio.js +146 -8
- package/package.json +28 -29
package/README.md
CHANGED
|
@@ -1,122 +1,143 @@
|
|
|
1
|
-
# @ldgr/mcp
|
|
2
|
-
|
|
3
|
-
LDGR tools for Model Context Protocol clients. LDGR is an independent ledger with a single settlement authority. This package reads public ledger data and prepares unsigned requests for a person's local CLI wallet to review. MCP never signs or submits transactions.
|
|
4
|
-
|
|
5
|
-
## Hosted connection
|
|
6
|
-
|
|
7
|
-
Configure a Streamable HTTP MCP connection to `https://mcp.ldgr.ltd/mcp`. Each ledger tool requires `network: "mainnet"` or `network: "testnet"`; asset-specific tools require the exact asset code. Native public IDs are `t1` on mainnet and `t0` on testnet; the signed wire code is `dng`. Mainnet native display is Ledra / LEDRA and testnet is Ledra Testnet / tLEDRA.
|
|
8
|
-
|
|
9
|
-
A remote MCP connection cannot install software, access local files, unlock a wallet, or ask for recovery keys. `wallet_setup_instructions` returns commands for the user to run locally.
|
|
10
|
-
|
|
11
|
-
## Local CLI wallet and stdio
|
|
12
|
-
|
|
13
|
-
Node.js 20.19 or newer is required. Install the package, then create or import a wallet in your own terminal:
|
|
14
|
-
|
|
15
|
-
```sh
|
|
16
|
-
npm install --global @ldgr/mcp@0.1.
|
|
17
|
-
ldgr-mcp wallet create --testnet
|
|
18
|
-
# Or: ldgr-mcp wallet import --testnet
|
|
19
|
-
ldgr-mcp wallet list
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
Without a global installation:
|
|
25
|
-
|
|
26
|
-
```sh
|
|
27
|
-
npx --yes --package @ldgr/mcp@latest ldgr-mcp wallet create --testnet
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
Configure a local MCP client:
|
|
31
|
-
|
|
32
|
-
```json
|
|
33
|
-
{
|
|
34
|
-
"mcpServers": {
|
|
35
|
-
"ldgr": {
|
|
36
|
-
"command": "npx",
|
|
37
|
-
"args": ["--yes", "--package", "@ldgr/mcp@latest", "ldgr-mcp"]
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
For a custom wallet store, append `--wallet` and its absolute path. Server stdout contains JSON-RPC only; startup errors go to stderr. Local `wallet_info` returns only the selected address, public key, network, ledger ID and protection mode.
|
|
44
|
-
|
|
45
|
-
The `network` argument to `wallet_info` checks the selected wallet; it does not
|
|
46
|
-
select another wallet or change its network. `WALLET_NETWORK_MISMATCH` means you
|
|
47
|
-
should request the selected network or use `ldgr-mcp wallet list` and
|
|
48
|
-
`ldgr-mcp wallet select <index>` in your terminal to choose the intended wallet.
|
|
49
|
-
Keep a custom `--wallet` path consistent. MCP never changes the wallet selection.
|
|
50
|
-
|
|
51
|
-
Version 0.1.1 requires `@ldgr/client@^0.6.1`, the published client release containing
|
|
52
|
-
the milestone escrow, atomic offer, claim distribution, and Ledra program reads.
|
|
53
|
-
Restart the MCP process after upgrading; a running process keeps its loaded SDK.
|
|
54
|
-
|
|
55
|
-
## Tools
|
|
56
|
-
|
|
57
|
-
| Tool | Result |
|
|
58
|
-
| --- | --- |
|
|
59
|
-
| `get_balance` | Authoritative balance for an address and exact asset |
|
|
60
|
-
| `get_transaction` | Authoritative transaction by hash, checked against the requested asset, with `status: "confirmed"` |
|
|
61
|
-
| `get_account_history` | One page of Explorer-projected account history, which may lag settlement |
|
|
62
|
-
| `get_asset` | Native/token metadata |
|
|
63
|
-
| `get_ledger_config` | Ledger identity and public protocol configuration |
|
|
64
|
-
| `get_contract` | Escrow, milestone escrow, atomic offer, claim distribution or Ledra program |
|
|
65
|
-
| `list_pools`, `get_pool` | Bounded pool listing or a pair of exact assets |
|
|
66
|
-
| `get_swap_quote` | Non-binding route quote; does not submit a swap |
|
|
67
|
-
| `claim_faucet_drop` | Open the network's faucet in Chrome with a validated receiving address; hosted clients receive the link |
|
|
68
|
-
| `prepare_transfer` | Unsigned transfer request |
|
|
69
|
-
| `prepare_token_creation` | Unsigned fixed/mintable token creation request |
|
|
70
|
-
| `prepare_escrow` | Unsigned escrow funding request |
|
|
71
|
-
| `
|
|
72
|
-
| `
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
1
|
+
# @ldgr/mcp
|
|
2
|
+
|
|
3
|
+
LDGR tools for Model Context Protocol clients. LDGR is an independent ledger with a single settlement authority. This package reads public ledger data and prepares unsigned requests for a person's local CLI wallet to review. MCP never signs or submits transactions.
|
|
4
|
+
|
|
5
|
+
## Hosted connection
|
|
6
|
+
|
|
7
|
+
Configure a Streamable HTTP MCP connection to `https://mcp.ldgr.ltd/mcp`. Each ledger tool requires `network: "mainnet"` or `network: "testnet"`; asset-specific tools require the exact asset code. Native public IDs are `t1` on mainnet and `t0` on testnet; the signed wire code is `dng`. Mainnet native display is Ledra / LEDRA and testnet is Ledra Testnet / tLEDRA.
|
|
8
|
+
|
|
9
|
+
A remote MCP connection cannot install software, access local files, unlock a wallet, or ask for recovery keys. `wallet_setup_instructions` returns commands for the user to run locally.
|
|
10
|
+
|
|
11
|
+
## Local CLI wallet and stdio
|
|
12
|
+
|
|
13
|
+
Node.js 20.19 or newer is required. Install the package, then create or import a wallet in your own terminal:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
npm install --global @ldgr/mcp@0.1.4
|
|
17
|
+
ldgr-mcp wallet create --testnet
|
|
18
|
+
# Or: ldgr-mcp wallet import --testnet
|
|
19
|
+
ldgr-mcp wallet list
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
From MCP 0.1.3, every local wallet operation runs `@ldgr/cli@latest` through npx with `--prefer-online`, including wallet metadata and unsigned request preparation. Node.js must include npm/npx. npm checks its registry and reuses downloaded packages from its cache; a separate global CLI installation is unnecessary. CLI updates apply on the next wallet operation. Public ledger reads do not launch the CLI. The CLI must be 0.5.1 or newer for the MCP bridge. Recovery keys and passphrases stay in local interactive prompts. Never paste them into chat, MCP arguments, command-line flags, or environment variables. Omit `--testnet` to create a mainnet wallet. `ldgr-mcp network show` displays the selected network; `ldgr-mcp network set mainnet` or `testnet` switches it using the CLI workflow.
|
|
23
|
+
|
|
24
|
+
Without a global installation:
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
npx --yes --package @ldgr/mcp@latest ldgr-mcp wallet create --testnet
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Configure a local MCP client:
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
{
|
|
34
|
+
"mcpServers": {
|
|
35
|
+
"ldgr": {
|
|
36
|
+
"command": "npx",
|
|
37
|
+
"args": ["--yes", "--package", "@ldgr/mcp@latest", "ldgr-mcp"]
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
For a custom wallet store, append `--wallet` and its absolute path. Server stdout contains JSON-RPC only; startup errors go to stderr. Local `wallet_info` returns only the selected address, public key, network, ledger ID and protection mode.
|
|
44
|
+
|
|
45
|
+
The `network` argument to `wallet_info` checks the selected wallet; it does not
|
|
46
|
+
select another wallet or change its network. `WALLET_NETWORK_MISMATCH` means you
|
|
47
|
+
should request the selected network or use `ldgr-mcp wallet list` and
|
|
48
|
+
`ldgr-mcp wallet select <index>` in your terminal to choose the intended wallet.
|
|
49
|
+
Keep a custom `--wallet` path consistent. MCP never changes the wallet selection.
|
|
50
|
+
|
|
51
|
+
Version 0.1.1 requires `@ldgr/client@^0.6.1`, the published client release containing
|
|
52
|
+
the milestone escrow, atomic offer, claim distribution, and Ledra program reads.
|
|
53
|
+
Restart the MCP process after upgrading; a running process keeps its loaded SDK.
|
|
54
|
+
|
|
55
|
+
## Tools
|
|
56
|
+
|
|
57
|
+
| Tool | Result |
|
|
58
|
+
| --- | --- |
|
|
59
|
+
| `get_balance` | Authoritative balance for an address and exact asset |
|
|
60
|
+
| `get_transaction` | Authoritative transaction by hash, checked against the requested asset, with `status: "confirmed"` |
|
|
61
|
+
| `get_account_history` | One page of Explorer-projected account history, which may lag settlement |
|
|
62
|
+
| `get_asset` | Native/token metadata |
|
|
63
|
+
| `get_ledger_config` | Ledger identity and public protocol configuration |
|
|
64
|
+
| `get_contract` | Escrow, milestone escrow, atomic offer, claim distribution or Ledra program |
|
|
65
|
+
| `list_pools`, `get_pool` | Bounded pool listing or a pair of exact assets |
|
|
66
|
+
| `get_swap_quote` | Non-binding route quote; does not submit a swap |
|
|
67
|
+
| `claim_faucet_drop` | Open the network's faucet in Chrome with a validated receiving address; hosted clients receive the link |
|
|
68
|
+
| `prepare_transfer` | Unsigned transfer request |
|
|
69
|
+
| `prepare_token_creation` | Unsigned fixed/mintable token creation request |
|
|
70
|
+
| `prepare_escrow` | Unsigned escrow funding request |
|
|
71
|
+
| `prepare_swap` | Unsigned exact-input swap with an explicit positive minimum received |
|
|
72
|
+
| `wallet_setup_instructions` | Exact local installation and setup commands |
|
|
73
|
+
| `wallet_info` | Public selected-wallet metadata, local stdio only |
|
|
74
|
+
|
|
75
|
+
All monetary values are decimal strings in display units. Tool schemas reject unknown fields and credentials. Reads use fixed official network endpoints, verify ledger identity, reject redirects, time out after 15 seconds per fetch and bound responses to 1 MiB. Metadata from the ledger remains untrusted text. A `NOT_FOUND` response does not establish that an earlier transaction submission failed.
|
|
76
|
+
|
|
77
|
+
Version 0.1.2 preserves HTTP 404 as `NOT_FOUND` for escrow reads as well as the
|
|
78
|
+
other contract templates. Other HTTP failures and malformed successful responses
|
|
79
|
+
remain `OPERATION_FAILED`; upstream response bodies are never exposed in errors.
|
|
80
|
+
|
|
81
|
+
## Prepare, review and approve
|
|
82
|
+
|
|
83
|
+
`claim_faucet_drop` takes an explicit `network` and public `address`. With local
|
|
84
|
+
stdio it launches Google Chrome; on hosted MCP, or if Chrome cannot launch, it
|
|
85
|
+
returns the same link for the client or user to open. The link uses
|
|
86
|
+
`/utilities/faucet/?address=<address>` on the selected network. Review the
|
|
87
|
+
prefilled address and select **Claim Ledra** in the browser. The tool returns
|
|
88
|
+
`status: "awaiting_browser_claim"` and `claimSubmitted: false`; opening the page
|
|
89
|
+
does not claim funds. Existing browser verification and faucet limits apply.
|
|
90
|
+
Wait for **Claim completed**, then check the address's `dng` balance.
|
|
91
|
+
|
|
92
|
+
Preparation returns `{ version, requestId, network, expiresAt, action, params }`. Request approval expires after 15 minutes. Escrow `params.expiresAt` is a separate deadline expressed as integer Unix milliseconds. No sender, nonce, signature, recovery key or passphrase is included.
|
|
93
|
+
|
|
94
|
+
Local stdio saves an owner-only request file bound to the selected CLI wallet's public identity and returns the exact approval argument array. The person reviews it in their terminal:
|
|
95
|
+
|
|
96
|
+
```sh
|
|
97
|
+
ldgr-mcp approve <request-file>
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
For hosted MCP, save the returned `request` JSON as a local file, then run:
|
|
101
|
+
|
|
102
|
+
```sh
|
|
103
|
+
npx --yes --package @ldgr/mcp@latest ldgr-mcp approve <request-file>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The CLI validates the request and selected network, resolves fresh asset precision, checks the ledger's current fees and nonces, and presents the transaction for explicit local approval. Do not run approval unattended. Prepared requests may become invalid as balances, fees, configuration, or deadlines change. Reconcile an uncertain submission before trying again.
|
|
107
|
+
|
|
108
|
+
## Swaps
|
|
109
|
+
|
|
110
|
+
MCP 0.1.4 adds `prepare_swap`; approving these requests requires CLI 0.5.2 or
|
|
111
|
+
newer. First use `get_swap_quote` for a non-binding quote. Then prepare an intent
|
|
112
|
+
with explicit `network`, `assetIn`, `assetOut`, `amountIn`, and `amountOutMin`.
|
|
113
|
+
Both amounts are positive decimal strings in their respective asset's display
|
|
114
|
+
units. `maxHops` is optional, accepts 1, 2, or 3, and defaults to 3. Native aliases
|
|
115
|
+
`t0`/`t1` resolve to `dng` for the selected network. Identical assets and zero
|
|
116
|
+
minimum output are rejected.
|
|
117
|
+
|
|
118
|
+
Preparation does not select a route or execute a swap. During `approve`, the
|
|
119
|
+
CLI fetches a fresh route, checks the input balance and native fee balance, and
|
|
120
|
+
shows the route, quote, exact input and minimum received. Output returns to the
|
|
121
|
+
selected wallet. The reviewed route and minimum are signed unchanged; the CLI
|
|
122
|
+
never lowers the minimum to make a quote executable. Pool fees are included in
|
|
123
|
+
the quote, while one additional native network fee is charged. Route swaps use
|
|
124
|
+
the `dng` nonce even when swapping between two tokens. The portable request's
|
|
125
|
+
expiry governs local approval; the existing protocol timestamp rules govern
|
|
126
|
+
signed submission validity.
|
|
127
|
+
|
|
128
|
+
## Embedding
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
import { createLdgrMcpServer } from "@ldgr/mcp";
|
|
132
|
+
const server = createLdgrMcpServer();
|
|
133
|
+
await server.connect(yourTransport);
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
The root export is browser/Worker compatible and does not import filesystem, process, CLI or signing adapters. It accepts optional `fetch` and `now` injection and explicit local hooks for hosts. Node-only stdio is a separate export:
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
import { startLdgrMcpStdio } from "@ldgr/mcp/stdio";
|
|
140
|
+
await startLdgrMcpStdio({ walletPath: "/absolute/path/wallet.json" });
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Build with `bun run build`, run tests with `bun test`, and check types with `bun run typecheck` from this package.
|
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// src/cli.ts
|
|
4
|
-
import { spawn
|
|
5
|
-
import {
|
|
3
|
+
// src/latest-cli.ts
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { access, realpath } from "node:fs/promises";
|
|
6
|
+
import { delimiter, dirname, join } from "node:path";
|
|
6
7
|
|
|
7
8
|
// src/commands.ts
|
|
8
9
|
function walletCommandArgs(input) {
|
|
@@ -36,9 +37,94 @@ function walletCommandArgs(input) {
|
|
|
36
37
|
return [...input];
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
// src/latest-cli.ts
|
|
41
|
+
var FAILURE = "Latest LDGR CLI could not complete. Check npm connectivity and CLI availability.";
|
|
42
|
+
function npxArguments(envelope) {
|
|
43
|
+
return ["--yes", "--prefer-online", "--offline=false", "--package", "@ldgr/cli@latest", "--", "ldgr", "mcp", envelope];
|
|
44
|
+
}
|
|
45
|
+
async function resolveNpxEntrypoint() {
|
|
46
|
+
const directories = [dirname(process.execPath), ...(process.env.PATH ?? "").split(delimiter).filter(Boolean)];
|
|
47
|
+
const candidates = [];
|
|
48
|
+
const npmPath = process.env.npm_execpath;
|
|
49
|
+
if (npmPath && /[\\/]npm-cli\.js$/.test(npmPath))
|
|
50
|
+
candidates.push(join(dirname(npmPath), "npx-cli.js"));
|
|
51
|
+
for (const directory of directories) {
|
|
52
|
+
candidates.push(join(directory, "node_modules/npm/bin/npx-cli.js"), join(directory, "../lib/node_modules/npm/bin/npx-cli.js"));
|
|
53
|
+
try {
|
|
54
|
+
const target = await realpath(join(directory, "npx"));
|
|
55
|
+
if (target.endsWith("npx-cli.js"))
|
|
56
|
+
candidates.push(target);
|
|
57
|
+
} catch {}
|
|
58
|
+
}
|
|
59
|
+
for (const candidate of candidates) {
|
|
60
|
+
try {
|
|
61
|
+
await access(candidate);
|
|
62
|
+
return candidate;
|
|
63
|
+
} catch {}
|
|
64
|
+
}
|
|
65
|
+
throw new Error("npm/npx is required for the local LDGR CLI. Install Node.js with npm.");
|
|
66
|
+
}
|
|
67
|
+
var runNpx = async (args, options) => {
|
|
68
|
+
const entry = await resolveNpxEntrypoint();
|
|
69
|
+
return new Promise((resolve, reject) => {
|
|
70
|
+
const child = spawn(process.execPath, [entry, ...args], {
|
|
71
|
+
shell: false,
|
|
72
|
+
windowsHide: true,
|
|
73
|
+
stdio: options.interactive ? "inherit" : ["pipe", "pipe", "ignore"]
|
|
74
|
+
});
|
|
75
|
+
let stdout = "", bytes = 0;
|
|
76
|
+
child.once("error", () => reject(new Error(FAILURE)));
|
|
77
|
+
if (!options.interactive) {
|
|
78
|
+
child.stdout.setEncoding("utf8");
|
|
79
|
+
child.stdout.on("data", (chunk) => {
|
|
80
|
+
bytes += Buffer.byteLength(chunk);
|
|
81
|
+
if (bytes > 128 * 1024) {
|
|
82
|
+
child.kill();
|
|
83
|
+
reject(new Error(FAILURE));
|
|
84
|
+
} else
|
|
85
|
+
stdout += chunk;
|
|
86
|
+
});
|
|
87
|
+
child.stdin.on("error", () => reject(new Error(FAILURE)));
|
|
88
|
+
child.stdin.end(options.input);
|
|
89
|
+
}
|
|
90
|
+
child.once("close", (code) => resolve({ code: code ?? 1, stdout }));
|
|
91
|
+
});
|
|
92
|
+
};
|
|
93
|
+
function createLatestCliAdapter(run = runNpx) {
|
|
94
|
+
async function machine(envelope) {
|
|
95
|
+
const input = JSON.stringify(envelope);
|
|
96
|
+
if (Buffer.byteLength(input) > 128 * 1024)
|
|
97
|
+
throw new Error(FAILURE);
|
|
98
|
+
const result = await run(npxArguments("--stdin"), { interactive: false, input });
|
|
99
|
+
try {
|
|
100
|
+
const value = JSON.parse(result.stdout);
|
|
101
|
+
if (result.code !== 0 || !value || typeof value !== "object" || Array.isArray(value) || "error" in value)
|
|
102
|
+
throw new Error;
|
|
103
|
+
return value;
|
|
104
|
+
} catch {
|
|
105
|
+
throw new Error(FAILURE);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
walletInfo: (walletPath) => machine({ operation: "wallet-info", walletPath }),
|
|
110
|
+
async saveRequest(request, options) {
|
|
111
|
+
const value = await machine({ operation: "save-request", request, ...options });
|
|
112
|
+
if (typeof value.requestPath !== "string" || !Array.isArray(value.approvalArgs) || !value.approvalArgs.every((arg) => typeof arg === "string"))
|
|
113
|
+
throw new Error(FAILURE);
|
|
114
|
+
return { requestPath: value.requestPath, approvalArgs: walletCommandArgs(value.approvalArgs) };
|
|
115
|
+
},
|
|
116
|
+
async command(input) {
|
|
117
|
+
const args = walletCommandArgs(input);
|
|
118
|
+
const envelope = Buffer.from(JSON.stringify({ operation: "command", args })).toString("base64url");
|
|
119
|
+
if (envelope.length > 6000)
|
|
120
|
+
throw new Error("Wallet command arguments are too long");
|
|
121
|
+
return (await run(npxArguments(envelope), { interactive: true })).code;
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
39
126
|
// src/stdio.ts
|
|
40
127
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
41
|
-
import { getMcpWalletInfo, saveMcpRequest, validateMcpRequest } from "@ldgr/cli/mcp";
|
|
42
128
|
|
|
43
129
|
// src/index.ts
|
|
44
130
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -163,7 +249,7 @@ class AssetMismatchError extends Error {
|
|
|
163
249
|
class WalletNetworkMismatchError extends Error {
|
|
164
250
|
}
|
|
165
251
|
function createLdgrMcpServer(options = {}) {
|
|
166
|
-
const server = new McpServer({ name: "ldgr", version: "0.1.
|
|
252
|
+
const server = new McpServer({ name: "ldgr", version: "0.1.4" }, { instructions: INSTRUCTIONS });
|
|
167
253
|
const request = readTransport(options.fetch);
|
|
168
254
|
const now = options.now ?? Date.now;
|
|
169
255
|
const envelope = (network, source, data) => ({ network, source, data });
|
|
@@ -263,12 +349,27 @@ function createLdgrMcpServer(options = {}) {
|
|
|
263
349
|
const a = exactAsset(args.assetA, args.network), b = exactAsset(args.assetB, args.network), client = await checkedClient(args.network, request);
|
|
264
350
|
return envelope(args.network, "ledger", await client.amm.getPoolByPair(a, b));
|
|
265
351
|
});
|
|
266
|
-
tool("get_swap_quote", "Read a non-binding swap quote for explicit input/output assets.
|
|
352
|
+
tool("get_swap_quote", "Read a non-binding swap quote for explicit input/output assets. Use prepare_swap with an explicit positive minimum received to prepare an unsigned intent; only human CLI approval can execute it.", { network: networkSchema, assetIn: assetSchema, assetOut: assetSchema, amountIn: positiveDecimalSchema }, async (args) => {
|
|
267
353
|
const assetIn = exactAsset(args.assetIn, args.network), assetOut = exactAsset(args.assetOut, args.network);
|
|
268
354
|
checkNativeAmount(assetIn, args.amountIn, args.network);
|
|
269
355
|
const client = await checkedClient(args.network, request);
|
|
270
356
|
return envelope(args.network, "ledger", await client.amm.quoteRoute(assetIn, assetOut, args.amountIn, 3));
|
|
271
357
|
});
|
|
358
|
+
tool("prepare_swap", "Prepare an unsigned exact-input swap for human CLI approval. Specify both assets, amountIn and a positive amountOutMin in decimal display units. The CLI fetches a fresh route of up to maxHops pools and shows it before signing; it never lowers the minimum. Output goes to the selected wallet. Preparation does not quote, unlock, sign or submit.", {
|
|
359
|
+
network: networkSchema,
|
|
360
|
+
assetIn: assetSchema,
|
|
361
|
+
assetOut: assetSchema,
|
|
362
|
+
amountIn: positiveDecimalSchema,
|
|
363
|
+
amountOutMin: positiveDecimalSchema,
|
|
364
|
+
maxHops: z2.number().int().min(1).max(3).default(3)
|
|
365
|
+
}, async (args) => {
|
|
366
|
+
const assetIn = exactAsset(args.assetIn, args.network), assetOut = exactAsset(args.assetOut, args.network);
|
|
367
|
+
if (assetIn === assetOut)
|
|
368
|
+
throw new Error("Swap assets must differ");
|
|
369
|
+
checkNativeAmount(assetIn, args.amountIn, args.network);
|
|
370
|
+
checkNativeAmount(assetOut, args.amountOutMin, args.network);
|
|
371
|
+
return prepared(unsignedRequest(args.network, "swap", { assetIn, assetOut, amountIn: args.amountIn, amountOutMin: args.amountOutMin, maxHops: args.maxHops }, now()));
|
|
372
|
+
}, false);
|
|
272
373
|
tool("prepare_transfer", "Prepare an unsigned transfer for human review in the local CLI wallet. Does not unlock, sign or submit.", { network: networkSchema, to: addressSchema, asset: assetSchema, amount: positiveDecimalSchema, memo: referenceSchema.optional() }, async (args) => {
|
|
273
374
|
const to = addressForNetwork(args.to, args.network), asset = exactAsset(args.asset, args.network);
|
|
274
375
|
checkNativeAmount(asset, args.amount, args.network);
|
|
@@ -312,7 +413,7 @@ function createLdgrMcpServer(options = {}) {
|
|
|
312
413
|
}, false);
|
|
313
414
|
tool("wallet_setup_instructions", "Explain user-run CLI wallet installation, creation/import and MCP connection. This tool cannot install software or access local files.", { network: networkSchema }, async (args) => ({
|
|
314
415
|
network: args.network,
|
|
315
|
-
install: "npm install --global @ldgr/mcp@0.1.
|
|
416
|
+
install: "npm install --global @ldgr/mcp@0.1.4",
|
|
316
417
|
node: ">=20.19.0",
|
|
317
418
|
create: `ldgr-mcp wallet create${args.network === "testnet" ? " --testnet" : ""}`,
|
|
318
419
|
import: `ldgr-mcp wallet import${args.network === "testnet" ? " --testnet" : ""}`,
|
|
@@ -337,10 +438,10 @@ function createLdgrMcpServer(options = {}) {
|
|
|
337
438
|
}
|
|
338
439
|
|
|
339
440
|
// src/browser.ts
|
|
340
|
-
import { spawn } from "node:child_process";
|
|
441
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
341
442
|
import { win32 } from "node:path";
|
|
342
443
|
var launchChrome = (command, args, waitForExit) => new Promise((resolve, reject) => {
|
|
343
|
-
const child =
|
|
444
|
+
const child = spawn2(command, [...args], {
|
|
344
445
|
shell: false,
|
|
345
446
|
stdio: "ignore",
|
|
346
447
|
windowsHide: true,
|
|
@@ -376,13 +477,14 @@ async function openFaucetInBrowser(url, options = {}) {
|
|
|
376
477
|
|
|
377
478
|
// src/stdio.ts
|
|
378
479
|
async function startLdgrMcpStdio(options = {}) {
|
|
480
|
+
const cli = createLatestCliAdapter();
|
|
379
481
|
const server = createLdgrMcpServer({
|
|
380
482
|
fetch: options.fetch,
|
|
381
483
|
now: options.now,
|
|
382
484
|
local: {
|
|
383
485
|
openBrowser: openFaucetInBrowser,
|
|
384
|
-
walletInfo: () =>
|
|
385
|
-
saveRequest: (request) =>
|
|
486
|
+
walletInfo: () => cli.walletInfo(options.walletPath),
|
|
487
|
+
saveRequest: (request) => cli.saveRequest(request, { walletPath: options.walletPath, directory: options.requestDirectory })
|
|
386
488
|
}
|
|
387
489
|
});
|
|
388
490
|
await server.connect(new StdioServerTransport);
|
|
@@ -390,7 +492,7 @@ async function startLdgrMcpStdio(options = {}) {
|
|
|
390
492
|
}
|
|
391
493
|
|
|
392
494
|
// src/cli.ts
|
|
393
|
-
var HELP = `LDGR MCP 0.1.
|
|
495
|
+
var HELP = `LDGR MCP 0.1.4
|
|
394
496
|
|
|
395
497
|
ldgr-mcp Start local MCP over stdio
|
|
396
498
|
ldgr-mcp --wallet <path> Start MCP with a selected wallet store
|
|
@@ -414,13 +516,7 @@ try {
|
|
|
414
516
|
} else if (args.length === 0 || args.length === 2 && args[0] === "--wallet" && args[1] && !args[1].startsWith("-")) {
|
|
415
517
|
await startLdgrMcpStdio({ walletPath: args[1] });
|
|
416
518
|
} else {
|
|
417
|
-
|
|
418
|
-
const entry = fileURLToPath(import.meta.resolve("@ldgr/cli/cli"));
|
|
419
|
-
process.exitCode = await new Promise((resolve, reject) => {
|
|
420
|
-
const child = spawn2(process.execPath, [entry, ...forwarded], { stdio: "inherit", shell: false });
|
|
421
|
-
child.once("error", reject);
|
|
422
|
-
child.once("exit", (code) => resolve(code ?? 1));
|
|
423
|
-
});
|
|
519
|
+
process.exitCode = await createLatestCliAdapter().command(args);
|
|
424
520
|
}
|
|
425
521
|
} catch {
|
|
426
522
|
console.error("LDGR MCP could not complete. Check --help, the local wallet and request file. Recovery keys and passphrases belong only in local interactive prompts.");
|
package/dist/index.js
CHANGED
|
@@ -121,7 +121,7 @@ class AssetMismatchError extends Error {
|
|
|
121
121
|
class WalletNetworkMismatchError extends Error {
|
|
122
122
|
}
|
|
123
123
|
function createLdgrMcpServer(options = {}) {
|
|
124
|
-
const server = new McpServer({ name: "ldgr", version: "0.1.
|
|
124
|
+
const server = new McpServer({ name: "ldgr", version: "0.1.4" }, { instructions: INSTRUCTIONS });
|
|
125
125
|
const request = readTransport(options.fetch);
|
|
126
126
|
const now = options.now ?? Date.now;
|
|
127
127
|
const envelope = (network, source, data) => ({ network, source, data });
|
|
@@ -221,12 +221,27 @@ function createLdgrMcpServer(options = {}) {
|
|
|
221
221
|
const a = exactAsset(args.assetA, args.network), b = exactAsset(args.assetB, args.network), client = await checkedClient(args.network, request);
|
|
222
222
|
return envelope(args.network, "ledger", await client.amm.getPoolByPair(a, b));
|
|
223
223
|
});
|
|
224
|
-
tool("get_swap_quote", "Read a non-binding swap quote for explicit input/output assets.
|
|
224
|
+
tool("get_swap_quote", "Read a non-binding swap quote for explicit input/output assets. Use prepare_swap with an explicit positive minimum received to prepare an unsigned intent; only human CLI approval can execute it.", { network: networkSchema, assetIn: assetSchema, assetOut: assetSchema, amountIn: positiveDecimalSchema }, async (args) => {
|
|
225
225
|
const assetIn = exactAsset(args.assetIn, args.network), assetOut = exactAsset(args.assetOut, args.network);
|
|
226
226
|
checkNativeAmount(assetIn, args.amountIn, args.network);
|
|
227
227
|
const client = await checkedClient(args.network, request);
|
|
228
228
|
return envelope(args.network, "ledger", await client.amm.quoteRoute(assetIn, assetOut, args.amountIn, 3));
|
|
229
229
|
});
|
|
230
|
+
tool("prepare_swap", "Prepare an unsigned exact-input swap for human CLI approval. Specify both assets, amountIn and a positive amountOutMin in decimal display units. The CLI fetches a fresh route of up to maxHops pools and shows it before signing; it never lowers the minimum. Output goes to the selected wallet. Preparation does not quote, unlock, sign or submit.", {
|
|
231
|
+
network: networkSchema,
|
|
232
|
+
assetIn: assetSchema,
|
|
233
|
+
assetOut: assetSchema,
|
|
234
|
+
amountIn: positiveDecimalSchema,
|
|
235
|
+
amountOutMin: positiveDecimalSchema,
|
|
236
|
+
maxHops: z2.number().int().min(1).max(3).default(3)
|
|
237
|
+
}, async (args) => {
|
|
238
|
+
const assetIn = exactAsset(args.assetIn, args.network), assetOut = exactAsset(args.assetOut, args.network);
|
|
239
|
+
if (assetIn === assetOut)
|
|
240
|
+
throw new Error("Swap assets must differ");
|
|
241
|
+
checkNativeAmount(assetIn, args.amountIn, args.network);
|
|
242
|
+
checkNativeAmount(assetOut, args.amountOutMin, args.network);
|
|
243
|
+
return prepared(unsignedRequest(args.network, "swap", { assetIn, assetOut, amountIn: args.amountIn, amountOutMin: args.amountOutMin, maxHops: args.maxHops }, now()));
|
|
244
|
+
}, false);
|
|
230
245
|
tool("prepare_transfer", "Prepare an unsigned transfer for human review in the local CLI wallet. Does not unlock, sign or submit.", { network: networkSchema, to: addressSchema, asset: assetSchema, amount: positiveDecimalSchema, memo: referenceSchema.optional() }, async (args) => {
|
|
231
246
|
const to = addressForNetwork(args.to, args.network), asset = exactAsset(args.asset, args.network);
|
|
232
247
|
checkNativeAmount(asset, args.amount, args.network);
|
|
@@ -270,7 +285,7 @@ function createLdgrMcpServer(options = {}) {
|
|
|
270
285
|
}, false);
|
|
271
286
|
tool("wallet_setup_instructions", "Explain user-run CLI wallet installation, creation/import and MCP connection. This tool cannot install software or access local files.", { network: networkSchema }, async (args) => ({
|
|
272
287
|
network: args.network,
|
|
273
|
-
install: "npm install --global @ldgr/mcp@0.1.
|
|
288
|
+
install: "npm install --global @ldgr/mcp@0.1.4",
|
|
274
289
|
node: ">=20.19.0",
|
|
275
290
|
create: `ldgr-mcp wallet create${args.network === "testnet" ? " --testnet" : ""}`,
|
|
276
291
|
import: `ldgr-mcp wallet import${args.network === "testnet" ? " --testnet" : ""}`,
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { UnsignedMcpRequest } from "./requests";
|
|
2
|
+
type RunOptions = {
|
|
3
|
+
readonly interactive: boolean;
|
|
4
|
+
readonly input?: string;
|
|
5
|
+
};
|
|
6
|
+
type Runner = (args: readonly string[], options: RunOptions) => Promise<{
|
|
7
|
+
code: number;
|
|
8
|
+
stdout: string;
|
|
9
|
+
}>;
|
|
10
|
+
/** Only fixed arguments or a base64url envelope reach npm's internal command shell. */
|
|
11
|
+
export declare function npxArguments(envelope: string): string[];
|
|
12
|
+
/** Execute npm's JS entry directly: Windows .cmd launchers require a shell. */
|
|
13
|
+
export declare function resolveNpxEntrypoint(): Promise<string>;
|
|
14
|
+
export declare function createLatestCliAdapter(run?: Runner): {
|
|
15
|
+
walletInfo: (walletPath?: string) => Promise<unknown>;
|
|
16
|
+
saveRequest(request: UnsignedMcpRequest, options: {
|
|
17
|
+
walletPath?: string;
|
|
18
|
+
directory?: string;
|
|
19
|
+
}): Promise<{
|
|
20
|
+
requestPath: string;
|
|
21
|
+
approvalArgs: string[];
|
|
22
|
+
}>;
|
|
23
|
+
command(input: readonly string[]): Promise<number>;
|
|
24
|
+
};
|
|
25
|
+
export {};
|
package/dist/requests.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export type UnsignedMcpRequest = {
|
|
|
5
5
|
readonly requestId: string;
|
|
6
6
|
readonly network: McpNetwork;
|
|
7
7
|
readonly expiresAt: string;
|
|
8
|
-
readonly action: "transfer" | "create_token" | "open_escrow";
|
|
8
|
+
readonly action: "transfer" | "create_token" | "open_escrow" | "swap";
|
|
9
9
|
readonly params: Readonly<Record<string, string | number | undefined>>;
|
|
10
10
|
};
|
|
11
11
|
export declare const networkSchema: z.ZodEnum<{
|
package/dist/stdio.js
CHANGED
|
@@ -1,6 +1,128 @@
|
|
|
1
1
|
// src/stdio.ts
|
|
2
2
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
// src/latest-cli.ts
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
import { access, realpath } from "node:fs/promises";
|
|
7
|
+
import { delimiter, dirname, join } from "node:path";
|
|
8
|
+
|
|
9
|
+
// src/commands.ts
|
|
10
|
+
function walletCommandArgs(input) {
|
|
11
|
+
if (input.some((value) => !value || /[\x00-\x1f\x7f]/.test(value)))
|
|
12
|
+
throw new TypeError("Invalid wallet arguments");
|
|
13
|
+
const positional = [];
|
|
14
|
+
let testnet = false, wallet = false;
|
|
15
|
+
for (let index = 0;index < input.length; index++) {
|
|
16
|
+
const value = input[index];
|
|
17
|
+
if (value === "--wallet") {
|
|
18
|
+
if (wallet || !input[index + 1] || input[index + 1].startsWith("-"))
|
|
19
|
+
throw new TypeError("Invalid wallet path");
|
|
20
|
+
wallet = true;
|
|
21
|
+
index++;
|
|
22
|
+
} else if (value === "--testnet") {
|
|
23
|
+
if (testnet)
|
|
24
|
+
throw new TypeError("Repeated network flag");
|
|
25
|
+
testnet = true;
|
|
26
|
+
} else if (value.startsWith("-"))
|
|
27
|
+
throw new TypeError("Unsupported wallet option");
|
|
28
|
+
else
|
|
29
|
+
positional.push(value);
|
|
30
|
+
}
|
|
31
|
+
const [first, second, third] = positional;
|
|
32
|
+
const creation = first === "wallet" && (second === "create" || second === "import");
|
|
33
|
+
if (testnet && !creation)
|
|
34
|
+
throw new TypeError("--testnet only applies to wallet create/import");
|
|
35
|
+
const valid = first === "wallet" && second === "create" && positional.length === 2 || first === "wallet" && second === "import" && (positional.length === 2 || positional.length === 3) || first === "wallet" && second === "list" && positional.length === 2 || first === "wallet" && second === "select" && positional.length === 3 && /^[0-9]+$/.test(third) || first === "approve" && positional.length === 2 || first === "address" && positional.length === 1 || first === "network" && second === "show" && positional.length === 2 || first === "network" && second === "set" && positional.length === 3 && (third === "mainnet" || third === "testnet");
|
|
36
|
+
if (!valid)
|
|
37
|
+
throw new TypeError("Unsupported wallet command");
|
|
38
|
+
return [...input];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// src/latest-cli.ts
|
|
42
|
+
var FAILURE = "Latest LDGR CLI could not complete. Check npm connectivity and CLI availability.";
|
|
43
|
+
function npxArguments(envelope) {
|
|
44
|
+
return ["--yes", "--prefer-online", "--offline=false", "--package", "@ldgr/cli@latest", "--", "ldgr", "mcp", envelope];
|
|
45
|
+
}
|
|
46
|
+
async function resolveNpxEntrypoint() {
|
|
47
|
+
const directories = [dirname(process.execPath), ...(process.env.PATH ?? "").split(delimiter).filter(Boolean)];
|
|
48
|
+
const candidates = [];
|
|
49
|
+
const npmPath = process.env.npm_execpath;
|
|
50
|
+
if (npmPath && /[\\/]npm-cli\.js$/.test(npmPath))
|
|
51
|
+
candidates.push(join(dirname(npmPath), "npx-cli.js"));
|
|
52
|
+
for (const directory of directories) {
|
|
53
|
+
candidates.push(join(directory, "node_modules/npm/bin/npx-cli.js"), join(directory, "../lib/node_modules/npm/bin/npx-cli.js"));
|
|
54
|
+
try {
|
|
55
|
+
const target = await realpath(join(directory, "npx"));
|
|
56
|
+
if (target.endsWith("npx-cli.js"))
|
|
57
|
+
candidates.push(target);
|
|
58
|
+
} catch {}
|
|
59
|
+
}
|
|
60
|
+
for (const candidate of candidates) {
|
|
61
|
+
try {
|
|
62
|
+
await access(candidate);
|
|
63
|
+
return candidate;
|
|
64
|
+
} catch {}
|
|
65
|
+
}
|
|
66
|
+
throw new Error("npm/npx is required for the local LDGR CLI. Install Node.js with npm.");
|
|
67
|
+
}
|
|
68
|
+
var runNpx = async (args, options) => {
|
|
69
|
+
const entry = await resolveNpxEntrypoint();
|
|
70
|
+
return new Promise((resolve, reject) => {
|
|
71
|
+
const child = spawn(process.execPath, [entry, ...args], {
|
|
72
|
+
shell: false,
|
|
73
|
+
windowsHide: true,
|
|
74
|
+
stdio: options.interactive ? "inherit" : ["pipe", "pipe", "ignore"]
|
|
75
|
+
});
|
|
76
|
+
let stdout = "", bytes = 0;
|
|
77
|
+
child.once("error", () => reject(new Error(FAILURE)));
|
|
78
|
+
if (!options.interactive) {
|
|
79
|
+
child.stdout.setEncoding("utf8");
|
|
80
|
+
child.stdout.on("data", (chunk) => {
|
|
81
|
+
bytes += Buffer.byteLength(chunk);
|
|
82
|
+
if (bytes > 128 * 1024) {
|
|
83
|
+
child.kill();
|
|
84
|
+
reject(new Error(FAILURE));
|
|
85
|
+
} else
|
|
86
|
+
stdout += chunk;
|
|
87
|
+
});
|
|
88
|
+
child.stdin.on("error", () => reject(new Error(FAILURE)));
|
|
89
|
+
child.stdin.end(options.input);
|
|
90
|
+
}
|
|
91
|
+
child.once("close", (code) => resolve({ code: code ?? 1, stdout }));
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
function createLatestCliAdapter(run = runNpx) {
|
|
95
|
+
async function machine(envelope) {
|
|
96
|
+
const input = JSON.stringify(envelope);
|
|
97
|
+
if (Buffer.byteLength(input) > 128 * 1024)
|
|
98
|
+
throw new Error(FAILURE);
|
|
99
|
+
const result = await run(npxArguments("--stdin"), { interactive: false, input });
|
|
100
|
+
try {
|
|
101
|
+
const value = JSON.parse(result.stdout);
|
|
102
|
+
if (result.code !== 0 || !value || typeof value !== "object" || Array.isArray(value) || "error" in value)
|
|
103
|
+
throw new Error;
|
|
104
|
+
return value;
|
|
105
|
+
} catch {
|
|
106
|
+
throw new Error(FAILURE);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
walletInfo: (walletPath) => machine({ operation: "wallet-info", walletPath }),
|
|
111
|
+
async saveRequest(request, options) {
|
|
112
|
+
const value = await machine({ operation: "save-request", request, ...options });
|
|
113
|
+
if (typeof value.requestPath !== "string" || !Array.isArray(value.approvalArgs) || !value.approvalArgs.every((arg) => typeof arg === "string"))
|
|
114
|
+
throw new Error(FAILURE);
|
|
115
|
+
return { requestPath: value.requestPath, approvalArgs: walletCommandArgs(value.approvalArgs) };
|
|
116
|
+
},
|
|
117
|
+
async command(input) {
|
|
118
|
+
const args = walletCommandArgs(input);
|
|
119
|
+
const envelope = Buffer.from(JSON.stringify({ operation: "command", args })).toString("base64url");
|
|
120
|
+
if (envelope.length > 6000)
|
|
121
|
+
throw new Error("Wallet command arguments are too long");
|
|
122
|
+
return (await run(npxArguments(envelope), { interactive: true })).code;
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
}
|
|
4
126
|
|
|
5
127
|
// src/index.ts
|
|
6
128
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -125,7 +247,7 @@ class AssetMismatchError extends Error {
|
|
|
125
247
|
class WalletNetworkMismatchError extends Error {
|
|
126
248
|
}
|
|
127
249
|
function createLdgrMcpServer(options = {}) {
|
|
128
|
-
const server = new McpServer({ name: "ldgr", version: "0.1.
|
|
250
|
+
const server = new McpServer({ name: "ldgr", version: "0.1.4" }, { instructions: INSTRUCTIONS });
|
|
129
251
|
const request = readTransport(options.fetch);
|
|
130
252
|
const now = options.now ?? Date.now;
|
|
131
253
|
const envelope = (network, source, data) => ({ network, source, data });
|
|
@@ -225,12 +347,27 @@ function createLdgrMcpServer(options = {}) {
|
|
|
225
347
|
const a = exactAsset(args.assetA, args.network), b = exactAsset(args.assetB, args.network), client = await checkedClient(args.network, request);
|
|
226
348
|
return envelope(args.network, "ledger", await client.amm.getPoolByPair(a, b));
|
|
227
349
|
});
|
|
228
|
-
tool("get_swap_quote", "Read a non-binding swap quote for explicit input/output assets.
|
|
350
|
+
tool("get_swap_quote", "Read a non-binding swap quote for explicit input/output assets. Use prepare_swap with an explicit positive minimum received to prepare an unsigned intent; only human CLI approval can execute it.", { network: networkSchema, assetIn: assetSchema, assetOut: assetSchema, amountIn: positiveDecimalSchema }, async (args) => {
|
|
229
351
|
const assetIn = exactAsset(args.assetIn, args.network), assetOut = exactAsset(args.assetOut, args.network);
|
|
230
352
|
checkNativeAmount(assetIn, args.amountIn, args.network);
|
|
231
353
|
const client = await checkedClient(args.network, request);
|
|
232
354
|
return envelope(args.network, "ledger", await client.amm.quoteRoute(assetIn, assetOut, args.amountIn, 3));
|
|
233
355
|
});
|
|
356
|
+
tool("prepare_swap", "Prepare an unsigned exact-input swap for human CLI approval. Specify both assets, amountIn and a positive amountOutMin in decimal display units. The CLI fetches a fresh route of up to maxHops pools and shows it before signing; it never lowers the minimum. Output goes to the selected wallet. Preparation does not quote, unlock, sign or submit.", {
|
|
357
|
+
network: networkSchema,
|
|
358
|
+
assetIn: assetSchema,
|
|
359
|
+
assetOut: assetSchema,
|
|
360
|
+
amountIn: positiveDecimalSchema,
|
|
361
|
+
amountOutMin: positiveDecimalSchema,
|
|
362
|
+
maxHops: z2.number().int().min(1).max(3).default(3)
|
|
363
|
+
}, async (args) => {
|
|
364
|
+
const assetIn = exactAsset(args.assetIn, args.network), assetOut = exactAsset(args.assetOut, args.network);
|
|
365
|
+
if (assetIn === assetOut)
|
|
366
|
+
throw new Error("Swap assets must differ");
|
|
367
|
+
checkNativeAmount(assetIn, args.amountIn, args.network);
|
|
368
|
+
checkNativeAmount(assetOut, args.amountOutMin, args.network);
|
|
369
|
+
return prepared(unsignedRequest(args.network, "swap", { assetIn, assetOut, amountIn: args.amountIn, amountOutMin: args.amountOutMin, maxHops: args.maxHops }, now()));
|
|
370
|
+
}, false);
|
|
234
371
|
tool("prepare_transfer", "Prepare an unsigned transfer for human review in the local CLI wallet. Does not unlock, sign or submit.", { network: networkSchema, to: addressSchema, asset: assetSchema, amount: positiveDecimalSchema, memo: referenceSchema.optional() }, async (args) => {
|
|
235
372
|
const to = addressForNetwork(args.to, args.network), asset = exactAsset(args.asset, args.network);
|
|
236
373
|
checkNativeAmount(asset, args.amount, args.network);
|
|
@@ -274,7 +411,7 @@ function createLdgrMcpServer(options = {}) {
|
|
|
274
411
|
}, false);
|
|
275
412
|
tool("wallet_setup_instructions", "Explain user-run CLI wallet installation, creation/import and MCP connection. This tool cannot install software or access local files.", { network: networkSchema }, async (args) => ({
|
|
276
413
|
network: args.network,
|
|
277
|
-
install: "npm install --global @ldgr/mcp@0.1.
|
|
414
|
+
install: "npm install --global @ldgr/mcp@0.1.4",
|
|
278
415
|
node: ">=20.19.0",
|
|
279
416
|
create: `ldgr-mcp wallet create${args.network === "testnet" ? " --testnet" : ""}`,
|
|
280
417
|
import: `ldgr-mcp wallet import${args.network === "testnet" ? " --testnet" : ""}`,
|
|
@@ -299,10 +436,10 @@ function createLdgrMcpServer(options = {}) {
|
|
|
299
436
|
}
|
|
300
437
|
|
|
301
438
|
// src/browser.ts
|
|
302
|
-
import { spawn } from "node:child_process";
|
|
439
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
303
440
|
import { win32 } from "node:path";
|
|
304
441
|
var launchChrome = (command, args, waitForExit) => new Promise((resolve, reject) => {
|
|
305
|
-
const child =
|
|
442
|
+
const child = spawn2(command, [...args], {
|
|
306
443
|
shell: false,
|
|
307
444
|
stdio: "ignore",
|
|
308
445
|
windowsHide: true,
|
|
@@ -338,13 +475,14 @@ async function openFaucetInBrowser(url, options = {}) {
|
|
|
338
475
|
|
|
339
476
|
// src/stdio.ts
|
|
340
477
|
async function startLdgrMcpStdio(options = {}) {
|
|
478
|
+
const cli = createLatestCliAdapter();
|
|
341
479
|
const server = createLdgrMcpServer({
|
|
342
480
|
fetch: options.fetch,
|
|
343
481
|
now: options.now,
|
|
344
482
|
local: {
|
|
345
483
|
openBrowser: openFaucetInBrowser,
|
|
346
|
-
walletInfo: () =>
|
|
347
|
-
saveRequest: (request) =>
|
|
484
|
+
walletInfo: () => cli.walletInfo(options.walletPath),
|
|
485
|
+
saveRequest: (request) => cli.saveRequest(request, { walletPath: options.walletPath, directory: options.requestDirectory })
|
|
348
486
|
}
|
|
349
487
|
});
|
|
350
488
|
await server.connect(new StdioServerTransport);
|
package/package.json
CHANGED
|
@@ -1,29 +1,28 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@ldgr/mcp",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"type": "module",
|
|
5
|
-
"types": "./dist/index.d.ts",
|
|
6
|
-
"exports": {
|
|
7
|
-
".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" },
|
|
8
|
-
"./stdio": { "types": "./dist/stdio.d.ts", "import": "./dist/stdio.js" }
|
|
9
|
-
},
|
|
10
|
-
"bin": { "ldgr-mcp": "dist/cli.js" },
|
|
11
|
-
"files": ["dist", "README.md"],
|
|
12
|
-
"publishConfig": { "access": "public" },
|
|
13
|
-
"engines": { "node": ">=20.19.0" },
|
|
14
|
-
"dependencies": {
|
|
15
|
-
"@ldgr/client": "^0.6.1",
|
|
16
|
-
"@ldgr/
|
|
17
|
-
"@
|
|
18
|
-
"
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@ldgr/mcp",
|
|
3
|
+
"version": "0.1.4",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"types": "./dist/index.d.ts",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" },
|
|
8
|
+
"./stdio": { "types": "./dist/stdio.d.ts", "import": "./dist/stdio.js" }
|
|
9
|
+
},
|
|
10
|
+
"bin": { "ldgr-mcp": "dist/cli.js" },
|
|
11
|
+
"files": ["dist", "README.md"],
|
|
12
|
+
"publishConfig": { "access": "public" },
|
|
13
|
+
"engines": { "node": ">=20.19.0" },
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@ldgr/client": "^0.6.1",
|
|
16
|
+
"@ldgr/protocol": "^0.5.0",
|
|
17
|
+
"@modelcontextprotocol/sdk": "1.32.2",
|
|
18
|
+
"zod": "^4.3.6"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "bun build src/index.ts src/stdio.ts src/cli.ts --outdir dist --target node --format esm --packages external && tsrs -p tsconfig.build.json",
|
|
22
|
+
"prepack": "bun run build",
|
|
23
|
+
"start": "bun src/cli.ts",
|
|
24
|
+
"test": "bun test",
|
|
25
|
+
"typecheck": "tsrs --noEmit -p tsconfig.json && tsrs --noEmit -p test/tsconfig.json"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": { "tsrs": "0.20.0-ts7.1.0-beta" }
|
|
28
|
+
}
|