Skip to content

Commit 045181e

Browse files
authored
fix(init): generate mcpls.toml config instead of unsupported --workspace-root flag (#1540)
* fix(init): generate mcpls.toml config instead of unsupported --workspace-root flag mcpls 0.3.4 does not accept --workspace-root; passing it caused an immediate exit with code 2, breaking LSP for all users who configured mcpls via `zeph init`. The wizard now writes .zeph/mcpls.toml with workspace roots, [[workspace.language_extensions]] (works around mcpls serde default-Vec bug where roots-only [workspace] section results in empty extension map), and rust-analyzer [[lsp_servers]] entry. The MCP server entry passes --config .zeph/mcpls.toml instead of --workspace-root pairs. Closes #1534 * ci: add CodeQL scheduled workflow, disable default setup on PRs
1 parent a7d3819 commit 045181e

3 files changed

Lines changed: 103 additions & 23 deletions

File tree

.github/workflows/codeql.yml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
name: CodeQL
2+
3+
on:
4+
schedule:
5+
- cron: "0 0 * * 1"
6+
7+
permissions:
8+
contents: read
9+
security-events: write
10+
11+
jobs:
12+
analyze:
13+
name: Analyze (${{ matrix.language }})
14+
runs-on: ubuntu-latest
15+
timeout-minutes: 60
16+
17+
strategy:
18+
fail-fast: false
19+
matrix:
20+
language: [actions, rust]
21+
22+
steps:
23+
- uses: actions/checkout@v4
24+
25+
- name: Initialize CodeQL
26+
uses: github/codeql-action/init@v3
27+
with:
28+
languages: ${{ matrix.language }}
29+
queries: security-and-quality
30+
31+
- name: Install Rust toolchain
32+
if: matrix.language == 'rust'
33+
uses: dtolnay/rust-toolchain@stable
34+
35+
- name: Build (Rust)
36+
if: matrix.language == 'rust'
37+
run: cargo build --workspace
38+
39+
- name: Perform CodeQL Analysis
40+
uses: github/codeql-action/analyze@v3
41+
with:
42+
category: "/language:${{ matrix.language }}"

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
88

99
### Fixed
1010

11+
- `--init` wizard generated unsupported `--workspace-root` flag for mcpls. The wizard now writes `.zeph/mcpls.toml` (with workspace roots, language extensions, and rust-analyzer LSP server config) and passes `--config .zeph/mcpls.toml` to mcpls instead. Fixes broken LSP setup for all users who configured mcpls via `zeph init`. (#1534)
1112
- Shell command blocklist (`blocked_commands`, `DEFAULT_BLOCKED`, `allow_network = false`) was silently skipped whenever a `PermissionPolicy` was attached to `ShellExecutor` (i.e., in all normal operation with `autonomy_level` set). `find_blocked_command()` now runs unconditionally before the policy check, making it a hard security boundary that cannot be bypassed by any autonomy level or permission policy configuration.
1213

1314
### Added

src/init.rs

Lines changed: 60 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -766,22 +766,13 @@ pub(crate) fn build_config(state: &WizardState) -> Config {
766766
}
767767

768768
if state.mcpls_enabled {
769-
let roots = if state.mcpls_workspace_roots.is_empty() {
770-
vec![".".to_owned()]
771-
} else {
772-
state.mcpls_workspace_roots.clone()
773-
};
774-
// Build args: one "--workspace-root <path>" pair per root.
775-
let args = roots
776-
.iter()
777-
.flat_map(|r| ["--workspace-root".to_owned(), r.clone()])
778-
.collect();
779-
// LSP servers need warmup time — use 60s timeout rather than the MCP default of 30s.
780-
// Language server selection is not passed as args: mcpls auto-detects from project files.
769+
// mcpls 0.3.4 does not support --workspace-root; pass a config file instead.
770+
// Workspace roots and language server settings are written to .zeph/mcpls.toml
771+
// by write_mcpls_config() in step_review_and_write().
781772
config.mcp.servers.push(McpServerConfig {
782773
id: "mcpls".to_owned(),
783774
command: Some("mcpls".to_owned()),
784-
args,
775+
args: vec!["--config".to_owned(), ".zeph/mcpls.toml".to_owned()],
785776
env: std::collections::HashMap::new(),
786777
url: None,
787778
timeout: 60,
@@ -1099,6 +1090,56 @@ fn mcpls_in_path() -> bool {
10991090
.any(|p| p.is_file())
11001091
}
11011092

1093+
/// Writes `.zeph/mcpls.toml` next to `config_path` so that `mcpls --config .zeph/mcpls.toml`
1094+
/// starts with the configured workspace roots and language server definitions.
1095+
///
1096+
/// # Errors
1097+
///
1098+
/// Returns an error if the directory cannot be created or the file cannot be written.
1099+
fn write_mcpls_config(state: &WizardState, config_path: &std::path::Path) -> anyhow::Result<()> {
1100+
let base = config_path
1101+
.parent()
1102+
.unwrap_or_else(|| std::path::Path::new("."));
1103+
let zeph_dir = base.join(".zeph");
1104+
std::fs::create_dir_all(&zeph_dir)?;
1105+
1106+
let roots = if state.mcpls_workspace_roots.is_empty() {
1107+
vec![".".to_owned()]
1108+
} else {
1109+
state.mcpls_workspace_roots.clone()
1110+
};
1111+
1112+
let roots_toml = roots
1113+
.iter()
1114+
.map(|r| format!("\"{}\"", r.replace('\\', "\\\\").replace('"', "\\\"")))
1115+
.collect::<Vec<_>>()
1116+
.join(", ");
1117+
1118+
// Include explicit language_extensions to work around mcpls serde default Vec bug
1119+
// where [workspace] with only `roots` results in an empty extension map.
1120+
let content = format!(
1121+
r#"[workspace]
1122+
roots = [{roots_toml}]
1123+
1124+
[[workspace.language_extensions]]
1125+
language_id = "rust"
1126+
extensions = ["rs"]
1127+
1128+
[[lsp_servers]]
1129+
language_id = "rust"
1130+
command = "rust-analyzer"
1131+
args = []
1132+
file_patterns = ["**/*.rs"]
1133+
"#
1134+
);
1135+
1136+
let mcpls_path = zeph_dir.join("mcpls.toml");
1137+
std::fs::write(&mcpls_path, content)?;
1138+
println!("mcpls config written to {}", mcpls_path.display());
1139+
1140+
Ok(())
1141+
}
1142+
11021143
fn step_lsp_context(state: &mut WizardState) -> anyhow::Result<()> {
11031144
if !state.mcpls_enabled {
11041145
// LSP context injection requires mcpls to be configured.
@@ -1384,6 +1425,10 @@ fn step_review_and_write(state: &WizardState, output: Option<PathBuf>) -> anyhow
13841425
std::fs::write(&path, &toml_str)?;
13851426
println!("Config written to {}", path.display());
13861427

1428+
if state.mcpls_enabled {
1429+
write_mcpls_config(state, &path)?;
1430+
}
1431+
13871432
print_secrets_instructions(state);
13881433
print_next_steps(state, &path);
13891434

@@ -1727,15 +1772,7 @@ mod tests {
17271772
let server = &config.mcp.servers[0];
17281773
assert_eq!(server.id, "mcpls");
17291774
assert_eq!(server.command.as_deref(), Some("mcpls"));
1730-
assert_eq!(
1731-
server.args,
1732-
vec![
1733-
"--workspace-root",
1734-
"./crate-a",
1735-
"--workspace-root",
1736-
"./crate-b"
1737-
]
1738-
);
1775+
assert_eq!(server.args, vec!["--config", ".zeph/mcpls.toml"]);
17391776
assert_eq!(server.timeout, 60);
17401777
// mcpls uses command+args, not an HTTP URL.
17411778
assert!(server.url.is_none());
@@ -1754,7 +1791,7 @@ mod tests {
17541791
let config = build_config(&state);
17551792
assert_eq!(config.mcp.servers.len(), 1);
17561793
let server = &config.mcp.servers[0];
1757-
assert_eq!(server.args, vec!["--workspace-root", "."]);
1794+
assert_eq!(server.args, vec!["--config", ".zeph/mcpls.toml"]);
17581795
}
17591796

17601797
#[test]

0 commit comments

Comments
 (0)