Skip to content

Commit 35ee06a

Browse files
authored
Merge pull request #26 from r0gue-io/fix/env-key-readme
refactor: custom key
2 parents 4d1f6d0 + 4c1d089 commit 35ee06a

10 files changed

Lines changed: 445 additions & 259 deletions

File tree

.github/workflows/integration-tests.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,5 +102,6 @@ jobs:
102102
- name: Run integration tests
103103
env:
104104
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
105+
PRIVATE_KEY: //Alice
105106
run: |
106107
cargo test --locked --features pop-e2e

README.md

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -409,27 +409,52 @@ Deploys and instantiates a contract.
409409
path: string, // Contract directory or .contract bundle
410410
constructor?: string, // Constructor name (default: "new")
411411
args?: string, // Constructor arguments (space-separated)
412-
suri?: string, // Signing key URI (default: //Alice)
413-
url?: string, // Node WebSocket URL (default: ws://localhost:9944)
414-
dryRun?: boolean, // Dry run without submitting (default: false)
415-
uploadOnly?: boolean // Only upload code, don't instantiate (default: false)
412+
value?: string, // Initial balance to transfer (in tokens)
413+
execute?: boolean, // Submit an extrinsic for execution (default: false)
414+
url?: string // Node WebSocket URL (default: ws://localhost:9944)
416415
}
417416
```
418417

418+
Signing:
419+
- Set `PRIVATE_KEY` in the environment to a dev key URI (e.g. `//Alice`) when `execute=true`.
420+
421+
MCP client config example (recommended):
422+
423+
```toml
424+
[mcp_servers.pop-mcp.env]
425+
PRIVATE_KEY = "//Alice"
426+
```
427+
419428
#### call_contract
420429
Calls a method on a deployed contract.
421430

422431
```typescript
423432
{
433+
path: string, // Contract directory (needed for metadata)
424434
contract: string, // Contract address
425435
message: string, // Method name to call
426436
args?: string, // Method arguments (space-separated)
427-
suri?: string, // Signing key URI (default: //Alice)
428-
url?: string, // Node WebSocket URL (default: ws://localhost:9944)
429-
dryRun?: boolean // Dry run without submitting (default: false)
437+
value?: string, // Value to transfer with the call (in tokens)
438+
execute?: boolean, // Submit an extrinsic for execution (default: false)
439+
url?: string // Node WebSocket URL (default: ws://localhost:9944)
430440
}
431441
```
432442

443+
Signing:
444+
- Set `PRIVATE_KEY` in the environment to a dev key URI (e.g. `//Alice`) when `execute=true`.
445+
446+
MCP client config example (recommended):
447+
448+
```toml
449+
[mcp_servers.pop-mcp.env]
450+
PRIVATE_KEY = "//Alice"
451+
```
452+
453+
Security note:
454+
- Use dev keys only for local networks.
455+
- Prefer environment variables set in the MCP client config over CLI arguments.
456+
- Avoid logging or echoing secrets in tooling output.
457+
433458
### Network Tools
434459

435460
#### pop_up_parachain

src/lib.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,8 @@ pub mod tools;
1111
pub use error::{PopMcpError, PopMcpResult};
1212
pub use executor::PopExecutor;
1313
pub use server::PopMcpServer;
14+
15+
/// SURI from PRIVATE_KEY env var.
16+
pub fn read_private_key_suri() -> Option<String> {
17+
std::env::var("PRIVATE_KEY").ok()
18+
}

src/tools/call/chain.rs

Lines changed: 47 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -35,21 +35,20 @@ pub struct CallChainParams {
3535
#[schemars(description = "Arguments for the call as space-separated values")]
3636
pub args: Option<Vec<String>>,
3737

38-
/// Secret key URI for signing transactions.
39-
#[schemars(
40-
description = "Secret key URI for signing (e.g., '//Alice'). Required for transactions, not for queries/constants."
41-
)]
42-
pub suri: Option<String>,
43-
4438
/// Execute with root origin via sudo pallet.
4539
#[schemars(
4640
description = "Execute with root origin via sudo pallet. Not allowed with metadata=true."
4741
)]
4842
pub sudo: Option<bool>,
4943

