Skip to content

Commit 4e87caf

Browse files
authored
Merge pull request #5 from Moonsong-Labs/notlesh/add-subxt-tool
Add subxt tool
2 parents a5afdbf + 8363124 commit 4e87caf

File tree

15 files changed

+6856
-1081
lines changed

15 files changed

+6856
-1081
lines changed

Cargo.lock

Lines changed: 5200 additions & 1074 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,26 @@ version = "0.1.0"
44
edition = "2021"
55

66
[dependencies]
7+
rmcp = { version = "0.3.2", features = ["transport-io"] }
78
indoc = "2.0.6"
8-
rmcp = { version = "0.3.0", features = ["transport-io"] }
99
tokio = { version = "1", features = ["full"] }
1010
serde = { version = "1", features = ["derive"] }
1111
serde_json = "1"
1212
anyhow = "1"
1313
reqwest = { version = "0.12", features = ["json"] }
1414

1515
# For RPC client functionality
16-
jsonrpsee = { version = "0.20", features = ["http-client", "macros"] }
16+
jsonrpsee = { version = "0.20", features = ["http-client", "ws-client", "macros"] }
1717
codec = { package = "parity-scale-codec", version = "3.6", features = ["derive"] }
1818
futures = "0.3"
1919
hex = "0.4"
2020

2121
# Logging
2222
log = "0.4"
2323
env_logger = "0.11"
24+
25+
# Subxt for advanced chain interaction
26+
subxt = { version = "0.37", features = ["substrate-compat"] }
27+
subxt-signer = { version = "0.37" }
28+
scale-value = "0.16"
29+

