From 6e99ac39f75b972b73fab4283b7eee75a72a67c7 Mon Sep 17 00:00:00 2001 From: oshea00 Date: Sat, 14 Mar 2026 12:26:45 -0700 Subject: [PATCH 1/2] Header support added to connect and config export - Replace bearer_token with generic headers map in ConnectionConfig - Add -H "Name: Value" CLI flag (repeatable) for arbitrary HTTP headers - --bearer is now shorthand for -H "Authorization: Bearer " - export_config now includes headers in HTTP server entries - Add tests for new header export behavior --- README.md | 8 ++++++-- src/commands.rs | 2 +- src/config.rs | 25 ++++++++++++++++++++++--- src/main.rs | 25 ++++++++++++++++++++++++- src/transport/http.rs | 9 +++++---- 5 files changed, 58 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 771d457..9758593 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ An interactive command-line tool for connecting to and inspecting [Model Context ## Features - Connect to MCP servers via **stdio** (subprocess) or **HTTP/SSE** transports -- Supports Authorization header Bearer tokens via --bearer parameter +- Supports arbitrary HTTP request headers via `-H`; `--bearer` is shorthand for `Authorization: Bearer` - Inspect and invoke **tools**, **resources**, and **prompts** - View server **notifications** in real time or buffered - **Tab completion** for commands, tool names, resource URIs, and prompt names @@ -48,7 +48,8 @@ mcpi [OPTIONS] |------|-------------| | `--connect ` | Connect to a stdio MCP server on startup | | `--connect-http ` | Connect to an HTTP MCP server on startup | -| `--bearer ` | Bearer token for HTTP `Authorization` header | +| `--bearer ` | Shorthand for `-H "Authorization: Bearer "` | +| `-H ` | Add an HTTP request header (repeatable) | | `--live` | Print server notifications immediately instead of buffering them | | `--timeout ` | Request timeout in seconds (default: 10) | | `-e ` | Pass an environment variable to the server process (repeatable) | @@ -71,6 +72,9 @@ mcpi --connect-http http://localhost:3000/mcp # Auto-connect to an HTTP server with a Bearer token mcpi --connect-http http://localhost:3000/mcp --bearer $JWTTOKEN +# Auto-connect with arbitrary HTTP headers +mcpi --connect-http http://localhost:3000/mcp -H "X-MCP-Insiders: true" -H "X-Tenant: acme" + # Print server notifications live as they arrive mcpi --live --connect "npx -y @modelcontextprotocol/server-filesystem /tmp" diff --git a/src/commands.rs b/src/commands.rs index 044b959..8fe3dc9 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -148,7 +148,7 @@ async fn cmd_connect_http(state: &mut ReplState, args: &[String]) -> Result<()> let url = args[0].clone(); display::print_info(&format!("Connecting to '{url}'...")); - let channels = HttpTransport::connect(url.clone(), state.config.bearer_token.clone())?; + let channels = HttpTransport::connect(url.clone(), state.config.headers.clone())?; let (notif_tx, notif_rx) = mpsc::channel::(256); let client = McpClient::new( channels.tx, diff --git a/src/config.rs b/src/config.rs index 079bd21..53313ea 100644 --- a/src/config.rs +++ b/src/config.rs @@ -21,7 +21,7 @@ pub struct ConnectionConfig { pub args: Vec, pub env: HashMap, pub url: String, - pub bearer_token: Option, + pub headers: HashMap, } impl Default for ConnectionConfig { @@ -32,7 +32,7 @@ impl Default for ConnectionConfig { args: Vec::new(), env: HashMap::new(), url: String::new(), - bearer_token: None, + headers: HashMap::new(), } } } @@ -104,6 +104,9 @@ pub fn export_config(state: &ReplState) -> Value { if !config.env.is_empty() { entry["env"] = json!(config.env); } + if !config.headers.is_empty() { + entry["headers"] = json!(config.headers); + } entry } TransportType::Stdio => { @@ -186,6 +189,7 @@ mod tests { assert!(entry.get("url").is_some()); assert!(entry.get("command").is_none()); assert!(entry.get("env").is_none()); + assert!(entry.get("headers").is_none()); } #[test] @@ -203,6 +207,21 @@ mod tests { assert_eq!(entry["env"]["API_KEY"], "secret"); } + #[test] + fn export_config_http_with_headers() { + let mut state = make_state(); + state.config.transport_type = TransportType::Http; + state.config.url = "http://localhost:3000".to_string(); + state + .config + .headers + .insert("X-Custom".to_string(), "value".to_string()); + let v = export_config(&state); + let entry = &v["mcpServers"]["mcp-server"]; + assert!(entry.get("headers").is_some()); + assert_eq!(entry["headers"]["X-Custom"], "value"); + } + #[test] fn export_config_uses_server_name_as_key() { let mut state = make_state(); @@ -220,7 +239,7 @@ mod tests { assert!(cfg.args.is_empty()); assert!(cfg.env.is_empty()); assert!(cfg.url.is_empty()); - assert!(cfg.bearer_token.is_none()); + assert!(cfg.headers.is_empty()); } #[test] diff --git a/src/main.rs b/src/main.rs index 5afe2da..3db8863 100644 --- a/src/main.rs +++ b/src/main.rs @@ -32,6 +32,10 @@ struct Cli { #[arg(short = 'e', value_name = "KEY=VALUE", action = clap::ArgAction::Append)] env: Vec, + /// HTTP header to send with requests (\"Name: Value\", repeatable) + #[arg(short = 'H', value_name = "Name: Value", action = clap::ArgAction::Append)] + header: Vec, + /// Bearer token for HTTP Authorization header (use $VARNAME to pass from env) #[arg(long, value_name = "TOKEN")] bearer: Option, @@ -49,7 +53,6 @@ async fn main() -> Result<()> { let mut state = ReplState::new(completer_state); state.timeout_secs = cli.timeout; state.debug = cli.debug; - state.config.bearer_token = cli.bearer; // Populate env vars from -e KEY=VALUE flags for kv in &cli.env { @@ -60,6 +63,26 @@ async fn main() -> Result<()> { } } + // Populate HTTP headers from -H "Name: Value" flags + for h in &cli.header { + if let Some((k, v)) = h.split_once(':') { + state + .config + .headers + .insert(k.trim().to_string(), v.trim().to_string()); + } else { + eprintln!("Warning: ignoring malformed -H value (expected \"Name: Value\"): {h}"); + } + } + + // --bearer is shorthand for -H "Authorization: Bearer " + if let Some(token) = cli.bearer { + state + .config + .headers + .insert("Authorization".to_string(), format!("Bearer {token}")); + } + // Auto-connect if flags provided if let Some(cmd_str) = &cli.connect { let parts = shell_words::split(cmd_str) diff --git a/src/transport/http.rs b/src/transport/http.rs index e9f3a35..e94151c 100644 --- a/src/transport/http.rs +++ b/src/transport/http.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use anyhow::Result; use futures::StreamExt; use reqwest::Client; @@ -8,9 +10,8 @@ use crate::transport::TransportChannels; pub struct HttpTransport; impl HttpTransport { - pub fn connect(url: String, bearer_token: Option) -> Result { + pub fn connect(url: String, headers: HashMap) -> Result { let client = Client::new(); - let _url_clone = url.clone(); // Outgoing: POST each JSON-RPC message to the endpoint let (out_tx, mut out_rx) = mpsc::channel::(64); @@ -29,8 +30,8 @@ impl HttpTransport { }; let mut req = client_clone.post(&post_url).json(&body); - if let Some(token) = &bearer_token { - req = req.header("Authorization", format!("Bearer {token}")); + for (key, value) in &headers { + req = req.header(key.as_str(), value.as_str()); } match req.send().await { Ok(resp) => { From 131e7964050d6268b879077f5655691622063f9b Mon Sep 17 00:00:00 2001 From: oshea00 Date: Sat, 14 Mar 2026 12:29:37 -0700 Subject: [PATCH 2/2] bump version to 0.1.5 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index f92162b..a454bf2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mcpi" -version = "0.1.4" +version = "0.1.5" edition = "2021" description = "A command-line tool REPL for inspecting MCP servers" license = "MIT"