44+
/// Submit an extrinsic for on-chain execution.
45+
/// Note: tool-level flag; not passed to Pop CLI.
46+
#[schemars(description = "Submit an extrinsic for on-chain execution")]
47+
pub execute: Option<bool>,
48+
5049
/// Display chain metadata instead of executing a call.
5150
#[schemars(
52-
description = "Display chain metadata. Use alone to list all pallets, or with pallet to show pallet details (extrinsics, storage, constants). Cannot be used with function, args, suri, or sudo."
51+
description = "Display chain metadata. Use alone to list all pallets, or with pallet to show pallet details (extrinsics, storage, constants). Cannot be used with function, args, sudo, or execute."
5352
)]
5453
pub metadata: Option<bool>,
5554
}
@@ -67,12 +66,12 @@ impl CallChainParams {
6766
if self.args.is_some() {
6867
return Err("Cannot use 'args' with metadata=true".to_owned());
6968
}
70-
if self.suri.is_some() {
71-
return Err("Cannot use 'suri' with metadata=true".to_owned());
72-
}
7369
if self.sudo.unwrap_or(false) {
7470
return Err("Cannot use 'sudo' with metadata=true".to_owned());
7571
}
72+
if self.execute.unwrap_or(false) {
73+
return Err("Cannot use 'execute' with metadata=true".to_owned());
74+
}
7675
} else {
7776
// In call mode, pallet and function are required
7877
if self.pallet.is_none() {
@@ -81,6 +80,9 @@ impl CallChainParams {
8180
if self.function.is_none() {
8281
return Err("'function' is required when metadata is not set".to_owned());
8382
}
83+
if self.sudo.unwrap_or(false) && !self.execute.unwrap_or(false) {
84+
return Err("'execute' must be true when sudo=true".to_owned());
85+
}
8486
}
8587

8688
Ok(())
@@ -121,11 +123,6 @@ fn build_call_chain_args(params: &CallChainParams) -> Vec<String> {
121123
}
122124
}
123125

124-
if let Some(ref suri) = params.suri {
125-
args.push("--suri".to_owned());
126-
args.push(suri.clone());
127-
}
128-
129126
if params.sudo.unwrap_or(false) {
130127
args.push("--sudo".to_owned());
131128
}
@@ -156,10 +153,23 @@ fn is_error_output(output: &str) -> bool {
156153
pub fn call_chain(executor: &PopExecutor, params: CallChainParams) -> PopMcpResult<CallToolResult> {
157154
params.validate().map_err(PopMcpError::InvalidInput)?;
158155

159-
let args = build_call_chain_args(&params);
160-
let args_refs: Vec<&str> = args.iter().map(String::as_str).collect();
161-
162156
let metadata_mode = params.metadata.unwrap_or(false);
157+
// Read suri from PRIVATE_KEY environment variable
158+
let suri = crate::read_private_key_suri();
159+
if params.execute.unwrap_or(false) && suri.is_none() {
160+
return Err(PopMcpError::InvalidInput(
161+
"PRIVATE_KEY environment variable is required when execute=true".to_owned(),
162+
));
163+
}
164+
165+
let mut args = build_call_chain_args(&params);
166+
if !metadata_mode && params.execute.unwrap_or(false) {
167+
if let Some(suri) = suri {
168+
args.push("--suri".to_owned());
169+
args.push(suri);
170+
}
171+
}
172+
let args_refs: Vec<&str> = args.iter().map(String::as_str).collect();
163173

164174
match executor.execute(&args_refs) {
165175
Ok(output) => {
@@ -200,8 +210,8 @@ mod tests {
200210
pallet: Some("system".to_owned()),
201211
function: Some("account".to_owned()),
202212
args: None,
203-
suri: None,
204213
sudo: None,
214+
execute: None,
205215
metadata: Some(true),
206216
};
207217
assert!(params.validate().is_err());
@@ -214,36 +224,36 @@ mod tests {
214224
pallet: Some("system".to_owned()),
215225
function: None,
216226
args: Some(vec!["arg1".to_owned()]),
217-
suri: None,
218227
sudo: None,
228+
execute: None,
219229
metadata: Some(true),
220230
};
221231
assert!(params.validate().is_err());
222232
}
223233

224234
#[test]
225-
fn validate_rejects_suri_with_metadata() {
235+
fn validate_rejects_sudo_with_metadata() {
226236
let params = CallChainParams {
227237
url: "ws://localhost:9944".to_owned(),
228238
pallet: None,
229239
function: None,
230240
args: None,
231-
suri: Some("//Alice".to_owned()),
232-
sudo: None,
241+
sudo: Some(true),
242+
execute: None,
233243
metadata: Some(true),
234244
};
235245
assert!(params.validate().is_err());
236246
}
237247

238248
#[test]
239-
fn validate_rejects_sudo_with_metadata() {
249+
fn validate_rejects_execute_with_metadata() {
240250
let params = CallChainParams {
241251
url: "ws://localhost:9944".to_owned(),
242252
pallet: None,
243253
function: None,
244254
args: None,
245-
suri: None,
246-
sudo: Some(true),
255+
sudo: None,
256+
execute: Some(true),
247257
metadata: Some(true),
248258
};
249259
assert!(params.validate().is_err());
@@ -256,8 +266,8 @@ mod tests {
256266
pallet: None,
257267
function: Some("account".to_owned()),
258268
args: None,
259-
suri: None,
260269
sudo: None,
270+
execute: None,
261271
metadata: None,
262272
};
263273
assert!(params.validate().is_err());
@@ -270,8 +280,8 @@ mod tests {
270280
pallet: Some("system".to_owned()),
271281
function: None,
272282
args: None,
273-
suri: None,
274283
sudo: None,
284+
execute: None,
275285
metadata: None,
276286
};
277287
assert!(params.validate().is_err());
@@ -284,8 +294,8 @@ mod tests {
284294
pallet: None,
285295
function: None,
286296
args: None,
287-
suri: None,
288297
sudo: None,
298+
execute: None,
289299
metadata: Some(true),
290300
};
291301
assert!(params.validate().is_ok());
@@ -298,8 +308,8 @@ mod tests {
298308
pallet: Some("system".to_owned()),
299309
function: None,
300310
args: None,
301-
suri: None,
302311
sudo: None,
312+
execute: None,
303313
metadata: Some(true),
304314
};
305315
assert!(params.validate().is_ok());
@@ -312,8 +322,8 @@ mod tests {
312322
pallet: Some("system".to_owned()),
313323
function: Some("remark".to_owned()),
314324
args: Some(vec!["0x1234".to_owned()]),
315-
suri: Some("//Alice".to_owned()),
316325
sudo: None,
326+
execute: None,
317327
metadata: None,
318328
};
319329
assert!(params.validate().is_ok());
@@ -326,8 +336,8 @@ mod tests {
326336
pallet: None,
327337
function: None,
328338
args: None,
329-
suri: None,
330339
sudo: None,
340+
execute: None,
331341
metadata: Some(true),
332342
};
333343
let args = build_call_chain_args(&params);
@@ -350,8 +360,8 @@ mod tests {
350360
pallet: Some("System".to_owned()),
351361
function: None,
352362
args: None,
353-
suri: None,
354363
sudo: None,
364+
execute: None,
355365
metadata: Some(true),
356366
};
357367
let args = build_call_chain_args(&params);
@@ -378,8 +388,8 @@ mod tests {
378388
args: Some(vec![
379389
"5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY".to_owned()
380390
]),
381-
suri: None,
382391
sudo: None,
392+
execute: None,
383393
metadata: None,
384394
};
385395
let args = build_call_chain_args(&params);
@@ -402,14 +412,14 @@ mod tests {
402412
}
403413

404414
#[test]
405-
fn build_args_transaction_with_sudo() {
415+
fn build_args_transaction() {
406416
let params = CallChainParams {
407417
url: "ws://localhost:9944".to_owned(),
408418
pallet: Some("system".to_owned()),
409419
function: Some("remark".to_owned()),
410420
args: Some(vec!["0x1234".to_owned()]),
411-
suri: Some("//Alice".to_owned()),
412421
sudo: Some(true),
422+
execute: Some(true),
413423
metadata: None,
414424
};
415425
let args = build_call_chain_args(&params);
@@ -426,8 +436,6 @@ mod tests {
426436
"remark",
427437
"--args",
428438
"0x1234",
429-
"--suri",
430-
"//Alice",
431439
"--sudo",
432440
"-y"
433441
]
@@ -441,8 +449,8 @@ mod tests {
441449
pallet: Some("balances".to_owned()),
442450
function: Some("ExistentialDeposit".to_owned()),
443451
args: None,
444-
suri: None,
445452
sudo: None,
453+
execute: None,
446454
metadata: None,
447455
};
448456
let args = build_call_chain_args(&params);

0 commit comments

Comments
 (0)