README.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,67 @@ cargo build --release
1616
```
1717
The binary will be available at `./target/release/substrate-mcp`
1818

19+
## Prerequisites
20+
21+
For the `subxt_execute` tool, install the subxt CLI:
22+
```bash
23+
cargo install subxt-cli
24+
```
25+
26+
## Configuration
27+
28+
### RPC Endpoints
29+
The server uses a `rpc_endpoints.json` configuration file to manage RPC endpoints. This file is automatically created with default endpoints if it doesn't exist.
30+
31+
The configuration includes:
32+
- **Network name**: Human and LLM-friendly identifier (e.g., "polkadot", "kusama")
33+
- **URL**: WebSocket endpoint URL
34+
- **Description**: Detailed description of the network
35+
36+
Example configuration:
37+
```json
38+
{
39+
"endpoints": [
40+
{
41+
"name": "polkadot",
42+
"url": "wss://rpc.polkadot.io",
43+
"description": "Polkadot mainnet - The main Polkadot relay chain"
44+
},
45+
{
46+
"name": "westend",
47+
"url": "wss://westend-rpc.polkadot.io",
48+
"description": "Westend testnet - Public test network for Polkadot"
49+
},
50+
{
51+
"name": "local",
52+
"url": "ws://localhost:9944",
53+
"description": "Local development node - Substrate node running on your machine"
54+
}
55+
],
56+
"default_endpoint": "westend"
57+
}
58+
```
59+
60+
You can customize this file to add your own endpoints or modify existing ones.
61+
62+
## Available Tools
63+
64+
### Event Querying
65+
- **`query_events`** - Query and filter blockchain events within a specified block range. Supports filtering by pallet and event name with partial matching.
66+
- **`query_historical_events`** - Query events from historical blocks. Supports relative block numbers (e.g., -10 for 10 blocks ago).
67+
68+
### Storage Querying
69+
- **`query_storage`** - Query chain storage entries by pallet and storage name. Supports querying map-type storage with keys.
70+
- **`list_pallet_storage`** - List all storage entries available in a specific pallet.
71+
- **`chain_storage_bisect`** - Find all storage changes between two blocks for a specific key.
72+
73+
### Metadata and Chain Exploration
74+
- **`filter_metadata`** - Filter and search chain metadata to discover available pallets, storage entries, calls, events, constants, and errors. Supports partial name matching.
75+
- **`subxt_execute`** - Use subxt CLI to decode and explore Substrate blockchain data. Useful for analyzing chain metadata, generating type-safe Rust code, and understanding runtime APIs.
76+
77+
### Documentation
78+
- **`get_polkadot_sdk_release_prdocs`** - Get all documented changes for a given polkadot-sdk release.
79+
1980
## Usage with Claude Code
2081

2182
To use this MCP server with Claude Code, add it to your Claude Code configuration.

rpc_endpoints.json

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
{
2+
"endpoints": [
3+
{
4+
"name": "polkadot",
5+
"url": "wss://rpc.polkadot.io",
6+
"description": "Polkadot mainnet - The main Polkadot relay chain"
7+
},
8+
{
9+
"name": "kusama",
10+
"url": "wss://kusama-rpc.polkadot.io",
11+
"description": "Kusama canary network - Polkadot's experimental network"
12+
},
13+
{
14+
"name": "westend",
15+
"url": "wss://westend-rpc.polkadot.io",
16+
"description": "Westend testnet - Public test network for Polkadot"
17+
},
18+
{
19+
"name": "rococo",
20+
"url": "wss://rococo-rpc.polkadot.io",
21+
"description": "Rococo testnet - Parachain test network"
22+
},
23+
{
24+
"name": "paseo",
25+
"url": "wss://paseo.rpc.amforc.com",
26+
"description": "Paseo testnet - Community-run testnet (replaced Rococo for parachains)"
27+
},
28+
{
29+
"name": "asset-hub-polkadot",
30+
"url": "wss://polkadot-asset-hub-rpc.polkadot.io",
31+
"description": "Asset Hub on Polkadot - System parachain for asset management"
32+
},
33+
{
34+
"name": "asset-hub-kusama",
35+
"url": "wss://kusama-asset-hub-rpc.polkadot.io",
36+
"description": "Asset Hub on Kusama - System parachain for asset management on Kusama"
37+
},
38+
{
39+
"name": "asset-hub-westend",
40+
"url": "wss://westend-asset-hub-rpc.polkadot.io",
41+
"description": "Asset Hub on Westend - Test system parachain for asset management"
42+
},
43+
{
44+
"name": "local",
45+
"url": "ws://localhost:9944",
46+
"description": "Local development node - Substrate node running on your machine"
47+
}
48+
],
49+
"default_endpoint": "westend"
50+
}

src/config.rs

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
use anyhow::{Context, Result};
2+
use serde::{Deserialize, Serialize};
3+
use std::fs;
4+
use std::path::Path;
5+
6+
#[derive(Debug, Clone, Serialize, Deserialize)]
7+
pub struct RpcEndpoint {
8+
pub name: String,
9+
pub url: String,
10+
pub description: String,
11+
}
12+
13+
#[derive(Debug, Clone, Serialize, Deserialize)]
14+
pub struct Config {
15+
pub endpoints: Vec<RpcEndpoint>,
16+
pub default_endpoint: String,
17+
}
18+
19+
impl Config {
20+
pub fn load_from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
21+
let content = fs::read_to_string(path.as_ref())
22+
.with_context(|| format!("Failed to read config file: {}", path.as_ref().display()))?;
23+
24+
let config: Config =
25+
serde_json::from_str(&content).with_context(|| "Failed to parse config file")?;
26+
27+
config.validate()?;
28+
Ok(config)
29+
}
30+
31+
#[cfg(test)]
32+
pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
33+
let content = serde_json::to_string_pretty(self).context("Failed to serialize config")?;
34+
35+
fs::write(path.as_ref(), content)
36+
.with_context(|| format!("Failed to write config file: {}", path.as_ref().display()))?;
37+
38+
Ok(())
39+
}
40+
41+
pub fn get_endpoint(&self, name: &str) -> Option<&RpcEndpoint> {
42+
self.endpoints.iter().find(|e| e.name == name)
43+
}
44+
45+
pub fn get_endpoint_url(&self, name: &str) -> Option<&str> {
46+
self.get_endpoint(name).map(|e| e.url.as_str())
47+
}
48+
49+
pub fn get_default_endpoint(&self) -> Option<&RpcEndpoint> {
50+
self.get_endpoint(&self.default_endpoint)
51+
}
52+
53+
pub fn get_default_url(&self) -> Option<&str> {
54+
self.get_default_endpoint().map(|e| e.url.as_str())
55+
}
56+
57+
fn validate(&self) -> Result<()> {
58+
if self.endpoints.is_empty() {
59+
anyhow::bail!("Config must contain at least one endpoint");
60+
}
61+
62+
if !self
63+
.endpoints
64+
.iter()
65+
.any(|e| e.name == self.default_endpoint)
66+
{
67+
anyhow::bail!(
68+
"Default endpoint '{}' not found in endpoints list",
69+
self.default_endpoint
70+
);
71+
}
72+
73+
let names: Vec<_> = self.endpoints.iter().map(|e| &e.name).collect();
74+
let unique_names: std::collections::HashSet<_> = names.iter().collect();
75+
if names.len() != unique_names.len() {
76+
anyhow::bail!("Endpoint names must be unique");
77+
}
78+
79+
for endpoint in &self.endpoints {
80+
if endpoint.name.is_empty() {
81+
anyhow::bail!("Endpoint name cannot be empty");
82+
}
83+
if endpoint.url.is_empty() {
84+
anyhow::bail!("Endpoint URL cannot be empty");
85+
}
86+
if !endpoint.url.starts_with("ws://") && !endpoint.url.starts_with("wss://") {
87+
anyhow::bail!(
88+
"Endpoint URL '{}' must start with ws:// or wss://",
89+
endpoint.url
90+
);
91+
}
92+
}
93+
94+
Ok(())
95+
}
96+
}
97+
98+
#[allow(dead_code)]
99+
pub fn chain_name_from_endpoint(endpoint: &str, config: &Config) -> String {
100+
if let Some(rpc_endpoint) = config.endpoints.iter().find(|e| e.url == endpoint) {
101+
return rpc_endpoint.description.clone();
102+
}
103+
104+
match endpoint {
105+
e if e.contains("polkadot") && !e.contains("kusama") && !e.contains("westend") => {
106+
"Polkadot".to_string()
107+
}
108+
e if e.contains("kusama") => "Kusama".to_string(),
109+
e if e.contains("westend") => "Westend".to_string(),
110+
e if e.contains("rococo") => "Rococo".to_string(),
111+
e if e.contains("paseo") => "Paseo".to_string(),
112+
e if e.contains("asset-hub") && e.contains("polkadot") => "Asset Hub Polkadot".to_string(),
113+
e if e.contains("asset-hub") && e.contains("kusama") => "Asset Hub Kusama".to_string(),
114+
e if e.contains("asset-hub") && e.contains("westend") => "Asset Hub Westend".to_string(),
115+
_ => "Custom Chain".to_string(),
116+
}
117+
}
118+
119+
#[cfg(test)]
120+
mod tests {
121+
use super::*;
122+
use std::fs;
123+
use std::path::PathBuf;
124+
125+
#[test]
126+
fn test_load_existing_config() {
127+
// Test loading the actual config file
128+
let config = Config::load_from_file("rpc_endpoints.json");
129+
assert!(config.is_ok(), "Should be able to load rpc_endpoints.json");
130+
131+
let config = config.unwrap();
132+
assert!(!config.endpoints.is_empty());
133+
assert!(!config.default_endpoint.is_empty());
134+
assert!(config.get_endpoint(&config.default_endpoint).is_some());
135+
}
136+
137+
#[test]
138+
fn test_config_validation() {
139+
// Create a test config manually
140+
let mut config = Config {
141+
endpoints: vec![],
142+
default_endpoint: "test".to_string(),
143+
};
144+
145+
// Test empty endpoints
146+
assert!(config.validate().is_err());
147+
148+
// Test invalid default endpoint
149+
config.endpoints = vec![RpcEndpoint {
150+
name: "test".to_string(),
151+
url: "wss://test.com".to_string(),
152+
description: "Test".to_string(),
153+
}];
154+
config.default_endpoint = "nonexistent".to_string();
155+
assert!(config.validate().is_err());
156+
157+
// Test duplicate names
158+
config.endpoints.push(RpcEndpoint {
159+
name: "test".to_string(),
160+
url: "wss://test2.com".to_string(),
161+
description: "Test 2".to_string(),
162+
});
163+
config.default_endpoint = "test".to_string();
164+
assert!(config.validate().is_err());
165+
}
166+
167+
#[test]
168+
fn test_save_and_load() {
169+
let temp_file = PathBuf::from("test_config_temp.json");
170+
let config = Config {
171+
endpoints: vec![RpcEndpoint {
172+
name: "test".to_string(),
173+
url: "wss://test.com".to_string(),
174+
description: "Test endpoint".to_string(),
175+
}],
176+
default_endpoint: "test".to_string(),
177+
};
178+
179+
// Save config
180+
assert!(config.save_to_file(&temp_file).is_ok());
181+
182+
// Load config
183+
let loaded = Config::load_from_file(&temp_file).unwrap();
184+
assert_eq!(loaded.endpoints.len(), config.endpoints.len());
185+
assert_eq!(loaded.default_endpoint, config.default_endpoint);
186+
assert_eq!(loaded.endpoints[0].name, config.endpoints[0].name);
187+
188+
// Clean up
189+
fs::remove_file(temp_file).ok();
190+
}
191+
}

src/lib.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
pub mod config;
2+
pub mod polkadot_sdk_releases;
3+
pub mod prompts;
4+
pub mod public_endpoints;
5+
pub mod resources;
6+
pub mod server;
7+
pub mod substrate;

src/main.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ use anyhow::Result;
22
use rmcp::service::ServiceExt;
33
use tokio::io::{stdin, stdout};
44

5+
mod config;
56
mod polkadot_sdk_releases;
67
mod prompts;
8+
mod public_endpoints;
79
mod resources;
810
mod server;
911
mod substrate;

src/public_endpoints.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/// Public RPC endpoints for various Substrate-based chains
2+
#[allow(dead_code)]
3+
pub mod endpoints {
4+
/// Polkadot mainnet
5+
pub const POLKADOT: &str = "wss://rpc.polkadot.io";
6+
7+
/// Kusama canary network
8+
pub const KUSAMA: &str = "wss://kusama-rpc.polkadot.io";
9+
10+
/// Westend testnet
11+
pub const WESTEND: &str = "wss://westend-rpc.polkadot.io";
12+
13+
/// Rococo testnet
14+
pub const ROCOCO: &str = "wss://rococo-rpc.polkadot.io";
15+
16+
/// Paseo testnet (replaced Rococo for parachains)
17+
pub const PASEO: &str = "wss://paseo.rpc.amforc.com";
18+
19+
/// Asset Hub on Polkadot
20+
pub const ASSET_HUB_POLKADOT: &str = "wss://polkadot-asset-hub-rpc.polkadot.io";
21+
22+
/// Asset Hub on Kusama
23+
pub const ASSET_HUB_KUSAMA: &str = "wss://kusama-asset-hub-rpc.polkadot.io";
24+
25+
/// Asset Hub on Westend
26+
pub const ASSET_HUB_WESTEND: &str = "wss://westend-asset-hub-rpc.polkadot.io";
27+
28+
/// Default endpoint (Westend testnet is a good default for testing)
29+
pub const DEFAULT: &str = WESTEND;
30+
}

0 commit comments

Comments
 (0)