Skip to content

Commit 00a701b

Browse files
committed
Fix code review: env test races, zsh completions, reserved names, audit logging, OAuth port ordering
Signed-off-by: Avelino <31996+avelino@users.noreply.github.com>
1 parent f7f3db1 commit 00a701b

8 files changed

Lines changed: 90 additions & 20 deletions

File tree

docs/reference/cli.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ mcp add filesystem
262262
Fails if:
263263
- Server not found in registry
264264
- Server already exists in config
265-
- Name is reserved (`search`, `add`, `remove`, `list`, `help`, `version`, `serve`, `logs`, `config`, `completions`)
265+
- Name is reserved (`search`, `add`, `remove`, `update`, `list`, `help`, `version`, `serve`, `logs`, `acl`, `config`, `completions`, `healthcheck`)
266266

267267
### `mcp add --url <url> <name>`
268268

src/audit.rs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -251,9 +251,22 @@ impl AuditLogger {
251251
Utc::now().timestamp_millis(),
252252
uuid::Uuid::new_v4()
253253
);
254-
if let Ok(doc) = serde_json::to_value(&entry) {
255-
if let Ok(db) = writer_pool.acquire() {
256-
let _ = db.put(&key, &doc, None);
254+
match serde_json::to_value(&entry) {
255+
Ok(doc) => match writer_pool.acquire() {
256+
Ok(db) => {
257+
if let Err(e) = db.put(&key, &doc, None) {
258+
tracing::warn!(error = ?e, "failed to write audit entry");
259+
}
260+
}
261+
Err(e) => {
262+
tracing::warn!(
263+
error = format!("{e:#}"),
264+
"failed to acquire db for audit"
265+
);
266+
}
267+
},
268+
Err(e) => {
269+
tracing::warn!(error = %e, "failed to serialize audit entry");
257270
}
258271
}
259272
}

src/auth/oauth.rs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,12 @@ pub async fn run_oauth_flow(server_url: &str) -> Result<String> {
7272

7373
let metadata = discover_auth_server(&key).await?;
7474

75-
let client_id = match get_or_register_client(&key, &metadata).await {
75+
// Bind callback listener BEFORE client registration so the redirect_uri
76+
// in the registration request matches the actual port we're listening on.
77+
let (listener, port) = bind_callback_listener().await?;
78+
let redirect_uri = format!("http://localhost:{port}/callback");
79+
80+
let client_id = match get_or_register_client(&key, &metadata, &redirect_uri).await {
7681
Ok(id) => id,
7782
Err(e) => {
7883
tracing::warn!(error = format!("{e:#}"), "OAuth registration not available");
@@ -83,9 +88,6 @@ pub async fn run_oauth_flow(server_url: &str) -> Result<String> {
8388
let (code_verifier, code_challenge) = generate_pkce();
8489
let state = generate_random_string(32);
8590

86-
let (listener, port) = bind_callback_listener().await?;
87-
let redirect_uri = format!("http://localhost:{port}/callback");
88-
8991
let scopes = metadata.scopes_supported.join(" ");
9092
let mut auth_url = Url::parse(&metadata.authorization_endpoint)?;
9193
auth_url
@@ -235,6 +237,7 @@ async fn discover_auth_server(server_url: &str) -> Result<AuthServerMetadata> {
235237
async fn get_or_register_client(
236238
server_key_str: &str,
237239
metadata: &AuthServerMetadata,
240+
redirect_uri: &str,
238241
) -> Result<String> {
239242
let auth_store = store::load_auth_store()?;
240243
if let Some(reg) = auth_store.clients.get(server_key_str) {
@@ -249,7 +252,7 @@ async fn get_or_register_client(
249252
let http = reqwest::Client::new();
250253
let body = serde_json::json!({
251254
"client_name": "mcp",
252-
"redirect_uris": ["http://localhost:8085/callback"],
255+
"redirect_uris": [redirect_uri],
253256
"grant_types": ["authorization_code", "refresh_token"],
254257
"response_types": ["code"],
255258
"token_endpoint_auth_method": "none"

src/cli/completions.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ _mcp() {{
176176
_describe 'subcommand' subcommands
177177
;;
178178
args)
179-
case $words[1] in
179+
case $words[2] in
180180
config)
181181
_values 'config subcommand' 'path[Show config file path]' 'edit[Open config in editor]'
182182
;;

src/cli/config.rs

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,24 +72,61 @@ fn handle_config_edit() -> Result<()> {
7272
#[cfg(test)]
7373
mod tests {
7474
use super::*;
75+
use std::sync::{Mutex, OnceLock};
76+
77+
fn env_lock() -> &'static Mutex<()> {
78+
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
79+
LOCK.get_or_init(|| Mutex::new(()))
80+
}
81+
82+
struct EnvRestore {
83+
editor: Option<String>,
84+
visual: Option<String>,
85+
}
86+
87+
impl EnvRestore {
88+
fn capture() -> Self {
89+
Self {
90+
editor: std::env::var("EDITOR").ok(),
91+
visual: std::env::var("VISUAL").ok(),
92+
}
93+
}
94+
}
95+
96+
impl Drop for EnvRestore {
97+
fn drop(&mut self) {
98+
match &self.editor {
99+
Some(v) => std::env::set_var("EDITOR", v),
100+
None => std::env::remove_var("EDITOR"),
101+
}
102+
match &self.visual {
103+
Some(v) => std::env::set_var("VISUAL", v),
104+
None => std::env::remove_var("VISUAL"),
105+
}
106+
}
107+
}
75108

76109
#[test]
77110
fn test_resolve_editor_from_env() {
111+
let _guard = env_lock().lock().unwrap();
112+
let _restore = EnvRestore::capture();
78113
std::env::set_var("EDITOR", "nano");
79114
assert_eq!(resolve_editor(), "nano");
80-
std::env::remove_var("EDITOR");
81115
}
82116

83117
#[test]
84118
fn test_resolve_editor_visual_fallback() {
119+
let _guard = env_lock().lock().unwrap();
120+
let _restore = EnvRestore::capture();
85121
std::env::remove_var("EDITOR");
86122
std::env::set_var("VISUAL", "code");
87123
assert_eq!(resolve_editor(), "code");
88-
std::env::remove_var("VISUAL");
89124
}
90125

91126
#[test]
92127
fn test_resolve_editor_default() {
128+
let _guard = env_lock().lock().unwrap();
129+
let _restore = EnvRestore::capture();
93130
std::env::remove_var("EDITOR");
94131
std::env::remove_var("VISUAL");
95132
let editor = resolve_editor();
@@ -102,7 +139,6 @@ mod tests {
102139

103140
#[test]
104141
fn test_config_path_json_output() {
105-
// Verify the JSON mode produces valid JSON with expected fields
106142
let path = config::config_path().unwrap();
107143
let json = serde_json::json!({
108144
"path": path.to_string_lossy(),

src/config.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,16 @@ const RESERVED_NAMES: &[&str] = &[
1313
"search",
1414
"add",
1515
"remove",
16+
"update",
1617
"list",
1718
"help",
1819
"version",
1920
"serve",
2021
"logs",
22+
"acl",
2123
"config",
2224
"completions",
25+
"healthcheck",
2326
];
2427

2528
#[derive(Debug, Default, Deserialize, Clone, PartialEq)]

src/serve/http.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -136,10 +136,14 @@ pub async fn run_http(config: Config, bind_addr: &str, insecure: bool) -> Result
136136
let auth_provider = server_auth::build_auth_provider(&config.server_auth)?;
137137
let acl = config.server_auth.acl.clone();
138138

139-
let pool = crate::db::create_pool(&config.audit).unwrap_or_else(|e| {
140-
tracing::warn!(error = format!("{e:#}"), "failed to create db pool");
139+
let pool = if config.audit.output == crate::audit::AuditOutput::File {
140+
crate::db::create_pool(&config.audit).unwrap_or_else(|e| {
141+
tracing::warn!(error = format!("{e:#}"), "failed to create db pool");
142+
Arc::new(crate::db::DbPool::disabled())
143+
})
144+
} else {
141145
Arc::new(crate::db::DbPool::disabled())
142-
});
146+
};
143147
let audit = AuditLogger::open(&config.audit, pool.clone()).unwrap_or(AuditLogger::Disabled);
144148
let cache_store = ToolCacheStore::new(pool);
145149
let mut server = ProxyServer::new(

src/serve/stdio.rs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,22 @@ use super::discovery::discover_pending_backends;
1414
use super::dispatch::dispatch_request;
1515
use super::proxy::{shutdown_clients_in_parallel, ProxyServer, SharedProxy};
1616

17-
pub async fn run_stdio(config: Config) -> Result<()> {
18-
let pool = crate::db::create_pool(&config.audit).unwrap_or_else(|e| {
19-
tracing::warn!(error = format!("{e:#}"), "failed to create db pool");
17+
pub async fn run_stdio(mut config: Config) -> Result<()> {
18+
// In stdio mode, stdout is the JSON-RPC transport. Redirect audit to stderr
19+
// to avoid interleaving audit JSON lines with protocol responses.
20+
if config.audit.output == crate::audit::AuditOutput::Stdout {
21+
tracing::warn!("audit output=stdout conflicts with stdio transport, redirecting to stderr");
22+
config.audit.output = crate::audit::AuditOutput::Stderr;
23+
}
24+
25+
let pool = if config.audit.output == crate::audit::AuditOutput::File {
26+
crate::db::create_pool(&config.audit).unwrap_or_else(|e| {
27+
tracing::warn!(error = format!("{e:#}"), "failed to create db pool");
28+
Arc::new(crate::db::DbPool::disabled())
29+
})
30+
} else {
2031
Arc::new(crate::db::DbPool::disabled())
21-
});
32+
};
2233
let audit = AuditLogger::open(&config.audit, pool.clone()).unwrap_or(AuditLogger::Disabled);
2334
let cache_store = ToolCacheStore::new(pool);
2435
let mut server = ProxyServer::new(

0 commit comments

Comments
 (0)