# Directory Structure
```
├── .github
│ └── workflows
│ ├── ci.yml
│ ├── grpc.yml
│ └── release.yml
├── .gitignore
├── assets
│ └── mcp-screenshot.png
├── Cargo.toml
├── LICENSE
├── README.md
├── src
│ ├── client
│ │ ├── config.rs
│ │ ├── mod.rs
│ │ └── node.rs
│ ├── docs
│ │ ├── bkpr-channelsapy.md
│ │ ├── bkpr-listaccountevents.md
│ │ ├── bkpr-listbalances.md
│ │ ├── bkpr-listincome.md
│ │ ├── checkmessage.md
│ │ ├── decode.md
│ │ ├── decodepay.md
│ │ ├── feerates.md
│ │ ├── getinfo.md
│ │ ├── getlog.md
│ │ ├── getroute.md
│ │ ├── listaddresses.md
│ │ ├── listchannels.md
│ │ ├── listclosedchannels.md
│ │ ├── listconfigs.md
│ │ ├── listdatastore.md
│ │ ├── listforwards.md
│ │ ├── listfunds.md
│ │ ├── listhtlcs.md
│ │ ├── listinvoicerequests.md
│ │ ├── listinvoices.md
│ │ ├── listnodes.md
│ │ ├── listoffers.md
│ │ ├── listpays.md
│ │ ├── listpeerchannels.md
│ │ ├── listpeers.md
│ │ └── listsendpays.md
│ ├── error.rs
│ ├── lib.rs
│ ├── main.rs
│ └── utils
│ ├── mod.rs
│ └── tls.rs
└── tests
└── integration_test.rs
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
/target/
cargo.lock
/certs/
# Release artifacts
*.tar.gz
*.zip
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# Core Lightning MCP Server
<div align="center">
[](https://www.rust-lang.org)
[](LICENSE)

[](https://github.com/adi2011/cln-mcp/actions/workflows/ci.yml)
[](https://github.com/adi2011/cln-mcp/actions/workflows/grpc.yml)
</div>
A Rust-based gRPC server that provides a standardized interface to Core Lightning nodes. This server implements the MCP (Model Context Protocol) specification to enable control of the Core Lightning node using LLM.

## Installation
### Option 1: From Release (Recommended)
1. Download the appropriate binary for your platform from the [latest release](https://github.com/adi2011/cln-mcp/releases/latest)
2. Extract the archive:
```bash
# For Linux/macOS
tar -xzf cln-mcp-<platform>.tar.gz
# For Windows
# Use your preferred zip extractor
```
3. Make the binary executable (Linux/macOS only):
```bash
chmod +x cln-mcp
```
### Option 2: From Source
## Prerequisites
- Rust 1.80 or higher
- Protocol Buffers Compiler (protoc)
- Core Lightning (with gRPC enabled)
- MCP clients ([Claude](https://claude.ai/download), [Goose](https://github.com/block/goose), etc.)
#### Protocol Buffers Compiler (protoc)
**Ubuntu/Debian:**
```bash
sudo apt-get update
sudo apt-get install -y protobuf-compiler
```
**macOS:**
```bash
brew install protobuf
```
**Windows:**
```bash
choco install protoc
```
**Verify installation:**
```bash
protoc --version # Should show version 3.0.0 or higher
```
1. Clone the repository:
```bash
git clone https://github.com/adi2011/cln-mcp.git
cd cln-mcp
```
2. Build the project:
```bash
cargo build --release
```
## Configuration
The server can be configured using command-line arguments:
```bash
cln-mcp [OPTIONS]
Options:
--certs-dir <path> Path to certificates directory
--node-address <url> Node address (default: https://localhost:9736)
--help Shows help message
```
### TLS Certificate Setup
Add the `--grpc-port`(default: 9736) option while running CLN, and it'll automatically generate the appropriate mTLS certificates.
Copy the following PEM files from the Lightning directory to a separate directory:
- `ca.pem`: CA certificate
- `client.pem`: Client certificate
- `client-key.pem`: Client private key
### Claude Setup
- Install [Claude](https://claude.ai/download)
- Go to settings -> Developer
- Edit Config
```
{
"mcpServers" : {
"cln-mcp" : {
"command": "Path/to/cln-mcp" (ex: "/Users/MyPC/cln-mcp/target/release/cln-mcp" or the executable unzipped from the release),
"args": [
"--certs-dir",
"Path/to/certificates" (ex: "/Users/MyPC/cln-mcp/certs")
]
}
}
}
```
- Restart Claude
# Future Goals
[ ] Enable it to derive parameters for the RPC calls
[ ] Choose the most appropriate and useful RPCs for maximum utility
[ ] Extend support for LND
[ ] Host multiple servers to make it more efficient
This is a work in progress. We welcome code reviews, pull requests, and issues based on your usage.
```
--------------------------------------------------------------------------------
/src/utils/mod.rs:
--------------------------------------------------------------------------------
```rust
pub mod tls;
```
--------------------------------------------------------------------------------
/src/client/mod.rs:
--------------------------------------------------------------------------------
```rust
pub mod config;
pub mod node;
```
--------------------------------------------------------------------------------
/src/lib.rs:
--------------------------------------------------------------------------------
```rust
pub mod client;
pub mod error;
pub mod utils;
pub use client::config::{create_channel, ClientConfig};
pub use client::node::NodeService;
pub use utils::tls::load_tls_config;
```
--------------------------------------------------------------------------------
/src/docs/listconfigs.md:
--------------------------------------------------------------------------------
```markdown
listconfigs -- Command to list all configuration options.
**listconfigs** [*config*]
The **listconfigs** RPC command to list all configuration options, or with *config* only one.
- **config** (string, optional): Configuration option name to restrict return.
```
--------------------------------------------------------------------------------
/src/docs/getlog.md:
--------------------------------------------------------------------------------
```markdown
lightning-getlog -- Command to show logs.
=========================================
The **getlog** the RPC command to show logs, with optional log *level*.
- **level** (string, optional) (one of "broken", "unusual", "info", "debug", "trace", "io"): A string that represents the log level. The default is *info*.
```
--------------------------------------------------------------------------------
/src/docs/listnodes.md:
--------------------------------------------------------------------------------
```markdown
lightning-listnodes -- Command to get the list of nodes in the known network.
**listnodes** [*id*]
DESCRIPTION
The **listnodes** command returns nodes the node has learned about via gossip messages, or a single one if the node *id* was specified.
- **id** (pubkey, optional): The public key of the node to list.
```
--------------------------------------------------------------------------------
/src/docs/listhtlcs.md:
--------------------------------------------------------------------------------
```markdown
lightning-listhtlcs -- Command for querying HTLCs
**listhtlcs** [*id*]
The **listhtlcs** RPC command gets all HTLCs (which, generally, we remember for as long as a channel is open, even if they've completed long ago).
- **id** (string, optional): A short channel id (e.g. 1x2x3) or full 64-byte hex channel id, it will only list htlcs for that channel (which must be known).
```
--------------------------------------------------------------------------------
/src/error.rs:
--------------------------------------------------------------------------------
```rust
use thiserror::Error;
#[derive(Error, Debug)]
pub enum CertError {
#[error("Failed to read certificate file: {0}")]
IoError(#[from] std::io::Error),
#[error("Missing certificate file: {0}")]
MissingCert(String),
#[error("TLS configuration error: {0}")]
TlsError(#[from] tonic::transport::Error),
}
pub type Result<T> = std::result::Result<T, CertError>;
```
--------------------------------------------------------------------------------
/src/docs/listoffers.md:
--------------------------------------------------------------------------------
```markdown
lightning-listoffers -- Command for listing offers
**listoffers** [*offer\_id*] [*active\_only*]
The **listoffers** RPC command list all offers, or with `offer_id`, only the offer with that offer\_id (if it exists).
- **offer\_id** (hash, optional): Offer\_id to get details for (if it exists).
- **active\_only** (boolean, optional): If set and is true, only offers with `active` true are returned.
```
--------------------------------------------------------------------------------
/src/docs/listfunds.md:
--------------------------------------------------------------------------------
```markdown
lightning-listfunds -- Command showing all funds currently managed by the Core Lightning node
**listfunds** [*spent*]
The **listfunds** RPC command displays all funds available, either in unspent outputs (UTXOs) in the internal wallet or funds locked in currently open channels.
- **spent** (boolean, optional): If True, then the *outputs* will include spent outputs in addition to the unspent ones. The default is False.
```
--------------------------------------------------------------------------------
/src/docs/listclosedchannels.md:
--------------------------------------------------------------------------------
```markdown
lightning-listclosedchannels -- Get data on our closed historical channels
**listclosedchannels** [*id*]
Command *added* in v23.05.
The **listclosedchannels** RPC command returns data on channels which are otherwise forgotten (more than 100 blocks after they're completely resolved onchain).
- **id** (pubkey, optional): If no *id* is supplied, then channel data on all historical channels are given. Supplying *id* will filter the results to only match channels to that peer. Note that prior to v23.05, old peers were forgotten.
```
--------------------------------------------------------------------------------
/src/docs/listpeerchannels.md:
--------------------------------------------------------------------------------
```markdown
lightning-listpeerchannels -- Command returning data on channels of connected lightning nodes
**listpeerchannels** [*id*]
Command *added* in v23.02.
The **listpeerchannels** RPC command returns list of this node's channels, with the possibility to filter them by peer's node id.
If no *id* is supplied, then channel data on all lightning nodes that are connected, or not connected but have open channels with this node, are returned.
- **id** (pubkey, optional): If supplied, limits the channels to just the peer with the given ID, if it exists.
```
--------------------------------------------------------------------------------
/src/docs/listdatastore.md:
--------------------------------------------------------------------------------
```markdown
lightning-listdatastore -- Command for listing (plugin) data
**listdatastore** [*key*]
The **listdatastore** RPC command allows plugins to fetch data which was stored in the Core Lightning database.
- **key** (one of, optional):
- (array of strings): All immediate children of the *key* (or root children) are returned.
Using the first element of the key as the plugin name (e.g. `[ 'summary' ]`) is recommended.
An array of values to form a hierarchy (though a single value is treated as a one-element array).
- (string, optional)
- (string)
```
--------------------------------------------------------------------------------
/src/docs/getinfo.md:
--------------------------------------------------------------------------------
```markdown
getinfo -- Command to receive all information about the Core Lightning node.
The **getinfo** gives a summary of the current running node.
Returns node information including:
- Identity: id (pubkey), alias (string), color (hex)
- Channel stats: num_peers, num_pending_channels, num_active_channels, num_inactive_channels
- Node info: version, lightning-dir, blockheight, network, fees_collected_msat
- Network: address (announced addresses), binding (listening addresses)
- Features: our_features (init, node, channel, invoice)
Warnings:
- warning_bitcoind_sync: When bitcoind is not up-to-date
- warning_lightningd_sync: When still loading blocks
```
--------------------------------------------------------------------------------
/src/docs/listinvoicerequests.md:
--------------------------------------------------------------------------------
```markdown
lightning-listinvoicerequests -- Command for querying invoice\_request status
**listinvoicerequests** [*invreq\_id*] [*active\_only*]
Command *added* in v22.11.
The **listinvoicerequests** RPC command gets the status of a specific `invoice_request`, if it exists, or the status of all `invoice_requests` if given no argument.
- **invreq\_id** (string, optional): A specific invoice can be queried by providing the `invreq_id`, which is presented by lightning-invoicerequest(7), or can be calculated from a bolt12 invoice.
- **active\_only** (boolean, optional): If it is *True* then only active invoice requests are returned. The default is *False*.
```
--------------------------------------------------------------------------------
/src/docs/listaddresses.md:
--------------------------------------------------------------------------------
```markdown
lightning-listaddresses -- Command to list all addresses issued by the node to date
===================================================================================
**listaddresses** [*address*] [*start*] [*limit*]
Command *added* in v24.11.
The **listaddresses** RPC command provides a detailed list of all Bitcoin addresses that have been generated and issued by the Core Lightning node up to the current date.
- **address** (string, optional): A Bitcoin accepted type, including a bech32, address for lookup in the list of addresses issued to date.
- **start** (u64, optional): Starting key index for listing addresses or searching for a particular address. The default is 1.
- **limit** (u32, optional): The maximum number of addresses to return or search for. The default is Total number of addresses issued.
```
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
```toml
[package]
name = "cln-mcp"
version = "0.1.0"
edition = "2021"
authors = ["Aditya Sharma <[email protected]>"]
description = "A Rust-based MCP server for Core Lightning nodes"
license = "MIT"
repository = "https://github.com/adi2011/cln-mcp"
[dependencies]
rmcp = { version = "0.1", features = ["transport-io", "server"] }
tokio = { version = "1.43.0", features = ["full"] }
serde_json = "1.0.139"
serde = { version = "1.0.218", features = ["derive"] }
cln-grpc = "0.4.0"
anyhow = "1.0.98"
tonic = { version = "0.11", features = ["tls"] }
prost = "0.11" # ✅ Needed by tonic 0.9
http = "0.2" # ✅ Matches tonic 0.9
thiserror = "2.0.12"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
[[test]]
name = "integration_test"
path = "tests/integration_test.rs"
```
--------------------------------------------------------------------------------
/src/docs/decode.md:
--------------------------------------------------------------------------------
```markdown
lightning-decode -- Command for decoding an invoice string (low-level)
======================================================================
SYNOPSIS
--------
**decode** *string*
DESCRIPTION
-----------
Command *added* in v23.05.
The **decode** RPC command checks and parses `bolt11`, `bolt12`, `rune` or `emergency_recover`. It may decode other formats in future.
- **string** (string): Value to be decoded:
* a *bolt11* or *bolt12* string (optionally prefixed by `lightning:` or `LIGHTNING:`) as specified by the BOLT 11 and BOLT 12 specifications.
* a *rune* as created by lightning-commando-rune(7).
* an *emergency\_recover* string generated by hsmtool like `lightning-hsmtool getemergencyrecover <path/to/emergency.recover>`. It holds `emergency.recover` contents and starts with `clnemerg1`.
```
--------------------------------------------------------------------------------
/src/client/config.rs:
--------------------------------------------------------------------------------
```rust
use std::time::Duration;
use tonic::transport::Endpoint;
pub struct ClientConfig {
pub endpoint: String,
pub keep_alive_interval: Duration,
pub keep_alive_timeout: Duration,
}
impl ClientConfig {
pub fn new(
endpoint: String,
keep_alive_interval: Duration,
keep_alive_timeout: Duration,
) -> Self {
Self {
endpoint,
keep_alive_interval,
keep_alive_timeout,
}
}
}
pub fn create_channel(config: &ClientConfig) -> Result<Endpoint, tonic::transport::Error> {
Ok(Endpoint::from_shared(config.endpoint.clone())?
.tcp_keepalive(Some(Duration::from_secs(1)))
.http2_keep_alive_interval(config.keep_alive_interval)
.keep_alive_timeout(config.keep_alive_timeout)
.keep_alive_while_idle(true))
}
```
--------------------------------------------------------------------------------
/src/docs/checkmessage.md:
--------------------------------------------------------------------------------
```markdown
lightning-checkmessage -- Command to check if a signature is from a node
========================================================================
SYNOPSIS
--------
**checkmessage** *message* *zbase* [*pubkey*]
DESCRIPTION
-----------
The **checkmessage** RPC command is the counterpart to **signmessage**: given a node id (*pubkey*), signature (*zbase*) and a *message*, it verifies that the signature was generated by that node for that message (more technically: by someone who knows that node's secret).
As a special case, if *pubkey* is not specified, we will try every known node key (as per *listnodes*), and verification succeeds if it matches for any one of them. Note: this is implemented far more efficiently than trying each one, so performance is not a concern.
- **message** (string): Message to be checked against the signature.
- **zbase** (string): The Zbase32 encoded signature to verify.
- **pubkey** (pubkey, optional): The Zbase32 encoded signature to verify.
```
--------------------------------------------------------------------------------
/src/docs/listchannels.md:
--------------------------------------------------------------------------------
```markdown
lightning-listchannels -- Command to query active lightning channels in the entire network
**listchannels** [*short\_channel\_id*] [*source*] [*destination*]
The **listchannels** RPC command returns data on channels that are known to the node. Because channels may be bidirectional, up to 2 objects will be returned for each channel (one for each direction).
Only one of *short\_channel\_id*, *source* or *destination* can be supplied. If nothing is supplied, data on all lightning channels known to this node, are returned. These can be local channels or public channels broadcast on the gossip network.
- **short\_channel\_id** (short\_channel\_id, optional): If short\_channel\_id is a short channel id, then only known channels with a matching short\_channel\_id are returned. Otherwise, it must be null.
- **source** (pubkey, optional): If source is a node id, then only channels leading from that node id are returned.
- **destination** (pubkey, optional): If destination is a node id, then only channels leading to that node id are returned.
```
--------------------------------------------------------------------------------
/src/docs/listforwards.md:
--------------------------------------------------------------------------------
```markdown
lightning-listforwards -- Command showing all htlcs and their information
**listforwards** [*status*] [*in\_channel*] [*out\_channel*] [*index* [*start*] [*limit*]]
The **listforwards** RPC command displays all htlcs that have been attempted to be forwarded by the Core Lightning node.
- **status** (string, optional) (one of "offered", "settled", "local\_failed", "failed"): If specified, then only the forwards with the given status are returned.
- **in\_channel** (short\_channel\_id, optional): Only the matching forwards on the given inbound channel are returned.
- **out\_channel** (short\_channel\_id, optional): Only the matching forwards on the given outbount channel are returned.
- **index** (string, optional) (one of "created", "updated"): If neither *in\_channel* nor *out\_channel* is specified, it controls ordering. The default is `created`. *(added v23.11)*
- **start** (u64, optional): If `index` is specified, `start` may be specified to start from that value, which is generally returned from lightning-wait(7). *(added v23.11)*
- **limit** (u32, optional): If `index` is specified, `limit` can be used to specify the maximum number of entries to return. *(added v23.11)*
```
--------------------------------------------------------------------------------
/src/docs/listpeers.md:
--------------------------------------------------------------------------------
```markdown
lightning-listpeers -- Command returning data on connected lightning nodes
**listpeers** [*id*] [*level*]
The **listpeers** RPC command returns data on nodes that are connected or are not connected but have open channels with this node.
Once a connection to another lightning node has been established, using the **connect** command, data on the node can be returned using **listpeers** and the *id* that was used with the **connect** command.
If no *id* is supplied, then data on all lightning nodes that are connected, or not connected but have open channels with this node, are returned.
If a channel is open with a node and the connection has been lost, then the node will still appear in the output of the command and the value of the *connected* attribute of the node will be "false".
The channel will remain open for a set blocktime, after which if the connection has not been re-established, the channel will close and the node will no longer appear in the command output.
- **id** (pubkey, optional): If supplied, limits the result to just the peer with the given ID, if it exists.
- **level** (string, optional) (one of "io", "trace", "debug", "info", "unusual"): Supplying level will show log entries related to that peer at the given log level.
```
--------------------------------------------------------------------------------
/src/docs/listsendpays.md:
--------------------------------------------------------------------------------
```markdown
lightning-listsendpays -- Low-level command for querying sendpay status
**listsendpays** [*bolt11*] [*payment\_hash*] [*status*] [*index* [*start*] [*limit*]]
The **listsendpays** RPC command gets the status of all *sendpay* commands (which is also used by the *pay* command), or with *bolt11* or *payment\_hash* limits results to that specific payment. You cannot specify both. It is possible to filter the payments also by *status*.
Note that there may be more than one concurrent *sendpay* command per *pay*, so this command should be used with caution.
- **bolt11** (string, optional): Bolt11 invoice.
- **payment\_hash** (hash, optional): The hash of the payment\_preimage.
- **status** (string, optional) (one of "pending", "complete", "failed"): Whether the invoice has been paid, pending, or failed.
- **index** (string, optional) (one of "created", "updated"): If neither bolt11 or payment\_hash is specified, `index` controls ordering, by `created` (default) or `updated`. *(added v23.11)*
- **start** (u64, optional): If `index` is specified, `start` may be specified to start from that value, which is generally returned from lightning-wait(7). *(added v23.11)*
- **limit** (u32, optional): If `index` is specified, `limit` can be used to specify the maximum number of entries to return. *(added v23.11)*
```
--------------------------------------------------------------------------------
/src/docs/listinvoices.md:
--------------------------------------------------------------------------------
```markdown
lightning-listinvoices -- Command for querying invoice status
**listinvoices** [*label*] [*invstring*] [*payment\_hash*] [*offer\_id*] [*index* [*start*] [*limit*]]
The **listinvoices** RPC command gets the status of a specific invoice, if it exists, or the status of all invoices if given no argument.
Only one of the query parameters can be used from *label*, *invstring*, *payment\_hash*, or *offer\_id*.
- **label** (one of, optional): A label used a the creation of the invoice to get a specific invoice.:
- (string)
- (integer)
- **invstring** (string, optional): The string value to query a specific invoice.
- **payment\_hash** (hex, optional): A payment\_hash of the invoice to get the details of a specific invoice.
- **offer\_id** (string, optional): A local `offer_id` the invoice was issued for a specific invoice details.
- **index** (string, optional) (one of "created", "updated"): If neither *in\_channel* nor *out\_channel* is specified, it controls ordering. The default is `created`. *(added v23.08)*
- **start** (u64, optional): If `index` is specified, `start` may be specified to start from that value, which is generally returned from lightning-wait(7). *(added v23.08)*
- **limit** (u32, optional): If `index` is specified, `limit` can be used to specify the maximum number of entries to return. *(added v23.08)*
```
--------------------------------------------------------------------------------
/src/docs/bkpr-listbalances.md:
--------------------------------------------------------------------------------
```markdown
lightning-bkpr-listbalances -- Command for listing current channel + wallet balances
====================================================================================
SYNOPSIS
--------
**bkpr-listbalances**
DESCRIPTION
-----------
The **bkpr-listbalances** RPC command is a list of all current and historical account balances. An account is either the on-chain *wallet* or a channel balance. Any funds sent to an *external* account will not be accounted for here.
Note that any channel that was recorded will be listed. Closed channel balances will be 0msat.
RETURN VALUE
------------
On success, an object containing **accounts** is returned. It is an array of objects, where each object contains:
- **account** (string): The account name. If the account is a channel, the channel\_id.
- **balances** (array of objects):
- **balance\_msat** (msat): Current account balance.
- **coin\_type** (string): Coin type, same as HRP for bech32.
If **peer\_id** is present:
- **peer\_id** (pubkey): Node id for the peer this account is with.
- **we\_opened** (boolean): Did we initiate this account open (open the channel).
- **account\_closed** (boolean):
- **account\_resolved** (boolean): Has this channel been closed and all outputs resolved?
- **resolved\_at\_block** (u32, optional): Blockheight account resolved on chain.
```
--------------------------------------------------------------------------------
/src/docs/listpays.md:
--------------------------------------------------------------------------------
```markdown
lightning-listpays -- Command for querying payment status
**listpays** [*bolt11*] [*payment\_hash*] [*status*] [*index*] [*start*] [*limit*]
The **listpays** RPC command gets the status of all *pay* commands (by combining results from listsendpays which lists every payment part), or a single one if either *bolt11* or *payment\_hash* was specified.
- **bolt11** (string, optional): Bolt11 string to get the payment details.
- **payment\_hash** (hash, optional): Payment hash to get the payment details.
- **status** (string, optional) (one of "pending", "complete", "failed"): To filter the payment by status.
- **index** (string, optional) (one of "created", "updated"): If neither *in\_channel* nor *out\_channel* is specified, it controls ordering, by `created` or `updated`. *(added v24.11)*
- **start** (u64, optional): If `index` is specified, `start` may be specified to start from that value, which is generally returned from lightning-wait(7).
NOTE: if this is used, `amount_sent_msat` and `number_of_parts` fields may be lower than expected, as not all payment parts will be considered *(added v24.11)*
- **limit** (u32, optional): If `index` is specified, `limit` can be used to specify the maximum number of entries to return.
NOTE: if this is used, `amount_sent_msat` and `number_of_parts` fields may be lower than expected, as not all payment parts will be considered
NOTE: the actual number returned may be less than the limit, as individual payment parts are combined together *(added v24.11)*
```
--------------------------------------------------------------------------------
/src/utils/tls.rs:
--------------------------------------------------------------------------------
```rust
use crate::error::{CertError, Result};
use std::path::Path;
use tonic::transport::{Certificate, ClientTlsConfig, Identity};
pub struct CertPaths {
pub ca_cert: String,
pub client_cert: String,
pub client_key: String,
}
impl CertPaths {
fn new(cert_dir: String) -> Self {
Self {
ca_cert: cert_dir.clone() + "/ca.pem",
client_cert: cert_dir.clone() + "/client.pem",
client_key: cert_dir.clone() + "/client-key.pem",
}
}
}
pub async fn load_tls_config(cert_dir: Option<String>) -> Result<ClientTlsConfig> {
let dir = cert_dir.unwrap_or("~".into());
let paths = CertPaths::new(dir);
load_tls_config_with_paths(&paths).await
}
pub async fn load_tls_config_with_paths(paths: &CertPaths) -> Result<ClientTlsConfig> {
// Check if files exist first
check_cert_files(paths)?;
// Load certificates
let ca_cert = tokio::fs::read(&paths.ca_cert)
.await
.map_err(CertError::IoError)?;
let client_cert = tokio::fs::read(&paths.client_cert)
.await
.map_err(CertError::IoError)?;
let client_key = tokio::fs::read(&paths.client_key)
.await
.map_err(CertError::IoError)?;
// Create TLS configuration
let tls_config = ClientTlsConfig::new()
.domain_name("localhost")
.ca_certificate(Certificate::from_pem(&ca_cert))
.identity(Identity::from_pem(&client_cert, &client_key));
Ok(tls_config)
}
fn check_cert_files(paths: &CertPaths) -> Result<()> {
let check_file = |path: &str| -> Result<()> {
if !Path::new(path).exists() {
return Err(CertError::MissingCert(path.to_string()));
}
Ok(())
};
check_file(&paths.ca_cert)?;
check_file(&paths.client_cert)?;
check_file(&paths.client_key)?;
Ok(())
}
```
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
```yaml
name: Release
on:
push:
tags:
- 'v*'
env:
CARGO_TERM_COLOR: always
jobs:
build:
name: Build
runs-on: ${{ matrix.os }}
strategy:
matrix:
include:
- os: ubuntu-latest
name: linux-x86_64
artifact_name: cln-mcp-linux-x86_64
artifact_pattern: "cln-mcp-linux-x86_64.tar.gz"
- os: macos-latest
name: macos-x86_64
artifact_name: cln-mcp-macos-x86_64
artifact_pattern: "cln-mcp-macos-x86_64.tar.gz"
- os: windows-latest
name: windows-x86_64
artifact_name: cln-mcp-windows-x86_64
artifact_pattern: "cln-mcp-windows-x86_64.zip"
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Rust stable toolchain
run: |
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile=minimal --default-toolchain stable
rustup override set stable
- name: Install protoc
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y protobuf-compiler
- name: Install protoc
if: runner.os == 'macOS'
run: |
brew install protobuf
- name: Install protoc
if: runner.os == 'Windows'
run: |
choco install protoc
- name: Build
run: |
cargo build --release
- name: Package Windows
if: runner.os == 'Windows'
run: |
cd target/release
7z a -tzip ../../${{ matrix.artifact_pattern }} cln-mcp.exe
cd ../..
- name: Package Unix
if: runner.os != 'Windows'
run: |
cd target/release
tar -czf ../../${{ matrix.artifact_pattern }} cln-mcp
cd ../..
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact_name }}
path: ${{ matrix.artifact_pattern }}
if-no-files-found: error
```
--------------------------------------------------------------------------------
/src/docs/bkpr-listincome.md:
--------------------------------------------------------------------------------
```markdown
lightning-bkpr-listincome -- Command for listing all income impacting events
============================================================================
SYNOPSIS
--------
**bkpr-listincome** [*consolidate\_fees*] [*start\_time*] [*end\_time*]
DESCRIPTION
-----------
Command *added* in pre-v0.10.1.
The **bkpr-listincome** RPC command is a list of all income impacting events that the bookkeeper plugin has recorded for this node.
- **consolidate\_fees** (boolean, optional): If true, we emit a single, consolidated event for any onchain-fees for a txid and account. Otherwise, events for every update to the onchain fee calculation for this account and txid will be printed. Note that this means that the events emitted are non-stable, i.e. calling **listincome** twice may result in different onchain fee events being emitted, depending on how much information we've logged for that transaction. The default is True.
- **start\_time** (u32, optional): UNIX timestamp (in seconds) that filters events after the provided timestamp. The default is zero.
- **end\_time** (u32, optional): UNIX timestamp (in seconds) that filters events up to and at the provided timestamp. The default is max-int.
RETURN VALUE
------------
On success, an object containing **income\_events** is returned. It is an array of objects, where each object contains:
- **account** (string): The account name. If the account is a channel, the channel\_id.
- **tag** (string): Type of income event.
- **credit\_msat** (msat): Amount earned (income).
- **debit\_msat** (msat): Amount spent (expenses).
- **currency** (string): Human-readable bech32 part for this coin type.
- **timestamp** (u32): Timestamp this event was recorded by the node. For consolidated events such as onchain\_fees, the most recent timestamp.
- **description** (string, optional): More information about this event. If a `invoice` type, typically the bolt11/bolt12 description.
- **outpoint** (string, optional): The txid:outnum for this event, if applicable.
- **txid** (txid, optional): The txid of the transaction that created this event, if applicable.
- **payment\_id** (hex, optional): Lightning payment identifier. For an htlc, this will be the preimage.
```
--------------------------------------------------------------------------------
/src/docs/feerates.md:
--------------------------------------------------------------------------------
```markdown
lightning-feerates -- Command for querying recommended onchain feerates
=======================================================================
**feerates** *style*
The **feerates** command returns the feerates that CLN will use. The feerates will be based on the recommended feerates from the backend. The backend may fail to provide estimates, but if it was able to provide estimates in the past, CLN will continue to use those for a while. CLN will also smoothen feerate estimations from the backend.
Explorers often present fees in "sat/vB": 4 sat/vB is `4000perkb` or `1000perkw`.
Bitcoin transactions have non-witness and witness bytes:
* Non-witness bytes count as 4 weight, 1 virtual byte. All bytes other than SegWit witness count as non-witness bytes. * Witness bytes count as 1 weight, 0.25 virtual bytes.
Thus, all *perkb* feerates will be exactly 4 times *perkw* feerates.
To compute the fee for a transaction, multiply its weight or virtual bytes by the appropriate *perkw* or *perkw* feerate returned by this command, then divide by 1000.
There is currently no way to change these feerates from the RPC. If you need custom control over onchain feerates, you will need to provide your own plugin that replaces the `bcli` plugin backend. For commands like lightning-withdraw(7) or lightning-fundchannel(7) you can provide a preferred feerate directly as a parameter, which will override the recommended feerates returned by **feerates**.
- **style** (string) (one of "perkb", "perkw"): Fee rate style to use. This can be:
*perkw* - provide feerate in units of satoshis per 1000 weight (e.g. the minimum fee is usually `253perkw`).
*perkb* - provide feerate in units of satoshis per 1000 virtual bytes (eg. the minimum fee is usually `1000perkb`).
Many other commands have a *feerate* parameter. This can be:
* One of the strings to use lightningd's internal estimates:
* *urgent* (next 6 blocks or so)
* *normal* (next 12 blocks or so)
* *slow* (next 100 blocks or so)
* *minimum* for the lowest value bitcoind will currently accept (added in v23.05)
* A number, with an optional suffix:
* *blocks* means aim for confirmation in that many blocks (added in v23.05)
* *perkw* means the number is interpreted as satoshi-per-kilosipa (weight)
* *perkb* means it is interpreted bitcoind-style as satoshi-per-kilobyte.
Omitting the suffix is equivalent to *perkb*.
```
--------------------------------------------------------------------------------
/src/docs/decodepay.md:
--------------------------------------------------------------------------------
```markdown
lightning-decodepay -- Command for decoding a bolt11 string (low-level)
=======================================================================
SYNOPSIS
--------
**decodepay** *bolt11* [*description*]
DESCRIPTION
-----------
Command *added* in v23.05.
WARNING: deprecated: use *decode* which also handles bolt12.
The **decodepay** RPC command checks and parses a *bolt11* string as specified by the BOLT 11 specification.
- **bolt11** (string): Bolt11 invoice to decode.
- **description** (string, optional): Description of the invoice to decode.
RETURN VALUE
------------
On success, an object is returned, containing:
- **currency** (string): The BIP173 name for the currency.
- **created\_at** (u64): The UNIX-style timestamp of the invoice.
- **expiry** (u64): The number of seconds this is valid after *timestamp*.
- **payee** (pubkey): The public key of the recipient.
- **payment\_hash** (hash): The hash of the *payment\_preimage*.
- **signature** (signature): Signature of the *payee* on this invoice.
- **min\_final\_cltv\_expiry** (u32): The minimum CLTV delay for the final node.
- **amount\_msat** (msat, optional): Amount the invoice asked for.
- **description** (string, optional): The description of the purpose of the purchase.
- **description\_hash** (hash, optional): The hash of the description, in place of *description*.
- **payment\_secret** (hash, optional): The secret to hand to the payee node.
- **features** (hex, optional): The features bitmap for this invoice.
- **payment\_metadata** (hex, optional): The payment\_metadata to put in the payment.
- **fallbacks** (array of objects, optional): Onchain addresses.:
- **type** (string) (one of "P2PKH", "P2SH", "P2WPKH", "P2WSH", "P2TR"): The address type (if known).
- **hex** (hex): Raw encoded address.
- **addr** (string, optional): The address in appropriate format for *type*.
- **routes** (array of arrays, optional): Route hints to the *payee*.:
- (array of objects): Hops in the route.
- **pubkey** (pubkey): The public key of the node.
- **short\_channel\_id** (short\_channel\_id): A channel to the next peer.
- **fee\_base\_msat** (msat): The base fee for payments.
- **fee\_proportional\_millionths** (u32): The parts-per-million fee for payments.
- **cltv\_expiry\_delta** (u32): The CLTV delta across this hop.
- **extra** (array of objects, optional): Any extra fields we didn't know how to parse.:
- **tag** (string) (always 1 characters): The bech32 letter which identifies this field.
- **data** (string): The bech32 data for this field.
```
--------------------------------------------------------------------------------
/.github/workflows/grpc.yml:
--------------------------------------------------------------------------------
```yaml
name: grpc
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
env:
CARGO_TERM_COLOR: always
jobs:
CLN-GRPC:
name: Server test
runs-on: macos-14
timeout-minutes: 180
strategy:
fail-fast: true
matrix:
bitcoind-version: ["27.1"]
steps:
- name: Checkout source code
uses: actions/checkout@v4
- name: Get CLN
run: git clone https://github.com/ElementsProject/lightning.git
- name: Download Bitcoin ${{ matrix.bitcoind-version }} & install binaries
run: |
export BITCOIND_VERSION=${{ matrix.bitcoind-version }}
export TARGET_ARCH="arm64-apple-darwin"
wget https://bitcoincore.org/bin/bitcoin-core-${BITCOIND_VERSION}/bitcoin-${BITCOIND_VERSION}-${TARGET_ARCH}.tar.gz
tar -xzf bitcoin-${BITCOIND_VERSION}-${TARGET_ARCH}.tar.gz
sudo mv bitcoin-${BITCOIND_VERSION}/bin/* /usr/local/bin
rm -rf bitcoin-${BITCOIND_VERSION}-${TARGET_ARCH}.tar.gz bitcoin-${BITCOIND_VERSION}
- name: Install dependencies
run: |
cd lightning
export PATH="/usr/local/opt:/Users/runner/.local/bin:/opt/homebrew/bin/python3.10/bin:$PATH"
brew install gnu-sed [email protected] autoconf automake libtool protobuf
python3.10 -m pip install -U --user poetry==1.8.0 wheel pip mako
python3.10 -m poetry install
- name: Build and install CLN
run: |
cd lightning
export CPATH=/opt/homebrew/include
export LIBRARY_PATH=/opt/homebrew/lib
python3.10 -m poetry run ./configure --disable-valgrind --disable-compat
python3.10 -m poetry run make -j
- name: Start bitcoind in regtest mode
run: |
bitcoind -regtest -daemon
sleep 5
- name: Generate initial block
run: |
bitcoin-cli -regtest createwallet default_wallet
bitcoin-cli -regtest generatetoaddress 1 $(bitcoin-cli -regtest getnewaddress)
sleep 2
- name: Start CLN in regtest mode
run: |
cd lightning
lightningd/lightningd --network=regtest --log-file=/tmp/l1.log --daemon --grpc-port=9736
sleep 5
- name: Verify CLN is running
run: |
cd lightning
cli/lightning-cli --regtest getinfo
- name: Install Rust stable toolchain
run: |
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile=minimal --default-toolchain stable
rustup override set stable
- name: Build server
run: cargo test
```
--------------------------------------------------------------------------------
/src/main.rs:
--------------------------------------------------------------------------------
```rust
mod client;
mod error;
mod utils;
use crate::client::config::{create_channel, ClientConfig};
use crate::client::node::NodeService;
use crate::utils::tls::load_tls_config;
use anyhow::Result;
use rmcp::{transport::stdio, ServiceExt};
use std::env;
use std::net::ToSocketAddrs;
use std::time::Duration;
use tracing::{debug, error, info};
use tracing_subscriber::{self, EnvFilter};
fn print_usage() {
println!("Usage: cln-mcp [OPTIONS]");
println!("Options:");
println!(" --certs-dir <path> Path to certificates directory");
println!(" --node-address <url> Node address (default: https://localhost:9736)");
println!(" --help Shows this help message");
}
fn parse_args() -> (Option<String>, String) {
let args: Vec<String> = env::args().collect();
let mut certs_dir = None;
let mut node_address = "https://localhost:9736".to_string();
let mut i = 1;
while i < args.len() {
match args[i].as_str() {
"--certs-dir" => {
if i + 1 < args.len() {
certs_dir = Some(args[i + 1].clone());
i += 2;
} else {
error!("Error: --certs-dir requires a path");
std::process::exit(1);
}
}
"--node-adddress" => {
if i + 1 < args.len() {
node_address = args[i + 1].clone();
if node_address.to_socket_addrs().is_err() {
error!("Error: Invalid node address!");
std::process::exit(1);
}
i += 2;
} else {
error!("Error: --node-address requires a URL");
std::process::exit(1);
}
}
"--help" => {
print_usage();
std::process::exit(0);
}
_ => {
error!("Unknown argument: {}", args[i]);
print_usage();
std::process::exit(1);
}
}
}
(certs_dir, node_address)
}
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt()
.with_env_filter(
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("debug")),
)
.init();
info!("CLN MCP server initiated!");
let (certs_dir, node_addr) = parse_args();
// Load TLS certificate
let tls_config = load_tls_config(certs_dir).await?;
debug!("TLS certificate loaded!");
// Create channel with default config
let config = ClientConfig::new(node_addr, Duration::from_secs(1), Duration::from_secs(5));
let channel = create_channel(&config)?
.tls_config(tls_config)?
.connect_lazy();
info!("--------------------Server Started Running!--------------------------");
// Create and run the server with STDIO transport
let service = NodeService::new(channel)
.serve(stdio())
.await
.inspect_err(|e| {
println!("Error starting server: {e}");
})?;
service.waiting().await?;
Ok(())
}
```
--------------------------------------------------------------------------------
/src/docs/getroute.md:
--------------------------------------------------------------------------------
```markdown
lightning-getroute -- Command for routing a payment (low-level)
===============================================================
**getroute** *id* *amount\_msat* *riskfactor* [*cltv*] [*fromid*] [*fuzzpercent*] [*exclude*] [*maxhops*]
The **getroute** RPC command attempts to find the best route for the payment of *amount\_msat* to lightning node *id*, such that the payment will arrive at *id* with *cltv*.
There are two considerations for how good a route is: how low the fees are, and how long your payment will get stuck in a delayed output if a node goes down during the process. .
- **id** (pubkey): Node pubkey to find the best route for the payment.
- **amount\_msat** (msat): Amount to send. It can be a whole number, or a whole number ending in *msat* or *sat*, or a number with three decimal places ending in *sat*, or a number with 1 to 11 decimal places ending in *btc*. The 0 value is special: it ignores any *htlc\_minimum\_msat* setting on channels, and simply returns a possible route (if any) which is useful for simple probing.
- **riskfactor** (u64): A non-negative floating-point field controls this tradeoff; it is the annual cost of your funds being stuck (as a percentage). For example, if you thought the convenience of keeping your funds liquid (not stuck) was worth 20% per annum interest, *riskfactor* would be 20. If you didn't care about risk, *riskfactor* would be zero.
- **cltv** (u32, optional): Cltv-blocks to spare. The default is 9.
- **fromid** (pubkey, optional): The node to start the route from. The default is this node.
- **fuzzpercent** (u32, optional): Used to distort fees to provide some randomization to the route generated, but it was not properly implemented and is ignored.
- **exclude** (array of strings, optional): A JSON array of short-channel-id/direction (e.g. ['564334x877x1/0', '564195x1292x0/1' ]) or node-id which should be excluded from consideration for routing. Note if the source or destination is excluded, the command result is undefined. The default is not to exclude any channels or nodes.:
- (string, optional)
- **maxhops** (u32, optional): The maximum number of channels to return. The default is 20.
RISKFACTOR EFFECT ON ROUTING
----------------------------
The risk factor is treated as if it were an additional fee on the route, for the purposes of comparing routes.
The formula used is the following approximation:
risk-fee = amount x blocks-timeout x per-block-cost
We are given a *riskfactor* expressed as a percentage. There are 52596 blocks per year, thus *per-block-cost* is *riskfactor* divided by 5,259,600.
The final result is:
risk-fee = amount x blocks-timeout x riskfactor / 5259600
RECOMMENDED RISKFACTOR VALUES
-----------------------------
The default *fuzz* factor is 5%, so as you can see from the table above, that tends to overwhelm the effect of *riskfactor* less than about 5.
1 is a conservative value for a stable lightning network with very few failures.
1000 is an aggressive value for trying to minimize timeouts at all costs.
The default for lightning-pay(7) is 10, which starts to become a major factor for larger amounts, and is basically ignored for tiny ones.
```
--------------------------------------------------------------------------------
/src/docs/bkpr-listaccountevents.md:
--------------------------------------------------------------------------------
```markdown
lightning-bkpr-listaccountevents -- Command for listing recorded bookkeeping events
===================================================================================
SYNOPSIS
--------
**bkpr-listaccountevents** [*account*] [*payment\_id*]
DESCRIPTION
-----------
The **bkpr-listaccountevents** RPC command is a list of all bookkeeping events that have been recorded for this node.
If the optional parameter **account** is set, we only emit events for the specified account, if exists.
If the optional parameter **payment\_id** is set, we only emit events which have that value as payment hash or as transaction id.
The parameters **account** and **payment\_id** are mutually exclusive.
Note that the type **onchain\_fees** that are emitted are of opposite credit/debit than as they appear in **listincome**, as **listincome** shows all events from the perspective of the node, whereas **listaccountevents** just dumps the event data as we've got it. Onchain fees are updated/recorded as we get more information about input and output spends -- the total onchain fees that were recorded for a transaction for an account can be found by summing all onchain fee events and taking the difference between the **credit\_msat** and **debit\_msat** for these events. We do this so that successive calls to **listaccountevents** always produce the same list of events -- no previously emitted event will be subsequently updated, rather we add a new event to the list.
- **account** (string, optional): Receive events for the specified account.
- **payment\_id** (string, optional): Receive events for the specified payment id. *(added v24.08)*
RETURN VALUE
------------
On success, an object containing **events** is returned. It is an array of objects, where each object contains:
- **account** (string): The account name. If the account is a channel, the channel\_id.
- **type** (string) (one of "onchain\_fee", "chain", "channel"): Coin movement type.
- **tag** (string): Description of movement.
- **credit\_msat** (msat): Amount credited.
- **debit\_msat** (msat): Amount debited.
- **currency** (string): Human-readable bech32 part for this coin type.
- **timestamp** (u32): Timestamp this event was recorded by the node. For consolidated events such as onchain\_fees, the most recent timestamp.
If **type** is "chain":
- **outpoint** (string): The txid:outnum for this event.
- **blockheight** (u32): For chain events, blockheight this occured at.
- **description** (string, optional): The description of this event.
- **origin** (string, optional): The account this movement originated from.
- **payment\_id** (hex, optional): Lightning payment identifier. For an htlc, this will be the preimage.
- **txid** (txid, optional): The txid of the transaction that created this event.
If **type** is "onchain\_fee":
- **txid** (txid): The txid of the transaction that created this event.
If **type** is "channel":
- **fees\_msat** (msat, optional): Amount paid in fees.
- **is\_rebalance** (boolean, optional): Is this payment part of a rebalance.
- **payment\_id** (hex, optional): Lightning payment identifier. For an htlc, this will be the preimage.
- **part\_id** (u32, optional): Counter for multi-part payments.
```
--------------------------------------------------------------------------------
/src/docs/bkpr-channelsapy.md:
--------------------------------------------------------------------------------
```markdown
lightning-bkpr-channelsapy -- Command to list stats on channel earnings
=======================================================================
SYNOPSIS
--------
**bkpr-channelsapy** [*start\_time*] [*end\_time*]
DESCRIPTION
-----------
The **bkpr-channelsapy** RPC command lists stats on routing income, leasing income, and various calculated APYs for channel routed funds.
- **start\_time** (u64, optional): UNIX timestamp (in seconds) to filter events after the provided timestamp. The default is zero.
- **end\_time** (u64, optional): UNIX timestamp (in seconds) to filter events up to and at the provided timestamp. The default is max-int.
RETURN VALUE
------------
On success, an object containing **channels\_apy** is returned. It is an array of objects, where each object contains:
- **account** (string): The account name. If the account is a channel, the channel\_id. The 'net' entry is the rollup of all channel accounts.
- **routed\_out\_msat** (msat): Sats routed (outbound).
- **routed\_in\_msat** (msat): Sats routed (inbound).
- **lease\_fee\_paid\_msat** (msat): Sats paid for leasing inbound (liquidity ads).
- **lease\_fee\_earned\_msat** (msat): Sats earned for leasing outbound (liquidity ads).
- **pushed\_out\_msat** (msat): Sats pushed to peer at open.
- **pushed\_in\_msat** (msat): Sats pushed in from peer at open.
- **our\_start\_balance\_msat** (msat): Starting balance in channel at funding. Note that if our start balance is zero, any \_initial field will be omitted (can't divide by zero).
- **channel\_start\_balance\_msat** (msat): Total starting balance at funding.
- **fees\_out\_msat** (msat): Fees earned on routed outbound.
- **utilization\_out** (string): Sats routed outbound / total start balance.
- **utilization\_in** (string): Sats routed inbound / total start balance.
- **apy\_out** (string): Fees earned on outbound routed payments / total start balance for the length of time this channel has been open amortized to a year (APY).
- **apy\_in** (string): Fees earned on inbound routed payments / total start balance for the length of time this channel has been open amortized to a year (APY).
- **apy\_total** (string): Total fees earned on routed payments / total start balance for the length of time this channel has been open amortized to a year (APY).
- **fees\_in\_msat** (msat, optional): Fees earned on routed inbound.
- **utilization\_out\_initial** (string, optional): Sats routed outbound / our start balance.
- **utilization\_in\_initial** (string, optional): Sats routed inbound / our start balance.
- **apy\_out\_initial** (string, optional): Fees earned on outbound routed payments / our start balance for the length of time this channel has been open amortized to a year (APY).
- **apy\_in\_initial** (string, optional): Fees earned on inbound routed payments / our start balance for the length of time this channel has been open amortized to a year (APY).
- **apy\_total\_initial** (string, optional): Total fees earned on routed payments / our start balance for the length of time this channel has been open amortized to a year (APY).
- **apy\_lease** (string, optional): Lease fees earned over total amount leased for the lease term, amortized to a year (APY). Only appears if channel was leased out by us.
```
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
```yaml
name: CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
env:
CARGO_TERM_COLOR: always
jobs:
build:
strategy:
fail-fast: false
matrix:
platform: [ ubuntu-latest, windows-latest, macos-latest ]
toolchain: [ stable, beta, nightly ]
runs-on: ${{ matrix.platform }}
steps:
- name: Checkout source code
uses: actions/checkout@v4
- name: Install Rust ${{ matrix.toolchain }} toolchain
run: |
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile=minimal --default-toolchain ${{ matrix.toolchain }}
rustup override set ${{ matrix.toolchain }}
# Add protoc installation steps for each OS
- name: Install protoc (Ubuntu)
if: "matrix.platform == 'ubuntu-latest'"
run: |
sudo apt-get update
sudo apt-get install -y protobuf-compiler
- name: Install protoc (macOS)
if: "matrix.platform == 'macos-latest'"
run: |
brew install protobuf
- name: Install protoc (Windows)
if: "matrix.platform == 'windows-latest'"
run: |
choco install protoc
- name: Install no-std-check dependencies for ARM Embedded
if: "matrix.platform == 'ubuntu-latest'"
run: |
rustup target add thumbv7m-none-eabi
sudo apt-get -y install gcc-arm-none-eabi
sudo apt-get -y install gcc-arm-none-eabi
- name: Build server
run: cargo build
check_release:
runs-on: ubuntu-latest
env:
TOOLCHAIN: stable
steps:
- name: Checkout source code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Rust ${{ env.TOOLCHAIN }} toolchain
run: |
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile=minimal --default-toolchain ${{ env.TOOLCHAIN }}
rustup override set ${{ env.TOOLCHAIN }}
- name: Install protoc (Ubuntu)
run: |
sudo apt-get update
sudo apt-get install -y protobuf-compiler
- name: Run cargo check for release build.
run: |
cargo check --release
cargo doc --release
rustfmt:
runs-on: ubuntu-latest
env:
TOOLCHAIN: stable
steps:
- name: Checkout source code
uses: actions/checkout@v4
- name: Install Rust ${{ env.TOOLCHAIN }} toolchain
run: |
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile=minimal --default-toolchain ${{ env.TOOLCHAIN }}
rustup override set ${{ env.TOOLCHAIN }}
- name: Install rustfmt
run: |
rustup component add rustfmt
- name: Run rustfmt checks
run: cargo fmt --check
linting:
runs-on: ubuntu-latest
env:
TOOLCHAIN: stable
steps:
- name: Checkout source code
uses: actions/checkout@v4
- name: Install Rust ${{ env.TOOLCHAIN }} toolchain
run: |
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile=minimal --default-toolchain ${{ env.TOOLCHAIN }}
rustup override set ${{ env.TOOLCHAIN }}
- name: Install protoc (Ubuntu)
run: |
sudo apt-get update
sudo apt-get install -y protobuf-compiler
- name: Install clippy
run: |
rustup component add clippy
- name: Run default clippy linting
run: cargo clippy -- -D warnings
```
--------------------------------------------------------------------------------
/src/client/node.rs:
--------------------------------------------------------------------------------
```rust
use cln_grpc::pb::node_client::NodeClient;
use cln_grpc::pb::{
BkprchannelsapyRequest, BkprlistaccounteventsRequest, BkprlistbalancesRequest,
BkprlistincomeRequest, FeeratesRequest, GetinfoRequest, GetlogRequest, ListaddressesRequest,
ListchannelsRequest, ListclosedchannelsRequest, ListconfigsRequest, ListdatastoreRequest,
ListforwardsRequest, ListfundsRequest, ListhtlcsRequest, ListinvoicesRequest, ListnodesRequest,
ListoffersRequest, ListpaysRequest, ListpeerchannelsRequest, ListpeersRequest,
ListsendpaysRequest,
};
use rmcp::{model::*, tool, Error as McpError};
use serde::Serialize;
use std::sync::Arc;
use tokio::sync::Mutex;
use tonic::{transport::Channel, Request, Response};
use tracing::debug;
macro_rules! doc_from_file {
($path:expr) => {
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/docs/", $path))
};
}
#[derive(Clone)]
pub struct NodeService {
client: Arc<Mutex<NodeClient<Channel>>>,
}
fn get_response<T>(res: Result<Response<T>, tonic::Status>) -> Result<CallToolResult, McpError>
where
T: Serialize,
{
match res {
Ok(response) => {
// Convert the response into a JSON-serializable format
let response_data = serde_json::to_value(response.into_inner())
.map_err(|_| McpError::internal_error("Failed to serialize response", None))?;
Ok(CallToolResult::success(vec![
Content::json(response_data).unwrap()
]))
}
Err(e) => Err(McpError::internal_error(
format!("Failed to communicate with lightning node: {e}"),
None,
)),
}
}
#[tool(tool_box)]
impl NodeService {
pub fn new(channel: Channel) -> Self {
Self {
client: Arc::new(Mutex::new(NodeClient::new(channel))),
}
}
// Node Information
#[tool(description = doc_from_file!("getinfo.md"))]
pub async fn get_info(&self) -> Result<CallToolResult, McpError> {
let request = Request::new(GetinfoRequest::default());
let mut client = self.client.lock().await;
let res = client.getinfo(request).await;
debug!("get_info called!");
get_response(res)
}
#[tool(description = doc_from_file!("listconfigs.md"))]
pub async fn list_configs(&self) -> Result<CallToolResult, McpError> {
let request = Request::new(ListconfigsRequest::default());
let mut client = self.client.lock().await;
let res = client.list_configs(request).await;
debug!("list_configs called!");
get_response(res)
}
#[tool(description = doc_from_file!("listaddresses.md"))]
pub async fn list_addresses(&self) -> Result<CallToolResult, McpError> {
let request = Request::new(ListaddressesRequest::default());
let mut client = self.client.lock().await;
let res = client.list_addresses(request).await;
debug!("list_addresses called!");
get_response(res)
}
// Channel Information
#[tool(description = doc_from_file!("listchannels.md"))]
pub async fn list_channels(&self) -> Result<CallToolResult, McpError> {
let request = Request::new(ListchannelsRequest::default());
let mut client = self.client.lock().await;
let res = client.list_channels(request).await;
debug!("list_channels called!");
get_response(res)
}
#[tool(description = doc_from_file!("listpeerchannels.md"))]
pub async fn list_peer_channels(&self) -> Result<CallToolResult, McpError> {
let request = Request::new(ListpeerchannelsRequest::default());
let mut client = self.client.lock().await;
let res = client.list_peer_channels(request).await;
debug!("list_peer_channels called!");
get_response(res)
}
#[tool(description = doc_from_file!("listclosedchannels.md"))]
pub async fn list_closed_channels(&self) -> Result<CallToolResult, McpError> {
let request = Request::new(ListclosedchannelsRequest::default());
let mut client = self.client.lock().await;
let res = client.list_closed_channels(request).await;
debug!("list_closed_channels called!");
get_response(res)
}
#[tool(description = doc_from_file!("listhtlcs.md"))]
pub async fn list_htlcs(&self) -> Result<CallToolResult, McpError> {
let request = Request::new(ListhtlcsRequest::default());
let mut client = self.client.lock().await;
let res = client.list_htlcs(request).await;
debug!("list_htlcs called!");
get_response(res)
}
// Payment Information
#[tool(description = doc_from_file!("listpays.md"))]
pub async fn list_pays(&self) -> Result<CallToolResult, McpError> {
let request = Request::new(ListpaysRequest::default());
let mut client = self.client.lock().await;
let res = client.list_pays(request).await;
debug!("list_pays called!");
get_response(res)
}
#[tool(description = doc_from_file!("listsendpays.md"))]
pub async fn list_send_pays(&self) -> Result<CallToolResult, McpError> {
let request = Request::new(ListsendpaysRequest::default());
let mut client = self.client.lock().await;
let res = client.list_send_pays(request).await;
debug!("list_send_pays called!");
get_response(res)
}
#[tool(description = doc_from_file!("listforwards.md"))]
pub async fn list_forwards(&self) -> Result<CallToolResult, McpError> {
let request = Request::new(ListforwardsRequest::default());
let mut client = self.client.lock().await;
let res = client.list_forwards(request).await;
debug!("list_forwards called!");
get_response(res)
}
#[tool(description = doc_from_file!("listinvoices.md"))]
pub async fn list_invoices(&self) -> Result<CallToolResult, McpError> {
let request = Request::new(ListinvoicesRequest::default());
let mut client = self.client.lock().await;
let res = client.list_invoices(request).await;
debug!("list_invoices called!");
get_response(res)
}
// #[tool(description = doc_from_file!("listinvoicerequests.md"))]
// pub async fn list_invoice_requests(&self) -> Result<CallToolResult, McpError> {
// let request = Request::new(ListinvoicerequestsRequest::default());
// let mut client = self.client.lock().await;
// let res = client.list_invoice_requests(request).await;
// get_response(res)
// }
// Network Information
#[tool(description = doc_from_file!("listpeers.md"))]
pub async fn list_peers(&self) -> Result<CallToolResult, McpError> {
let request = Request::new(ListpeersRequest::default());
let mut client = self.client.lock().await;
let res = client.list_peers(request).await;
debug!("list_peers called!");
get_response(res)
}
#[tool(description = doc_from_file!("listnodes.md"))]
pub async fn list_nodes(&self) -> Result<CallToolResult, McpError> {
let request = Request::new(ListnodesRequest::default());
let mut client = self.client.lock().await;
let res = client.list_nodes(request).await;
debug!("list_nodes called!");
get_response(res)
}
#[tool(description = doc_from_file!("listfunds.md"))]
pub async fn list_funds(&self) -> Result<CallToolResult, McpError> {
let request = Request::new(ListfundsRequest::default());
let mut client = self.client.lock().await;
let res = client.list_funds(request).await;
debug!("list_funds called!");
get_response(res)
}
// #[tool(description = doc_from_file!("getroute.md"))]
// pub async fn get_route(&self) -> Result<CallToolResult, McpError> {
// let request = Request::new(GetrouteRequest::default());
// let mut client = self.client.lock().await;
// let res = client.get_route(request).await;
// get_response(res)
// }
// Offer Information
#[tool(description = doc_from_file!("listoffers.md"))]
pub async fn list_offers(&self) -> Result<CallToolResult, McpError> {
let request = Request::new(ListoffersRequest::default());
let mut client = self.client.lock().await;
let res = client.list_offers(request).await;
debug!("list_offers called!");
get_response(res)
}
// Database Information
#[tool(description = doc_from_file!("listdatastore.md"))]
pub async fn list_datastore(&self) -> Result<CallToolResult, McpError> {
let request = Request::new(ListdatastoreRequest::default());
let mut client = self.client.lock().await;
let res = client.list_datastore(request).await;
debug!("list_datastore called!");
get_response(res)
}
// Bookkeeping Information
#[tool(description = doc_from_file!("bkpr-channelsapy.md"))]
pub async fn bkpr_channels_apy(&self) -> Result<CallToolResult, McpError> {
let request = Request::new(BkprchannelsapyRequest::default());
let mut client = self.client.lock().await;
let res = client.bkpr_channels_apy(request).await;
debug!("bkpr_channels_pay called!");
get_response(res)
}
#[tool(description = doc_from_file!("bkpr-listbalances.md"))]
pub async fn bkpr_list_balances(&self) -> Result<CallToolResult, McpError> {
let request = Request::new(BkprlistbalancesRequest::default());
let mut client = self.client.lock().await;
let res = client.bkpr_list_balances(request).await;
debug!("bkpr_list_balances called!");
get_response(res)
}
#[tool(description = doc_from_file!("bkpr-listincome.md"))]
pub async fn bkpr_list_income(&self) -> Result<CallToolResult, McpError> {
let request = Request::new(BkprlistincomeRequest::default());
let mut client = self.client.lock().await;
let res = client.bkpr_list_income(request).await;
debug!("bkpr_list_income called!");
get_response(res)
}
#[tool(description = doc_from_file!("bkpr-listaccountevents.md"))]
pub async fn bkpr_list_account_events(&self) -> Result<CallToolResult, McpError> {
let request = Request::new(BkprlistaccounteventsRequest::default());
let mut client = self.client.lock().await;
let res = client.bkpr_list_account_events(request).await;
debug!("bkpr_list_account_events called!");
get_response(res)
}
// Utility Commands
// #[tool(description = doc_from_file!("decode.md"))]
// pub async fn decode(&self) -> Result<CallToolResult, McpError> {
// let request = Request::new(DecodeRequest::default());
// let mut client = self.client.lock().await;
// let res = client.decode(request).await;
// get_response(res)
// }
// #[tool(description = doc_from_file!("decodepay.md"))]
// pub async fn decode_pay(&self) -> Result<CallToolResult, McpError> {
// let request = Request::new(DecodepayRequest::default());
// let mut client = self.client.lock().await;
// let res = client.decode_pay(request).await;
// get_response(res)
// }
// #[tool(description = doc_from_file!("checkmessage.md"))]
// pub async fn check_message(&self) -> Result<CallToolResult, McpError> {
// let request = Request::new(CheckmessageRequest::default());
// let mut client = self.client.lock().await;
// let res = client.check_message(request).await;
// get_response(res)
// }
#[tool(description = doc_from_file!("feerates.md"))]
pub async fn feerates(&self) -> Result<CallToolResult, McpError> {
let request = Request::new(FeeratesRequest::default());
let mut client = self.client.lock().await;
let res = client.feerates(request).await;
debug!("feerates called!");
get_response(res)
}
#[tool(description = doc_from_file!("getlog.md"))]
pub async fn get_log(&self) -> Result<CallToolResult, McpError> {
let request = Request::new(GetlogRequest::default());
let mut client = self.client.lock().await;
let res = client.get_log(request).await;
debug!("get_log called!");
get_response(res)
}
}
#[tool(tool_box)]
impl rmcp::ServerHandler for NodeService {
fn get_info(&self) -> ServerInfo {
ServerInfo {
protocol_version: ProtocolVersion::V_2024_11_05,
instructions: Some("Core Lightning Node".into()),
server_info: Implementation::from_build_env(),
capabilities: ServerCapabilities::builder().enable_tools().build(),
}
}
}
```
--------------------------------------------------------------------------------
/tests/integration_test.rs:
--------------------------------------------------------------------------------
```rust
use cln_mcp::{create_channel, load_tls_config, ClientConfig, NodeService};
use serde_json::Value;
use std::time::Duration;
use tokio;
async fn setup_test_env() -> Result<NodeService, Box<dyn std::error::Error>> {
let tls_config = load_tls_config(Some("/Users/runner/.lightning/regtest/".to_string())).await?;
let config = ClientConfig::new(
"https://localhost:9736".to_string(),
Duration::from_secs(1),
Duration::from_secs(5),
);
let channel = create_channel(&config)?
.tls_config(tls_config)?
.connect_lazy();
Ok(NodeService::new(channel))
}
#[tokio::test]
async fn test_server_initialization() {
let result = setup_test_env().await;
assert!(
result.is_ok(),
"Failed to initialize server: {:?}",
result.err()
);
}
// Node Information Tests
#[tokio::test]
async fn test_get_info() {
let service = setup_test_env()
.await
.expect("Failed to setup test environment");
let result = service.get_info().await;
assert!(result.is_ok(), "get_info failed: {:?}", result.err());
if let Ok(response) = result {
let content = response.content.first().expect("Empty response!");
let res_val: Value = serde_json::from_str(&content.raw.as_text().unwrap().text).unwrap();
let obj = res_val.as_object().unwrap();
assert!(obj.contains_key("lightning_dir"));
assert!(obj.contains_key("id"));
assert!(obj.contains_key("alias"));
assert!(obj.contains_key("version"));
}
}
#[tokio::test]
async fn test_list_configs() {
let service = setup_test_env()
.await
.expect("Failed to setup test environment");
let result = service.list_configs().await;
assert!(result.is_ok(), "list_configs failed: {:?}", result.err());
if let Ok(response) = result {
let content = response.content.first().expect("Empty response!");
let res_val: Value = serde_json::from_str(&content.raw.as_text().unwrap().text).unwrap();
let obj = res_val.as_object().unwrap();
assert!(obj.contains_key("configs"));
}
}
#[tokio::test]
async fn test_list_addresses() {
let service = setup_test_env()
.await
.expect("Failed to setup test environment");
let result = service.list_addresses().await;
assert!(result.is_ok(), "list_addresses failed: {:?}", result.err());
if let Ok(response) = result {
let content = response.content.first().expect("Empty response!");
let res_val: Value = serde_json::from_str(&content.raw.as_text().unwrap().text).unwrap();
let obj = res_val.as_object().unwrap();
assert!(obj.contains_key("addresses"));
}
}
// Channel Information Tests
#[tokio::test]
async fn test_list_channels() {
let service = setup_test_env()
.await
.expect("Failed to setup test environment");
let result = service.list_channels().await;
assert!(result.is_ok(), "list_channels failed: {:?}", result.err());
if let Ok(response) = result {
let content = response.content.first().expect("Empty response!");
let res_val: Value = serde_json::from_str(&content.raw.as_text().unwrap().text).unwrap();
let obj = res_val.as_object().unwrap();
assert!(obj.contains_key("channels"));
}
}
#[tokio::test]
async fn test_list_peer_channels() {
let service = setup_test_env()
.await
.expect("Failed to setup test environment");
let result = service.list_peer_channels().await;
assert!(
result.is_ok(),
"list_peer_channels failed: {:?}",
result.err()
);
if let Ok(response) = result {
let content = response.content.first().expect("Empty response!");
let res_val: Value = serde_json::from_str(&content.raw.as_text().unwrap().text).unwrap();
let obj = res_val.as_object().unwrap();
assert!(obj.contains_key("channels"));
}
}
#[tokio::test]
async fn test_list_closed_channels() {
let service = setup_test_env()
.await
.expect("Failed to setup test environment");
let result = service.list_closed_channels().await;
assert!(
result.is_ok(),
"list_closed_channels failed: {:?}",
result.err()
);
if let Ok(response) = result {
let content = response.content.first().expect("Empty response!");
let res_val: Value = serde_json::from_str(&content.raw.as_text().unwrap().text).unwrap();
let obj = res_val.as_object().unwrap();
assert!(obj.contains_key("closedchannels"));
}
}
#[tokio::test]
async fn test_list_htlcs() {
let service = setup_test_env()
.await
.expect("Failed to setup test environment");
let result = service.list_htlcs().await;
assert!(result.is_ok(), "list_htlcs failed: {:?}", result.err());
if let Ok(response) = result {
let content = response.content.first().expect("Empty response!");
let res_val: Value = serde_json::from_str(&content.raw.as_text().unwrap().text).unwrap();
let obj = res_val.as_object().unwrap();
assert!(obj.contains_key("htlcs"));
}
}
// Payment Information Tests
#[tokio::test]
async fn test_list_pays() {
let service = setup_test_env()
.await
.expect("Failed to setup test environment");
let result = service.list_pays().await;
assert!(result.is_ok(), "list_pays failed: {:?}", result.err());
if let Ok(response) = result {
let content = response.content.first().expect("Empty response!");
let res_val: Value = serde_json::from_str(&content.raw.as_text().unwrap().text).unwrap();
let obj = res_val.as_object().unwrap();
assert!(obj.contains_key("pays"));
}
}
#[tokio::test]
async fn test_list_send_pays() {
let service = setup_test_env()
.await
.expect("Failed to setup test environment");
let result = service.list_send_pays().await;
assert!(result.is_ok(), "list_send_pays failed: {:?}", result.err());
if let Ok(response) = result {
let content = response.content.first().expect("Empty response!");
let res_val: Value = serde_json::from_str(&content.raw.as_text().unwrap().text).unwrap();
let obj = res_val.as_object().unwrap();
assert!(obj.contains_key("payments"));
}
}
#[tokio::test]
async fn test_list_forwards() {
let service = setup_test_env()
.await
.expect("Failed to setup test environment");
let result = service.list_forwards().await;
assert!(result.is_ok(), "list_forwards failed: {:?}", result.err());
if let Ok(response) = result {
let content = response.content.first().expect("Empty response!");
let res_val: Value = serde_json::from_str(&content.raw.as_text().unwrap().text).unwrap();
let obj = res_val.as_object().unwrap();
assert!(obj.contains_key("forwards"));
}
}
#[tokio::test]
async fn test_list_invoices() {
let service = setup_test_env()
.await
.expect("Failed to setup test environment");
let result = service.list_invoices().await;
assert!(result.is_ok(), "list_invoices failed: {:?}", result.err());
if let Ok(response) = result {
let content = response.content.first().expect("Empty response!");
let res_val: Value = serde_json::from_str(&content.raw.as_text().unwrap().text).unwrap();
let obj = res_val.as_object().unwrap();
assert!(obj.contains_key("invoices"));
}
}
// #[tokio::test]
// async fn test_list_invoice_requests() {
// let service = setup_test_env()
// .await
// .expect("Failed to setup test environment");
// let result = service.list_invoice_requests().await;
// assert!(result.is_ok(), "list_invoice_requests failed: {:?}", result.err());
// if let Ok(response) = result {
// let content = response.content.first().expect("Empty response!");
// let res_val: Value = serde_json::from_str(&content.raw.as_text().unwrap().text).unwrap();
// let obj = res_val.as_object().unwrap();
// assert!(obj.contains_key("invoice_requests"));
// }
// }
// Network Information Tests
#[tokio::test]
async fn test_list_peers() {
let service = setup_test_env()
.await
.expect("Failed to setup test environment");
let result = service.list_peers().await;
assert!(result.is_ok(), "list_peers failed: {:?}", result.err());
if let Ok(response) = result {
let content = response.content.first().expect("Empty response!");
let res_val: Value = serde_json::from_str(&content.raw.as_text().unwrap().text).unwrap();
let obj = res_val.as_object().unwrap();
assert!(obj.contains_key("peers"));
}
}
#[tokio::test]
async fn test_list_nodes() {
let service = setup_test_env()
.await
.expect("Failed to setup test environment");
let result = service.list_nodes().await;
assert!(result.is_ok(), "list_nodes failed: {:?}", result.err());
if let Ok(response) = result {
let content = response.content.first().expect("Empty response!");
let res_val: Value = serde_json::from_str(&content.raw.as_text().unwrap().text).unwrap();
let obj = res_val.as_object().unwrap();
assert!(obj.contains_key("nodes"));
}
}
#[tokio::test]
async fn test_list_funds() {
let service = setup_test_env()
.await
.expect("Failed to setup test environment");
let result = service.list_funds().await;
assert!(result.is_ok(), "list_funds failed: {:?}", result.err());
if let Ok(response) = result {
let content = response.content.first().expect("Empty response!");
let res_val: Value = serde_json::from_str(&content.raw.as_text().unwrap().text).unwrap();
let obj = res_val.as_object().unwrap();
assert!(obj.contains_key("outputs"));
assert!(obj.contains_key("channels"));
}
}
// #[tokio::test]
// async fn test_get_route() {
// let service = setup_test_env()
// .await
// .expect("Failed to setup test environment");
// let result = service.get_route().await;
// assert!(result.is_ok(), "get_route failed: {:?}", result.err());
// if let Ok(response) = result {
// let content = response.content.first().expect("Empty response!");
// let res_val: Value = serde_json::from_str(&content.raw.as_text().unwrap().text).unwrap();
// let obj = res_val.as_object().unwrap();
// assert!(obj.contains_key("route"));
// }
// }
// Offer Information Tests
#[tokio::test]
async fn test_list_offers() {
let service = setup_test_env()
.await
.expect("Failed to setup test environment");
let result = service.list_offers().await;
assert!(result.is_ok(), "list_offers failed: {:?}", result.err());
if let Ok(response) = result {
let content = response.content.first().expect("Empty response!");
let res_val: Value = serde_json::from_str(&content.raw.as_text().unwrap().text).unwrap();
let obj = res_val.as_object().unwrap();
assert!(obj.contains_key("offers"));
}
}
// Database Information Tests
#[tokio::test]
async fn test_list_datastore() {
let service = setup_test_env()
.await
.expect("Failed to setup test environment");
let result = service.list_datastore().await;
assert!(result.is_ok(), "list_datastore failed: {:?}", result.err());
if let Ok(response) = result {
let content = response.content.first().expect("Empty response!");
let res_val: Value = serde_json::from_str(&content.raw.as_text().unwrap().text).unwrap();
let obj = res_val.as_object().unwrap();
assert!(obj.contains_key("datastore"));
}
}
// Bookkeeping Information Tests
#[tokio::test]
async fn test_bkpr_channels_apy() {
let service = setup_test_env()
.await
.expect("Failed to setup test environment");
let result = service.bkpr_channels_apy().await;
assert!(
result.is_ok(),
"bkpr_channels_apy failed: {:?}",
result.err()
);
if let Ok(response) = result {
let content = response.content.first().expect("Empty response!");
let res_val: Value = serde_json::from_str(&content.raw.as_text().unwrap().text).unwrap();
let obj = res_val.as_object().unwrap();
assert!(obj.contains_key("channels_apy"));
}
}
#[tokio::test]
async fn test_bkpr_list_balances() {
let service = setup_test_env()
.await
.expect("Failed to setup test environment");
let result = service.bkpr_list_balances().await;
assert!(
result.is_ok(),
"bkpr_list_balances failed: {:?}",
result.err()
);
if let Ok(response) = result {
let content = response.content.first().expect("Empty response!");
let res_val: Value = serde_json::from_str(&content.raw.as_text().unwrap().text).unwrap();
let obj = res_val.as_object().unwrap();
assert!(obj.contains_key("accounts"));
}
}
#[tokio::test]
async fn test_bkpr_list_income() {
let service = setup_test_env()
.await
.expect("Failed to setup test environment");
let result = service.bkpr_list_income().await;
assert!(
result.is_ok(),
"bkpr_list_income failed: {:?}",
result.err()
);
if let Ok(response) = result {
let content = response.content.first().expect("Empty response!");
let res_val: Value = serde_json::from_str(&content.raw.as_text().unwrap().text).unwrap();
let obj = res_val.as_object().unwrap();
assert!(obj.contains_key("income_events"));
}
}
#[tokio::test]
async fn test_bkpr_list_account_events() {
let service = setup_test_env()
.await
.expect("Failed to setup test environment");
let result = service.bkpr_list_account_events().await;
assert!(
result.is_ok(),
"bkpr_list_account_events failed: {:?}",
result.err()
);
if let Ok(response) = result {
let content = response.content.first().expect("Empty response!");
let res_val: Value = serde_json::from_str(&content.raw.as_text().unwrap().text).unwrap();
let obj = res_val.as_object().unwrap();
assert!(obj.contains_key("events"));
}
}
// Utility Commands Tests
// #[tokio::test]
// async fn test_decode() {
// let service = setup_test_env()
// .await
// .expect("Failed to setup test environment");
// let result = service.decode().await;
// assert!(result.is_ok(), "decode failed: {:?}", result.err());
// if let Ok(response) = result {
// let content = response.content.first().expect("Empty response!");
// let res_val: Value = serde_json::from_str(&content.raw.as_text().unwrap().text).unwrap();
// let obj = res_val.as_object().unwrap();
// assert!(obj.contains_key("type"));
// }
// }
// #[tokio::test]
// async fn test_decode_pay() {
// let service = setup_test_env()
// .await
// .expect("Failed to setup test environment");
// let result = service.decode_pay().await;
// assert!(result.is_ok(), "decode_pay failed: {:?}", result.err());
// if let Ok(response) = result {
// let content = response.content.first().expect("Empty response!");
// let res_val: Value = serde_json::from_str(&content.raw.as_text().unwrap().text).unwrap();
// let obj = res_val.as_object().unwrap();
// assert!(obj.contains_key("currency"));
// assert!(obj.contains_key("created_at"));
// assert!(obj.contains_key("expiry"));
// assert!(obj.contains_key("payee"));
// assert!(obj.contains_key("amount_msat"));
// assert!(obj.contains_key("payment_hash"));
// assert!(obj.contains_key("signature"));
// assert!(obj.contains_key("description"));
// }
// }
// #[tokio::test]
// async fn test_check_message() {
// let service = setup_test_env()
// .await
// .expect("Failed to setup test environment");
// let result = service.check_message().await;
// assert!(result.is_ok(), "check_message failed: {:?}", result.err());
// if let Ok(response) = result {
// let content = response.content.first().expect("Empty response!");
// let res_val: Value = serde_json::from_str(&content.raw.as_text().unwrap().text).unwrap();
// let obj = res_val.as_object().unwrap();
// assert!(obj.contains_key("verified"));
// }
// }
#[tokio::test]
async fn test_feerates() {
let service = setup_test_env()
.await
.expect("Failed to setup test environment");
let result = service.feerates().await;
assert!(result.is_ok(), "feerates failed: {:?}", result.err());
if let Ok(response) = result {
let content = response.content.first().expect("Empty response!");
let res_val: Value = serde_json::from_str(&content.raw.as_text().unwrap().text).unwrap();
let obj = res_val.as_object().unwrap();
assert!(obj.contains_key("perkw"));
assert!(obj.contains_key("perkb"));
assert!(obj.contains_key("onchain_fee_estimates"));
}
}
#[tokio::test]
async fn test_get_log() {
let service = setup_test_env()
.await
.expect("Failed to setup test environment");
let result = service.get_log().await;
assert!(result.is_ok(), "get_log failed: {:?}", result.err());
if let Ok(response) = result {
let content = response.content.first().expect("Empty response!");
let res_val: Value = serde_json::from_str(&content.raw.as_text().unwrap().text).unwrap();
let obj = res_val.as_object().unwrap();
assert!(obj.contains_key("log"));
}
}
```