mirror of
https://github.com/storytold/craft-anything.git
synced 2026-11-05 20:47:45 +01:00
f64ae3bf3c
A native, pure-Rust app that shows anyone how a piece of software works, with evidence-backed answers, then helps them rebuild it. It ships a desktop app, a CLI and an MCP server, all driven by one engine. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
57 lines
3.1 KiB
Rust
57 lines
3.1 KiB
Rust
//! Plugins end to end: Craft Anything's own MCP server (`craft-anything-cli mcp`) configured as a
|
|
//! plugin, refreshed, listed and called through the CLI. Everything stays in temp folders.
|
|
|
|
use craft_anything_platform::process::{Output, Process};
|
|
use serde_json::Value;
|
|
use std::time::Duration;
|
|
|
|
fn cli(args: &[&str], config: &std::path::Path) -> Output {
|
|
Process::new(env!("CARGO_BIN_EXE_craft-anything-cli")).args(args).env("CRAFT_ANYTHING_CONFIG_DIR", config).timeout(Duration::from_secs(120)).run().unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn an_mcp_server_becomes_a_toolset_of_proxied_tools() {
|
|
let cfg = tempfile::tempdir().unwrap();
|
|
let projects = tempfile::tempdir().unwrap();
|
|
let exe = env!("CARGO_BIN_EXE_craft-anything-cli");
|
|
std::fs::write(
|
|
cfg.path().join("config.toml"),
|
|
format!("version = 1\n\n[[plugins.mcp]]\nid = \"selfmcp\"\nname = \"Itself\"\ncommand = '{exe}'\nargs = [\"mcp\", \"--all-tools\"]\nconsent = false\n"),
|
|
)
|
|
.unwrap();
|
|
|
|
// Before a refresh nothing is cached, so no proxied tools.
|
|
let list = cli(&["plugins-list", "--format", "json"], cfg.path());
|
|
assert!(list.success, "{}", list.stdout_text());
|
|
let v: Value = serde_json::from_slice(&list.stdout).unwrap();
|
|
assert_eq!(v["result"]["plugins"][0]["id"], "selfmcp");
|
|
assert_eq!(v["result"]["plugins"][0]["cached_tools"].as_array().map(Vec::len), Some(0));
|
|
|
|
// Refreshing starts the server (it's a program, so it needs --allow-run) and caches its tools.
|
|
assert!(!cli(&["plugins-refresh", "--format", "json"], cfg.path()).success, "refused without consent");
|
|
let r = cli(&["plugins-refresh", "--allow-run", "--format", "json"], cfg.path());
|
|
assert!(r.success, "{}", r.stdout_text());
|
|
let v: Value = serde_json::from_slice(&r.stdout).unwrap();
|
|
let tools = v["result"]["refreshed"][0]["tools"].as_array().cloned().unwrap_or_default();
|
|
assert!(tools.iter().any(|t| t == "project_list"), "{v}");
|
|
|
|
// The next start offers them as a toolset of its own.
|
|
let c = cli(&["commands", "--format", "json"], cfg.path());
|
|
let v: Value = serde_json::from_slice(&c.stdout).unwrap();
|
|
let ours = v.as_array().unwrap().iter().find(|c| c["command"] == "selfmcp-project-list").cloned();
|
|
assert_eq!(ours.as_ref().map(|c| c["toolset"].clone()), Some(Value::from("selfmcp")), "{v}");
|
|
|
|
// A plugin's tool may look at anything, so Clean room doesn't run it.
|
|
let dir = projects.path().to_str().unwrap();
|
|
let refused = cli(&["selfmcp-project-list", "--projects-dir", dir, "--format", "json"], cfg.path());
|
|
let v: Value = serde_json::from_slice(&refused.stdout).unwrap();
|
|
assert_eq!(v["error"]["details"]["reason"], "clean_room", "{v}");
|
|
|
|
// In Deep mode a call goes through the server and comes back as evidence from an external service.
|
|
let call = cli(&["--deep", "selfmcp-project-list", "--projects-dir", dir, "--format", "json"], cfg.path());
|
|
assert!(call.success, "{}", call.stdout_text());
|
|
let v: Value = serde_json::from_slice(&call.stdout).unwrap();
|
|
assert_eq!(v["authority"], "outside-service", "{v}");
|
|
assert!(v.to_string().contains("projects_dir"), "{v}");
|
|
}
|