fix(mcp): bootstrap desktop live server
This commit is contained in:
parent
ce813bcb3e
commit
f1b7e0989c
|
|
@ -230,6 +230,10 @@ impl ApplicationHandler for DesktopApp {
|
|||
// first paint so the launch document shows immediately.
|
||||
self.drain_opened_files();
|
||||
|
||||
if self.bootstrap_mcp_runtime_from_settings() {
|
||||
settings_io::save(self.host.editor_state());
|
||||
}
|
||||
|
||||
if self.try_init_render_context(event_loop) {
|
||||
if let (Some(ctx), Some(backend)) = (self.ctx.as_mut(), self.backend.as_mut()) {
|
||||
frame::paint(
|
||||
|
|
|
|||
|
|
@ -126,3 +126,33 @@ fn live_mcp_http_server_applies_write_requests_to_editor_state() {
|
|||
"MCP write should mutate the live editor state"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn startup_mcp_bootstrap_starts_live_server_for_enabled_cli() {
|
||||
let mut app = DesktopApp::new(None);
|
||||
let settings = &mut app.host.editor_state_mut().editor_ui.agent_settings;
|
||||
settings.mcp_server.port = 0;
|
||||
settings.mcp_server.running = false;
|
||||
let codex_idx = op_editor_core::agent_settings::McpCli::ALL
|
||||
.iter()
|
||||
.position(|cli| *cli == op_editor_core::agent_settings::McpCli::Codex)
|
||||
.expect("Codex CLI index");
|
||||
settings.mcp_cli_enabled[codex_idx] = true;
|
||||
|
||||
assert!(app.bootstrap_mcp_runtime_from_settings());
|
||||
|
||||
assert!(app.mcp_server_active());
|
||||
assert!(
|
||||
app.host
|
||||
.editor_state()
|
||||
.editor_ui
|
||||
.agent_settings
|
||||
.mcp_server
|
||||
.running
|
||||
);
|
||||
assert_ne!(
|
||||
app.mcp_server.as_ref().expect("server").port(),
|
||||
0,
|
||||
"ephemeral port should be reported after binding"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,13 @@ pub(crate) fn set_cli_enabled(cli: McpCli, enabled: bool, port: u16) -> Result<P
|
|||
set_cli_enabled_at_path(cli, enabled, port, path)
|
||||
}
|
||||
|
||||
pub(crate) fn detect_enabled_clis() -> [bool; 6] {
|
||||
let Some(home) = dirs::home_dir() else {
|
||||
return [false; 6];
|
||||
};
|
||||
detect_enabled_clis_for_home(&home, true)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn set_cli_enabled_at_home(
|
||||
cli: McpCli,
|
||||
|
|
@ -29,6 +36,20 @@ fn set_cli_enabled_at_home(
|
|||
set_cli_enabled_at_path(cli, enabled, port, path)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn detect_enabled_clis_at_home(home: &Path) -> [bool; 6] {
|
||||
detect_enabled_clis_for_home(home, false)
|
||||
}
|
||||
|
||||
fn detect_enabled_clis_for_home(home: &Path, use_env: bool) -> [bool; 6] {
|
||||
let mut flags = [false; 6];
|
||||
for (idx, cli) in McpCli::ALL.iter().copied().enumerate() {
|
||||
let path = config_path(cli, home, use_env);
|
||||
flags[idx] = cli_config_has_openpencil(cli, &path);
|
||||
}
|
||||
flags
|
||||
}
|
||||
|
||||
fn set_cli_enabled_at_path(
|
||||
cli: McpCli,
|
||||
enabled: bool,
|
||||
|
|
@ -46,6 +67,19 @@ fn set_cli_enabled_at_path(
|
|||
Ok(path)
|
||||
}
|
||||
|
||||
fn cli_config_has_openpencil(cli: McpCli, path: &Path) -> bool {
|
||||
match cli {
|
||||
McpCli::Codex => fs::read_to_string(path)
|
||||
.map(|text| codex_config_has_openpencil(&text))
|
||||
.unwrap_or(false),
|
||||
McpCli::ClaudeCode
|
||||
| McpCli::Gemini
|
||||
| McpCli::OpenCode
|
||||
| McpCli::Kiro
|
||||
| McpCli::GithubCopilot => json_config_has_openpencil(path),
|
||||
}
|
||||
}
|
||||
|
||||
fn config_path(cli: McpCli, home: &Path, use_env: bool) -> PathBuf {
|
||||
match cli {
|
||||
McpCli::ClaudeCode => home.join(".claude.json"),
|
||||
|
|
@ -66,6 +100,17 @@ fn config_path(cli: McpCli, home: &Path, use_env: bool) -> PathBuf {
|
|||
}
|
||||
}
|
||||
|
||||
fn json_config_has_openpencil(path: &Path) -> bool {
|
||||
read_json_object(path)
|
||||
.ok()
|
||||
.and_then(|root| {
|
||||
root.get("mcpServers")
|
||||
.and_then(Value::as_object)
|
||||
.map(|servers| servers.contains_key(SERVER_NAME))
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn update_json_config(path: &Path, enabled: bool, port: u16) -> Result<(), String> {
|
||||
let mut root = read_json_object(path)?;
|
||||
if enabled {
|
||||
|
|
@ -145,6 +190,10 @@ fn update_codex_config(path: &Path, enabled: bool, port: u16) -> Result<(), Stri
|
|||
fs::write(path, text).map_err(|e| format!("write {}: {e}", path.display()))
|
||||
}
|
||||
|
||||
fn codex_config_has_openpencil(input: &str) -> bool {
|
||||
input.lines().map(str::trim).any(is_codex_openpencil_table)
|
||||
}
|
||||
|
||||
fn remove_codex_server_block(input: &str) -> String {
|
||||
let mut out = String::new();
|
||||
let mut skipping = false;
|
||||
|
|
@ -255,4 +304,30 @@ mod tests {
|
|||
|
||||
let _ = fs::remove_dir_all(home);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_legacy_codex_openpencil_server_config() {
|
||||
let home = temp_home("codex-detect");
|
||||
let path = home.join(".codex").join("config.toml");
|
||||
fs::create_dir_all(path.parent().expect("parent")).expect("create codex dir");
|
||||
fs::write(
|
||||
&path,
|
||||
"model = \"gpt-5\"\n\n[mcp_servers.openpencil]\ncommand = \"/usr/local/bin/node\"\nargs = [\"/Applications/OpenPencil.app/Contents/Resources/mcp-server.cjs\"]\n",
|
||||
)
|
||||
.expect("seed legacy config");
|
||||
|
||||
let flags = detect_enabled_clis_at_home(&home);
|
||||
|
||||
let codex_idx = McpCli::ALL
|
||||
.iter()
|
||||
.position(|cli| *cli == McpCli::Codex)
|
||||
.expect("Codex CLI index");
|
||||
assert!(flags[codex_idx]);
|
||||
assert!(
|
||||
flags.iter().filter(|enabled| **enabled).count() == 1,
|
||||
"{flags:?}"
|
||||
);
|
||||
|
||||
let _ = fs::remove_dir_all(home);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,10 @@ impl McpLiveServer {
|
|||
pub(crate) fn start(port: u16) -> Result<Self, String> {
|
||||
let listener = TcpListener::bind(("127.0.0.1", port))
|
||||
.map_err(|e| format!("bind 127.0.0.1:{port}: {e}"))?;
|
||||
let bound_port = listener
|
||||
.local_addr()
|
||||
.map_err(|e| format!("read bound MCP port: {e}"))?
|
||||
.port();
|
||||
listener
|
||||
.set_nonblocking(true)
|
||||
.map_err(|e| format!("set nonblocking: {e}"))?;
|
||||
|
|
@ -49,9 +53,9 @@ impl McpLiveServer {
|
|||
.name("op-mcp-live-http".into())
|
||||
.spawn(move || server_loop(listener, req_tx, stop_rx))
|
||||
.map_err(|e| format!("spawn MCP live server: {e}"))?;
|
||||
eprintln!("openpencil-desktop mcp: listening on 127.0.0.1:{port}/mcp");
|
||||
eprintln!("openpencil-desktop mcp: listening on 127.0.0.1:{bound_port}/mcp");
|
||||
Ok(Self {
|
||||
port,
|
||||
port: bound_port,
|
||||
req_rx,
|
||||
stop_tx,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -3,6 +3,35 @@
|
|||
use super::{mcp_integrations, mcp_live, DesktopApp};
|
||||
|
||||
impl DesktopApp {
|
||||
pub(crate) fn bootstrap_mcp_runtime_from_settings(&mut self) -> bool {
|
||||
let detected_flags = mcp_integrations::detect_enabled_clis();
|
||||
let settings = &mut self.host.editor_state_mut().editor_ui.agent_settings;
|
||||
let mut changed = false;
|
||||
for (idx, detected) in detected_flags.iter().copied().enumerate() {
|
||||
if detected && !settings.mcp_cli_enabled[idx] {
|
||||
settings.mcp_cli_enabled[idx] = true;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
let any_cli_enabled = settings.mcp_cli_enabled.iter().any(|enabled| *enabled);
|
||||
let port = settings.mcp_server.port;
|
||||
if any_cli_enabled && !settings.mcp_server.running {
|
||||
settings.mcp_server.running = true;
|
||||
changed = true;
|
||||
}
|
||||
if changed {
|
||||
self.host.mark_editor_state_dirty();
|
||||
}
|
||||
if any_cli_enabled && port != 0 {
|
||||
changed |= self.reconcile_mcp_cli_integrations(Some(([false; 6], port)));
|
||||
}
|
||||
changed |= self.reconcile_mcp_server_from_settings();
|
||||
if self.mcp_server_active() {
|
||||
changed |= self.request_redraw(false);
|
||||
}
|
||||
changed
|
||||
}
|
||||
|
||||
pub(crate) fn reconcile_mcp_server_from_settings(&mut self) -> bool {
|
||||
let desired = self
|
||||
.host
|
||||
|
|
|
|||
Loading…
Reference in a new issue