Skip to content

Commit 39e9d92

Browse files
committed
fix(core): allow server to start with no LSP servers configured
When lsp_servers is empty the server starts in protocol-only mode instead of returning NoServersAvailable. MCP tools that require LSP will still fail at call time, but protocol handshake (initialize, list_tools) works without any LSP server present. Update two tests that expected the old NoServersAvailable behavior.
1 parent 8d7021b commit 39e9d92

2 files changed

Lines changed: 48 additions & 52 deletions

File tree

crates/mcpls-core/src/bridge/translator.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2778,13 +2778,14 @@ mod tests {
27782778
assert_eq!(extension_map.get("nu"), Some(&"nushell".to_string()));
27792779
assert_eq!(extension_map.get("rs"), Some(&"rust".to_string()));
27802780

2781+
// serve() starts in protocol-only mode when no LSP servers are configured;
2782+
// it may return a transport error but must not return NoServersAvailable.
27812783
let result = crate::serve(config).await;
2782-
assert!(result.is_err());
2783-
2784-
if let Err(crate::error::Error::NoServersAvailable(msg)) = result {
2785-
assert!(msg.contains("none configured"));
2786-
} else {
2787-
panic!("Expected NoServersAvailable error for empty server config");
2784+
if let Err(ref err) = result {
2785+
assert!(
2786+
!matches!(err, crate::error::Error::NoServersAvailable(_)),
2787+
"serve() must not return NoServersAvailable for empty lsp_servers config"
2788+
);
27882789
}
27892790
}
27902791
}

crates/mcpls-core/src/lib.rs

Lines changed: 41 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -147,45 +147,42 @@ pub async fn serve(config: ServerConfig) -> Result<(), Error> {
147147
applicable_configs.len()
148148
);
149149

150-
// Spawn all servers with graceful degradation
151-
let result = LspServer::spawn_batch(&applicable_configs).await;
152-
153-
// Handle the three possible outcomes
154-
if result.all_failed() {
155-
return Err(Error::AllServersFailedToInit {
156-
count: result.failure_count(),
157-
failures: result.failures,
158-
});
159-
}
150+
if !applicable_configs.is_empty() {
151+
// Spawn all servers with graceful degradation
152+
let result = LspServer::spawn_batch(&applicable_configs).await;
153+
154+
// Handle the three possible outcomes
155+
if result.all_failed() {
156+
return Err(Error::AllServersFailedToInit {
157+
count: result.failure_count(),
158+
failures: result.failures,
159+
});
160+
}
160161

161-
if result.partial_success() {
162-
warn!(
163-
"Partial server initialization: {} succeeded, {} failed",
164-
result.server_count(),
165-
result.failure_count()
166-
);
167-
for failure in &result.failures {
168-
error!("Server initialization failed: {}", failure);
162+
if result.partial_success() {
163+
warn!(
164+
"Partial server initialization: {} succeeded, {} failed",
165+
result.server_count(),
166+
result.failure_count()
167+
);
168+
for failure in &result.failures {
169+
error!("Server initialization failed: {}", failure);
170+
}
169171
}
170-
}
171172

172-
// Check if at least one server successfully initialized
173-
if !result.has_servers() {
174-
return Err(Error::NoServersAvailable(
175-
"none configured or all failed to initialize".to_string(),
176-
));
177-
}
173+
// Register all successfully initialized servers
174+
let server_count = result.server_count();
175+
for (language_id, server) in result.servers {
176+
let client = server.client().clone();
177+
translator.register_client(language_id.clone(), client);
178+
translator.register_server(language_id.clone(), server);
179+
}
178180

179-
// Register all successfully initialized servers
180-
let server_count = result.server_count();
181-
for (language_id, server) in result.servers {
182-
let client = server.client().clone();
183-
translator.register_client(language_id.clone(), client);
184-
translator.register_server(language_id.clone(), server);
181+
info!("Proceeding with {} LSP server(s)", server_count);
182+
} else {
183+
warn!("No applicable LSP servers configured — starting in protocol-only mode");
185184
}
186185

187-
info!("Proceeding with {} LSP server(s)", server_count);
188-
189186
let translator = Arc::new(Mutex::new(translator));
190187

191188
info!("Starting MCP server with rmcp...");
@@ -502,10 +499,12 @@ mod tests {
502499
}
503500

504501
#[tokio::test]
505-
async fn test_serve_fails_with_empty_config() {
502+
async fn test_serve_starts_with_empty_config() {
506503
use crate::config::WorkspaceConfig;
507504

508-
// Create a config with no servers
505+
// Server starts in protocol-only mode when no LSP servers are configured.
506+
// serve() blocks until the MCP transport closes, so it will error with a
507+
// connection/transport error — not NoServersAvailable.
509508
let config = ServerConfig {
510509
workspace: WorkspaceConfig {
511510
roots: vec![PathBuf::from("/tmp/test-workspace")],
@@ -518,17 +517,13 @@ mod tests {
518517

519518
let result = serve(config).await;
520519

521-
assert!(result.is_err());
522-
let err = result.unwrap_err();
523-
524-
// Should return NoServersAvailable because no servers were configured
525-
assert!(
526-
matches!(err, Error::NoServersAvailable(_)),
527-
"Expected NoServersAvailable error, got: {err:?}"
528-
);
529-
530-
if let Error::NoServersAvailable(msg) = err {
531-
assert!(msg.contains("none configured"));
520+
// serve() may succeed or fail with a transport error, but must NOT
521+
// return NoServersAvailable when the config simply has no servers.
522+
if let Err(ref err) = result {
523+
assert!(
524+
!matches!(err, Error::NoServersAvailable(_)),
525+
"serve() must not return NoServersAvailable for empty lsp_servers config"
526+
);
532527
}
533528
}
534529
}

0 commit comments

Comments
 (0)