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/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"); diff --git a/ailoop-cli/tests/cli_tests.rs b/ailoop-cli/tests/cli_tests.rs index 0b7daf9..c69efca 100644 --- a/ailoop-cli/tests/cli_tests.rs +++ b/ailoop-cli/tests/cli_tests.rs @@ -247,7 +247,43 @@ mod authorize_timeout_tests { stdout ); } +} +#[cfg(test)] +mod mcp_cli_tests { + 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 stdout = run_ailoop(&[ + "mcp", + "install", + "--agent", + "cursor", + "--stdio", + "--dry-run", + ]) + .expect("mcp install --dry-run should exit 0"); + + 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!( + !std::path::Path::new(".cursor/mcp.json").exists(), + ".cursor/mcp.json must NOT be created during --dry-run" + ); + } +} + +#[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" },