From d8d704fcc63500427b35c5e57137acf887eadffa Mon Sep 17 00:00:00 2001 From: Alex Oliveira <4482374+aroff@users.noreply.github.com> Date: Sun, 7 Jun 2026 10:03:46 +0000 Subject: [PATCH 1/3] chore(develop): 63-rfc-idea-enable-mcp-serve-mcp-install-expose-mcp-flags-exist --- ailoop-cli/Cargo.toml | 2 +- ailoop-cli/src/cli/forward.rs | 1 - ailoop-cli/tests/cli_tests.rs | 52 +++++++++++++++++++++++++++++++++++ ailoop-py/uv.lock | 2 +- 4 files changed, 54 insertions(+), 3 deletions(-) diff --git a/ailoop-cli/Cargo.toml b/ailoop-cli/Cargo.toml index 2a7349a..058b247 100644 --- a/ailoop-cli/Cargo.toml +++ b/ailoop-cli/Cargo.toml @@ -15,7 +15,7 @@ ailoop-core = { path = "../ailoop-core" } ailoop-server = { path = "../ailoop-server" } # CLI framework -cli-framework = { git = "https://github.com/aroff/cli-framework", features = ["doctor"] } +cli-framework = { git = "https://github.com/aroff/cli-framework", features = ["doctor", "mcp-server", "mcp-install"] } reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } # CLI-specific dependencies diff --git a/ailoop-cli/src/cli/forward.rs b/ailoop-cli/src/cli/forward.rs index 784270d..3a3423f 100644 --- a/ailoop-cli/src/cli/forward.rs +++ b/ailoop-cli/src/cli/forward.rs @@ -4,7 +4,6 @@ use crate::cli::message_converter::MessageConverter; use crate::parser::{create_parser, InputFormat}; use ailoop_core::transport::factory::{create_transport, TransportConfig, TransportType}; use anyhow::{Context, Result}; -use std::io::{self, BufRead, BufReader}; use std::path::PathBuf; use tokio::io::AsyncBufReadExt; diff --git a/ailoop-cli/tests/cli_tests.rs b/ailoop-cli/tests/cli_tests.rs index 0b7daf9..3eafd2e 100644 --- a/ailoop-cli/tests/cli_tests.rs +++ b/ailoop-cli/tests/cli_tests.rs @@ -247,7 +247,59 @@ mod authorize_timeout_tests { stdout ); } +} + +#[cfg(test)] +mod mcp_cli_tests { + use tempfile::TempDir; + + /// `mcp install --agent cursor --stdio --dry-run` must exit 0, print a JSON config + /// containing "ailoop" as a server key, and must NOT create `.cursor/mcp.json`. + #[test] + fn test_mcp_install_dry_run_cursor() { + let tmp = TempDir::new().expect("Failed to create temp dir"); + let cursor_config = tmp.path().join(".cursor").join("mcp.json"); + + let output = std::process::Command::new(env!("CARGO_BIN_EXE_ailoop")) + .args([ + "mcp", + "install", + "--agent", + "cursor", + "--stdio", + "--dry-run", + ]) + .env_remove("AILOOP_SERVER") + .env_remove("AILOOP_MODE") + .current_dir(tmp.path()) + .output() + .expect("Failed to run ailoop"); + + let stdout = String::from_utf8_lossy(&output.stdout).to_string(); + let stderr = String::from_utf8_lossy(&output.stderr).to_string(); + + assert!( + output.status.success(), + "mcp install --dry-run should exit 0, stderr: {}\nstdout: {}", + stderr, + stdout + ); + assert!( + stdout.contains("ailoop"), + "dry-run output should contain 'ailoop' as a server key, got: {}", + stdout + ); + assert!( + !cursor_config.exists(), + ".cursor/mcp.json must NOT be created during --dry-run, but found: {}", + cursor_config.display() + ); + } +} +#[cfg(test)] +mod authorize_timeout_tests_extra { + use super::*; /// Tests the actual InputResult::Timeout path: --default no + timeout fires -> DENIED. /// Stdin pipe is kept open so the process cannot receive EOF; only the countdown /// timer fires, exercising the InputResult::Timeout branch end-to-end. diff --git a/ailoop-py/uv.lock b/ailoop-py/uv.lock index bf99b97..c1d8b3a 100644 --- a/ailoop-py/uv.lock +++ b/ailoop-py/uv.lock @@ -4,7 +4,7 @@ requires-python = ">=3.11" [[package]] name = "ailoop-py" -version = "1.0.10" +version = "1.0.13" source = { editable = "." } dependencies = [ { name = "httpx" }, From af1981c8434022afd18ef5c866b3f97b835ee3f2 Mon Sep 17 00:00:00 2001 From: Alex Oliveira <4482374+aroff@users.noreply.github.com> Date: Sun, 7 Jun 2026 10:13:52 +0000 Subject: [PATCH 2/3] chore(develop): 63-rfc-idea-enable-mcp-serve-mcp-install-expose-mcp-flags-exist --- ailoop-cli/tests/cli_tests.rs | 44 +++++++++++------------------------ 1 file changed, 14 insertions(+), 30 deletions(-) diff --git a/ailoop-cli/tests/cli_tests.rs b/ailoop-cli/tests/cli_tests.rs index 3eafd2e..c69efca 100644 --- a/ailoop-cli/tests/cli_tests.rs +++ b/ailoop-cli/tests/cli_tests.rs @@ -251,48 +251,32 @@ mod authorize_timeout_tests { #[cfg(test)] mod mcp_cli_tests { - use tempfile::TempDir; + use super::*; /// `mcp install --agent cursor --stdio --dry-run` must exit 0, print a JSON config /// containing "ailoop" as a server key, and must NOT create `.cursor/mcp.json`. #[test] fn test_mcp_install_dry_run_cursor() { - let tmp = TempDir::new().expect("Failed to create temp dir"); - let cursor_config = tmp.path().join(".cursor").join("mcp.json"); + let stdout = run_ailoop(&[ + "mcp", + "install", + "--agent", + "cursor", + "--stdio", + "--dry-run", + ]) + .expect("mcp install --dry-run should exit 0"); - let output = std::process::Command::new(env!("CARGO_BIN_EXE_ailoop")) - .args([ - "mcp", - "install", - "--agent", - "cursor", - "--stdio", - "--dry-run", - ]) - .env_remove("AILOOP_SERVER") - .env_remove("AILOOP_MODE") - .current_dir(tmp.path()) - .output() - .expect("Failed to run ailoop"); - - let stdout = String::from_utf8_lossy(&output.stdout).to_string(); - let stderr = String::from_utf8_lossy(&output.stderr).to_string(); - - assert!( - output.status.success(), - "mcp install --dry-run should exit 0, stderr: {}\nstdout: {}", - stderr, - stdout - ); assert!( stdout.contains("ailoop"), "dry-run output should contain 'ailoop' as a server key, got: {}", stdout ); + + // --dry-run must not write any files; .cursor/mcp.json must not exist in CWD assert!( - !cursor_config.exists(), - ".cursor/mcp.json must NOT be created during --dry-run, but found: {}", - cursor_config.display() + !std::path::Path::new(".cursor/mcp.json").exists(), + ".cursor/mcp.json must NOT be created during --dry-run" ); } } From 6be28b2aee8a901a6eea097fd2f18737f7d19e94 Mon Sep 17 00:00:00 2001 From: Alex Oliveira <4482374+aroff@users.noreply.github.com> Date: Sun, 7 Jun 2026 10:49:32 +0000 Subject: [PATCH 3/3] fix(cli): add expose_chat field to Command initializers for cli-framework v0.4 cli-framework added a new required `expose_chat: bool` field to the Command struct (opt-out chat-agent tool visibility). Set it to match each command's expose_mcp value. Fixes E0063 build failures in CI. Co-Authored-By: Claude Opus 4.8 (1M context) --- ailoop-cli/src/main.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/ailoop-cli/src/main.rs b/ailoop-cli/src/main.rs index ea509b5..a374298 100644 --- a/ailoop-cli/src/main.rs +++ b/ailoop-cli/src/main.rs @@ -160,6 +160,7 @@ fn ask_command() -> Command { }), validator: None, expose_mcp: true, + expose_chat: true, execute: Arc::new(|_ctx, args| { Box::pin(async move { let payload = named(&args, "payload"); @@ -196,6 +197,7 @@ fn authorize_command() -> Command { }), validator: None, expose_mcp: true, + expose_chat: true, execute: Arc::new(|_ctx, args| { Box::pin(async move { let action = named(&args, "action"); @@ -232,6 +234,7 @@ fn say_command() -> Command { }), validator: None, expose_mcp: true, + expose_chat: true, execute: Arc::new(|_ctx, args| { Box::pin(async move { let message = named(&args, "message"); @@ -264,6 +267,7 @@ fn serve_command() -> Command { }), validator: None, expose_mcp: false, + expose_chat: false, execute: Arc::new(|_ctx, args| { Box::pin(async move { let host = named_or(&args, "host", "127.0.0.1"); @@ -295,6 +299,7 @@ fn config_command() -> Command { }), validator: None, expose_mcp: false, + expose_chat: false, execute: Arc::new(|_ctx, args| { Box::pin(async move { let init = flag(&args, "init"); @@ -325,6 +330,7 @@ fn image_command() -> Command { }), validator: None, expose_mcp: true, + expose_chat: true, execute: Arc::new(|_ctx, args| { Box::pin(async move { let image_path = named(&args, "image_path"); @@ -352,6 +358,7 @@ fn navigate_command() -> Command { }), validator: None, expose_mcp: true, + expose_chat: true, execute: Arc::new(|_ctx, args| { Box::pin(async move { let url = named(&args, "url"); @@ -391,6 +398,7 @@ fn forward_command() -> Command { }), validator: None, expose_mcp: false, + expose_chat: false, execute: Arc::new(|_ctx, args| { Box::pin(async move { let channel = named_or(&args, "channel", "public"); @@ -426,6 +434,7 @@ fn queue_command() -> Command { }), validator: None, expose_mcp: false, + expose_chat: false, execute: Arc::new(|_ctx, args| { Box::pin(async move { let server = named(&args, "server"); @@ -457,6 +466,7 @@ fn task_create_command() -> Command { }), validator: None, expose_mcp: true, + expose_chat: true, execute: Arc::new(|_ctx, args| { Box::pin(async move { let title = named(&args, "title"); @@ -488,6 +498,7 @@ fn task_list_command() -> Command { }), validator: None, expose_mcp: true, + expose_chat: true, execute: Arc::new(|_ctx, args| { Box::pin(async move { let channel = named_or(&args, "channel", "public"); @@ -517,6 +528,7 @@ fn task_show_command() -> Command { }), validator: None, expose_mcp: true, + expose_chat: true, execute: Arc::new(|_ctx, args| { Box::pin(async move { let task_id = named(&args, "task_id"); @@ -547,6 +559,7 @@ fn task_update_command() -> Command { }), validator: None, expose_mcp: true, + expose_chat: true, execute: Arc::new(|_ctx, args| { Box::pin(async move { let task_id = named(&args, "task_id"); @@ -572,6 +585,7 @@ fn task_ready_command() -> Command { }), validator: None, expose_mcp: true, + expose_chat: true, execute: Arc::new(|_ctx, args| { Box::pin(async move { let channel = named_or(&args, "channel", "public"); @@ -595,6 +609,7 @@ fn task_blocked_command() -> Command { }), validator: None, expose_mcp: true, + expose_chat: true, execute: Arc::new(|_ctx, args| { Box::pin(async move { let channel = named_or(&args, "channel", "public"); @@ -628,6 +643,7 @@ fn task_dep_add_command() -> Command { }), validator: None, expose_mcp: true, + expose_chat: true, execute: Arc::new(|_ctx, args| { Box::pin(async move { let child_id = named(&args, "child_id"); @@ -665,6 +681,7 @@ fn task_dep_remove_command() -> Command { }), validator: None, expose_mcp: true, + expose_chat: true, execute: Arc::new(|_ctx, args| { Box::pin(async move { let child_id = named(&args, "child_id"); @@ -693,6 +710,7 @@ fn task_dep_graph_command() -> Command { }), validator: None, expose_mcp: true, + expose_chat: true, execute: Arc::new(|_ctx, args| { Box::pin(async move { let task_id = named(&args, "task_id"); @@ -722,6 +740,7 @@ fn provider_list_command() -> Command { }), validator: None, expose_mcp: false, + expose_chat: false, execute: Arc::new(|_ctx, args| { Box::pin(async move { let config = named_or(&args, "config", "~/.config/ailoop/config.toml"); @@ -747,6 +766,7 @@ fn provider_telegram_test_command() -> Command { }), validator: None, expose_mcp: false, + expose_chat: false, execute: Arc::new(|_ctx, args| { Box::pin(async move { let config = named_or(&args, "config", "~/.config/ailoop/config.toml");