Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -48,7 +48,8 @@ mcpi [OPTIONS]
|------|-------------|
| `--connect <CMD>` | Connect to a stdio MCP server on startup |
| `--connect-http <URL>` | Connect to an HTTP MCP server on startup |
| `--bearer <TOKEN>` | Bearer token for HTTP `Authorization` header |
| `--bearer <TOKEN>` | Shorthand for `-H "Authorization: Bearer <TOKEN>"` |
| `-H <Name: Value>` | Add an HTTP request header (repeatable) |
| `--live` | Print server notifications immediately instead of buffering them |
| `--timeout <SECS>` | Request timeout in seconds (default: 10) |
| `-e <KEY=VALUE>` | Pass an environment variable to the server process (repeatable) |
Expand All @@ -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"

Expand Down
2 changes: 1 addition & 1 deletion src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<Notification>(256);
let client = McpClient::new(
channels.tx,
Expand Down
25 changes: 22 additions & 3 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ pub struct ConnectionConfig {
pub args: Vec<String>,
pub env: HashMap<String, String>,
pub url: String,
pub bearer_token: Option<String>,
pub headers: HashMap<String, String>,
}

impl Default for ConnectionConfig {
Expand All @@ -32,7 +32,7 @@ impl Default for ConnectionConfig {
args: Vec::new(),
env: HashMap::new(),
url: String::new(),
bearer_token: None,
headers: HashMap::new(),
}
}
}
Expand Down Expand Up @@ -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 => {
Expand Down Expand Up @@ -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]
Expand All @@ -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();
Expand All @@ -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]
Expand Down
25 changes: 24 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ struct Cli {
#[arg(short = 'e', value_name = "KEY=VALUE", action = clap::ArgAction::Append)]
env: Vec<String>,

/// HTTP header to send with requests (\"Name: Value\", repeatable)
#[arg(short = 'H', value_name = "Name: Value", action = clap::ArgAction::Append)]
header: Vec<String>,

/// Bearer token for HTTP Authorization header (use $VARNAME to pass from env)
#[arg(long, value_name = "TOKEN")]
bearer: Option<String>,
Expand All @@ -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 {
Expand All @@ -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 <token>"
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)
Expand Down
9 changes: 5 additions & 4 deletions src/transport/http.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::collections::HashMap;

use anyhow::Result;
use futures::StreamExt;
use reqwest::Client;
Expand All @@ -8,9 +10,8 @@ use crate::transport::TransportChannels;
pub struct HttpTransport;

impl HttpTransport {
pub fn connect(url: String, bearer_token: Option<String>) -> Result<TransportChannels> {
pub fn connect(url: String, headers: HashMap<String, String>) -> Result<TransportChannels> {
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::<String>(64);
Expand All @@ -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) => {
Expand Down
Loading