diff --git a/Cargo.lock b/Cargo.lock index e681d83..2ca28bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -45,7 +45,20 @@ dependencies = [ [[package]] name = "aikit-agent" version = "0.1.0" -source = "git+https://github.com/goaikit/aikit?rev=ec112be4aaedaa0d08a11b4824d69bf082e2f5a5#ec112be4aaedaa0d08a11b4824d69bf082e2f5a5" +source = "git+https://github.com/goaikit/aikit?rev=435a1132#435a1132859bbfaf2977c0fb0ce070c795525132" +dependencies = [ + "reqwest 0.12.28", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio", + "tracing", +] + +[[package]] +name = "aikit-agent" +version = "0.1.0" +source = "git+https://github.com/goaikit/aikit.git?rev=435a1132859bbfaf2977c0fb0ce070c795525132#435a1132859bbfaf2977c0fb0ce070c795525132" dependencies = [ "reqwest 0.12.28", "serde", @@ -58,9 +71,9 @@ dependencies = [ [[package]] name = "aikit-evals" version = "0.1.0" -source = "git+https://github.com/goaikit/aikit?rev=ec112be4aaedaa0d08a11b4824d69bf082e2f5a5#ec112be4aaedaa0d08a11b4824d69bf082e2f5a5" +source = "git+https://github.com/goaikit/aikit?rev=435a1132#435a1132859bbfaf2977c0fb0ce070c795525132" dependencies = [ - "aikit-sdk", + "aikit-sdk 0.3.0 (git+https://github.com/goaikit/aikit?rev=435a1132)", "async-trait", "base64 0.22.1", "num_cpus", @@ -75,9 +88,29 @@ dependencies = [ [[package]] name = "aikit-sdk" version = "0.3.0" -source = "git+https://github.com/goaikit/aikit?rev=ec112be4aaedaa0d08a11b4824d69bf082e2f5a5#ec112be4aaedaa0d08a11b4824d69bf082e2f5a5" +source = "git+https://github.com/goaikit/aikit?rev=435a1132#435a1132859bbfaf2977c0fb0ce070c795525132" +dependencies = [ + "aikit-agent 0.1.0 (git+https://github.com/goaikit/aikit?rev=435a1132)", + "dirs 6.0.0", + "glob", + "jsonschema", + "reqwest 0.12.28", + "serde", + "serde_json", + "tempfile", + "toml 1.1.2+spec-1.1.0", + "tracing", + "uuid", + "walkdir", + "zip 2.4.2", +] + +[[package]] +name = "aikit-sdk" +version = "0.3.0" +source = "git+https://github.com/goaikit/aikit.git?rev=435a1132859bbfaf2977c0fb0ce070c795525132#435a1132859bbfaf2977c0fb0ce070c795525132" dependencies = [ - "aikit-agent", + "aikit-agent 0.1.0 (git+https://github.com/goaikit/aikit.git?rev=435a1132859bbfaf2977c0fb0ce070c795525132)", "dirs 6.0.0", "glob", "jsonschema", @@ -95,10 +128,10 @@ dependencies = [ [[package]] name = "aikit-skillopt" version = "0.1.0" -source = "git+https://github.com/goaikit/aikit?rev=ec112be4aaedaa0d08a11b4824d69bf082e2f5a5#ec112be4aaedaa0d08a11b4824d69bf082e2f5a5" +source = "git+https://github.com/goaikit/aikit?rev=435a1132#435a1132859bbfaf2977c0fb0ce070c795525132" dependencies = [ "aikit-evals", - "aikit-sdk", + "aikit-sdk 0.3.0 (git+https://github.com/goaikit/aikit?rev=435a1132)", "aikit-textgrad", "anyhow", "async-trait", @@ -108,10 +141,10 @@ dependencies = [ [[package]] name = "aikit-textgrad" version = "0.1.0" -source = "git+https://github.com/goaikit/aikit?rev=ec112be4aaedaa0d08a11b4824d69bf082e2f5a5#ec112be4aaedaa0d08a11b4824d69bf082e2f5a5" +source = "git+https://github.com/goaikit/aikit?rev=435a1132#435a1132859bbfaf2977c0fb0ce070c795525132" dependencies = [ "aikit-evals", - "aikit-sdk", + "aikit-sdk 0.3.0 (git+https://github.com/goaikit/aikit?rev=435a1132)", "anyhow", "async-trait", "serde", @@ -1116,6 +1149,7 @@ name = "cli-framework" version = "0.4.2" source = "git+https://github.com/aroff/cli-framework?rev=76a83e0d57e88b55e5f8c44fd3dffe1444443924#76a83e0d57e88b55e5f8c44fd3dffe1444443924" dependencies = [ + "aikit-sdk 0.3.0 (git+https://github.com/goaikit/aikit.git?rev=435a1132859bbfaf2977c0fb0ce070c795525132)", "ailoop-core", "anyhow", "async-trait", @@ -1852,7 +1886,7 @@ checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" name = "fastskill-cli" version = "0.9.133" dependencies = [ - "aikit-sdk", + "aikit-sdk 0.3.0 (git+https://github.com/goaikit/aikit?rev=435a1132)", "aikit-skillopt", "anyhow", "assert_cmd", diff --git a/Cargo.toml b/Cargo.toml index 1b2db5b..b89e368 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,11 +15,11 @@ repository = "https://github.com/aroff/fastskill" [workspace.dependencies] # aikit-sdk for agent metadata file resolution -aikit-sdk = { git = "https://github.com/goaikit/aikit", rev = "ec112be4aaedaa0d08a11b4824d69bf082e2f5a5" } +aikit-sdk = { git = "https://github.com/goaikit/aikit", rev = "435a1132" } # aikit-evals provides the generic eval runner infrastructure -aikit-evals = { git = "https://github.com/goaikit/aikit", rev = "ec112be4aaedaa0d08a11b4824d69bf082e2f5a5" } +aikit-evals = { git = "https://github.com/goaikit/aikit", rev = "435a1132" } # aikit-skillopt provides the text-gradient skill optimization loop -aikit-skillopt = { git = "https://github.com/goaikit/aikit", rev = "ec112be4aaedaa0d08a11b4824d69bf082e2f5a5" } +aikit-skillopt = { git = "https://github.com/goaikit/aikit", rev = "435a1132" } # Async runtime tokio = { version = "1.52", features = ["rt-multi-thread", "net", "fs", "io-util", "macros", "process"] } diff --git a/crates/fastskill-cli/Cargo.toml b/crates/fastskill-cli/Cargo.toml index 985a65c..29875a2 100644 --- a/crates/fastskill-cli/Cargo.toml +++ b/crates/fastskill-cli/Cargo.toml @@ -18,7 +18,7 @@ path = "src/main.rs" fastskill-core = { path = "../fastskill-core", features = ["filesystem-storage", "registry-publish", "hot-reload"] } # CLI framework for AppBuilder, command registry, MCP server, and doctor -cli-framework = { workspace = true, default-features = false, features = ["mcp-server", "doctor", "testkit"] } +cli-framework = { workspace = true, default-features = false, features = ["mcp-server", "mcp-install", "doctor", "testkit"] } fastskill-evals = { path = "../fastskill-evals" } # aikit-sdk for agent integration diff --git a/tests/cli/mcp_install_e2e_tests.rs b/tests/cli/mcp_install_e2e_tests.rs new file mode 100644 index 0000000..a377b63 --- /dev/null +++ b/tests/cli/mcp_install_e2e_tests.rs @@ -0,0 +1,94 @@ +//! E2E tests for mcp install, mcp register, and mcp list subcommands. +//! +//! These tests execute the CLI binary and verify actual behavior. + +#![allow(clippy::all, clippy::unwrap_used, clippy::expect_used, clippy::panic)] + +use super::snapshot_helpers::run_fastskill_command; +use std::fs; +use tempfile::TempDir; + +#[test] +fn test_mcp_help_lists_install_register_list_serve() { + let result = run_fastskill_command(&["mcp", "--help"], None); + let combined = format!("{}{}", result.stdout, result.stderr); + assert!(combined.contains("install"), "mcp --help should list 'install'"); + assert!(combined.contains("register"), "mcp --help should list 'register'"); + assert!(combined.contains("list"), "mcp --help should list 'list'"); + assert!(combined.contains("serve"), "mcp --help should list 'serve'"); +} + +#[test] +fn test_mcp_install_dry_run_exits_zero_and_prints_output() { + let result = run_fastskill_command( + &["mcp", "install", "--agent", "cursor", "--stdio", "--dry-run"], + None, + ); + assert!(result.success, "dry-run should exit 0; stderr: {}", result.stderr); + let combined = format!("{}{}", result.stdout, result.stderr); + assert!(!combined.is_empty(), "dry-run should produce non-empty output"); +} + +#[test] +fn test_mcp_install_cursor_writes_config_file() { + let temp_dir = TempDir::new().unwrap(); + let result = run_fastskill_command( + &[ + "mcp", "install", + "--agent", "cursor", + "--stdio", + "--scope", "project", + "--overwrite", + ], + Some(temp_dir.path()), + ); + assert!(result.success, "mcp install should exit 0; stderr: {}", result.stderr); + let config_path = temp_dir.path().join(".cursor").join("mcp.json"); + assert!(config_path.exists(), ".cursor/mcp.json should be created"); + let contents = fs::read_to_string(&config_path).unwrap(); + let json: serde_json::Value = serde_json::from_str(&contents).unwrap(); + assert!(json.get("fastskill").is_some(), "mcp.json should contain 'fastskill' key"); + let args = &json["fastskill"]["args"]; + let args_str = args.to_string(); + assert!( + args_str.contains("mcp") && args_str.contains("serve") && args_str.contains("stdio"), + "args should include mcp serve --transport stdio; got: {args_str}" + ); +} + +#[test] +fn test_mcp_install_duplicate_without_overwrite_exits_nonzero() { + let temp_dir = TempDir::new().unwrap(); + // First install + let first = run_fastskill_command( + &[ + "mcp", "install", + "--agent", "cursor", + "--stdio", + "--scope", "project", + "--overwrite", + ], + Some(temp_dir.path()), + ); + assert!(first.success, "first install should succeed; stderr: {}", first.stderr); + + // Second install without --overwrite should fail + let second = run_fastskill_command( + &["mcp", "install", "--agent", "cursor", "--stdio", "--scope", "project"], + Some(temp_dir.path()), + ); + assert!(!second.success, "second install without --overwrite should exit non-zero"); + let combined = format!("{}{}", second.stdout, second.stderr); + assert!( + combined.to_lowercase().contains("already") || combined.to_lowercase().contains("exist"), + "error should mention entry already exists; got: {combined}" + ); +} + +#[test] +fn test_mcp_list_exits_zero() { + let result = run_fastskill_command(&["mcp", "list"], None); + assert!(result.success, "mcp list should exit 0; stderr: {}", result.stderr); + let combined = format!("{}{}", result.stdout, result.stderr); + assert!(!combined.is_empty(), "mcp list should produce output"); +} diff --git a/tests/cli/mod.rs b/tests/cli/mod.rs index f3cbc90..18f5b6a 100644 --- a/tests/cli/mod.rs +++ b/tests/cli/mod.rs @@ -12,6 +12,7 @@ pub mod example_tests; pub mod help_tests; pub mod init_tests; pub mod install_e2e_tests; +pub mod mcp_install_e2e_tests; pub mod install_recursive; pub mod install_tests; pub mod integration_tests; diff --git a/webdocs/integration/claude-code-integration.mdx b/webdocs/integration/claude-code-integration.mdx index 36b4130..4ef4324 100644 --- a/webdocs/integration/claude-code-integration.mdx +++ b/webdocs/integration/claude-code-integration.mdx @@ -64,6 +64,25 @@ directory contains at least one valid `SKILL.md`. Run `fastskill sync --agent cl **Wrong file detected:** If AGENTS.md exists alongside CLAUDE.md, fastskill picks AGENTS.md (it is checked first). Use `--agent claude` to force CLAUDE.md. +## MCP Registration + +Register `fastskill` as an MCP server inside Claude Code with a single command: + +```bash +# Register for the current project (stdio transport, recommended) +fastskill mcp install --agent claude --stdio --scope project --overwrite + +# Register globally (writes to ~/.claude.json) +fastskill mcp install --agent claude --stdio --scope global --overwrite + +# Preview the config change without writing any files +fastskill mcp install --agent claude --stdio --dry-run +``` + +After running the command, reload Claude Code. The tools exposed by `fastskill mcp serve --transport stdio` will be callable from the agent. + +> **Deprecated:** Manually editing `~/.claude.json` or `.mcp.json` to add a `fastskill` MCP entry is deprecated. Use `fastskill mcp install` instead. + ## HTTP API When using `fastskill serve`, the Claude-compatible skills API surface is available at: diff --git a/webdocs/integration/cursor-integration.mdx b/webdocs/integration/cursor-integration.mdx index d787189..d19ca4e 100644 --- a/webdocs/integration/cursor-integration.mdx +++ b/webdocs/integration/cursor-integration.mdx @@ -37,7 +37,23 @@ For semantic search over skills, index the skills directory: fastskill reindex --skills-dir .claude/skills/ ``` -### 3. Cursor integration +### 3. MCP Registration + +Register `fastskill` as an MCP server inside Cursor with a single command: + +```bash +# Register for the current project (stdio transport, recommended) +fastskill mcp install --agent cursor --stdio --scope project --overwrite + +# Preview the config change without writing any files +fastskill mcp install --agent cursor --stdio --dry-run +``` + +After running the command, reload Cursor. The tools exposed by `fastskill mcp serve --transport stdio` will be callable from the agent. + +> **Deprecated:** Manually editing `.cursor/mcp.json` to add a `fastskill` MCP entry is deprecated. Use `fastskill mcp install` instead. + +### 4. Cursor integration Cursor reads AGENTS.md and native skills/agents configuration to discover available skills.