diff --git a/docker-compose.example.yml b/docker-compose.example.yml index 5457332b..459b78af 100644 --- a/docker-compose.example.yml +++ b/docker-compose.example.yml @@ -44,7 +44,7 @@ services: # Uses the default CMD from Dockerfile - no need to specify # Option 1: Override with stdio transport - # command: ["wassette", "serve", "--stdio"] + # command: ["wassette", "run"] # Option 2: Override with SSE transport # command: ["wassette", "serve", "--sse"] diff --git a/docs/deployment/docker.md b/docs/deployment/docker.md index fb4f2cb0..a9e0d6e7 100644 --- a/docs/deployment/docker.md +++ b/docs/deployment/docker.md @@ -46,7 +46,7 @@ Then connect to `http://localhost:9001` from your MCP client. For use with MCP clients that expect stdio, override the default command: ```bash -docker run -i --rm wassette:latest wassette serve --stdio +docker run -i --rm wassette:latest wassette run ``` ### Run with SSE Transport @@ -89,7 +89,7 @@ docker run --rm -p 9001:9001 \ # For stdio transport, override the default: # docker run -i --rm \ # -v $(pwd)/examples/filesystem-rs/target/wasm32-wasip2/release:/home/wassette/.local/share/wassette/components:ro \ -# wassette:latest wassette serve --stdio +# wassette:latest wassette run ``` ### Example: Running with Multiple Component Directories diff --git a/docs/mcp-clients.md b/docs/mcp-clients.md index e93652e4..48c202cc 100644 --- a/docs/mcp-clients.md +++ b/docs/mcp-clients.md @@ -6,20 +6,20 @@ If you haven't installed Wassette yet, follow the [installation instructions](ht Add the Wassette MCP Server to GitHub Copilot in Visual Studio Code by clicking the **Install in VS Code** or **Install in VS Code Insiders** badge below: -[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install_Server-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://vscode.dev/redirect?url=vscode:mcp/install?%7B%22name%22%3A%22wassette%22%2C%22gallery%22%3Afalse%2C%22command%22%3A%22wassette%22%2C%22args%22%3A%5B%22serve%22%2C%22--stdio%22%5D%7D) [![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install_Server-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://vscode.dev/redirect?url=vscode-insiders:mcp/install?%7B%22name%22%3A%22wassette%22%2C%22gallery%22%3Afalse%2C%22command%22%3A%22wassette%22%2C%22args%22%3A%5B%22serve%22%2C%22--stdio%22%5D%7D) +[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install_Server-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://vscode.dev/redirect?url=vscode:mcp/install?%7B%22name%22%3A%22wassette%22%2C%22gallery%22%3Afalse%2C%22command%22%3A%22wassette%22%2C%22args%22%3A%5B%22run%22%5D%7D) [![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install_Server-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://vscode.dev/redirect?url=vscode-insiders:mcp/install?%7B%22name%22%3A%22wassette%22%2C%22gallery%22%3Afalse%2C%22command%22%3A%22wassette%22%2C%22args%22%3A%5B%22run%22%5D%7D) Alternatively, you can add the Wassete MCP server to VS Code from the command line using the `code` command in a bash/zsh or PowerShell terminal: ### bash/zsh ```bash -code --add-mcp '{"name":"Wassette","command":"wassette","args":["serve","--stdio"]}' +code --add-mcp '{"name":"Wassette","command":"wassette","args":["run"]}' ``` ### PowerShell ```powershell - code --% --add-mcp "{\"name\":\"wassette\",\"command\":\"wassette\",\"args\":[\"serve\",\"--stdio\"]}" + code --% --add-mcp "{\"name\":\"wassette\",\"command\":\"wassette\",\"args\":[\"run\"]}" ``` You can list and configure MCP servers in VS Code by running the command `MCP: List Servers` in the command palette (Ctrl+Shift+P or Cmd+Shift+P). @@ -33,7 +33,7 @@ To add Wassette to Cursor, you'll need to manually configure it in your MCP sett "mcpServers": { "wassette": { "command": "wassette", - "args": ["serve", "--stdio"] + "args": ["run"] } } } @@ -50,7 +50,7 @@ npm install -g @anthropic-ai/claude-code Add the Wassette MCP server to Claude Code using the following command: ```bash -claude mcp add -- wassette wassette serve --stdio +claude mcp add -- wassette wassette run ``` This will configure the Wassette MCP server as a local stdio server that Claude Code can use to execute Wassette commands and interact with your data infrastructure. @@ -80,7 +80,7 @@ To add the Wassette MCP server to Gemini CLI, you need to configure it in your s "mcpServers": { "wassette": { "command": "wassette", - "args": ["serve", "--stdio"] + "args": ["run"] } } } @@ -107,7 +107,7 @@ brew install codex Add the Wassette MCP server to Codex CLI using the following command: ```bash -codex mcp add wassette wassette serve --stdio +codex mcp add wassette wassette run ``` Run `codex` to start the CLI. diff --git a/docs/quick-start.md b/docs/quick-start.md index 11032a19..722a05c6 100644 --- a/docs/quick-start.md +++ b/docs/quick-start.md @@ -6,11 +6,11 @@ After installing Wassette, get started in 3 simple steps: For VS Code with GitHub Copilot, click to install: -[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install_Server-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://vscode.dev/redirect?url=vscode:mcp/install?%7B%22name%22%3A%22wassette%22%2C%22gallery%22%3Afalse%2C%22command%22%3A%22wassette%22%2C%22args%22%3A%5B%22serve%22%2C%22--stdio%22%5D%7D) +[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install_Server-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://vscode.dev/redirect?url=vscode:mcp/install?%7B%22name%22%3A%22wassette%22%2C%22gallery%22%3Afalse%2C%22command%22%3A%22wassette%22%2C%22args%22%3A%5B%22run%22%5D%7D) Or use the command line: ```bash -code --add-mcp '{"name":"Wassette","command":"wassette","args":["serve","--stdio"]}' +code --add-mcp '{"name":"Wassette","command":"wassette","args":["run"]}' ``` **2. Load a component** diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 0beb1081..1cda6d12 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -30,8 +30,8 @@ wassette component load oci://ghcr.io/microsoft/time-server-js:latest # Load a component from a local file wassette component load file:///path/to/component.wasm -# Start the MCP server (traditional mode) -wassette serve --stdio +# Start the MCP server for local development (stdio transport) +wassette run ``` ## Command Structure @@ -40,7 +40,8 @@ Wassette uses a hierarchical command structure organized around functional areas ``` wassette -├── serve # Start MCP server +├── run # Start MCP server with stdio transport (local development) +├── serve # Start MCP server with HTTP transports (remote access) ├── component # Component lifecycle management │ ├── load # Load components │ ├── unload # Remove components @@ -63,28 +64,35 @@ wassette ## Server Commands -### `wassette serve` +### `wassette run` -Start the Wassette MCP server to handle client requests. +Start the Wassette MCP server with stdio transport for local development and testing. This is the recommended mode for MCP clients. -**Stdio Transport (recommended for MCP clients):** +**Basic usage:** ```bash # Start server with stdio transport -wassette serve --stdio +wassette run # Use with specific configuration directory -wassette serve --stdio --component-dir /custom/components +wassette run --component-dir /custom/components ``` -**HTTP Transport (for development and debugging):** -```bash -# Start server with HTTP transport -wassette serve --http +**Options:** +- `--component-dir `: Set component storage directory (default: `$XDG_DATA_HOME/wassette/components`) +- `--env `: Set environment variables (can be specified multiple times) +- `--env-file `: Load environment variables from a file +- `--disable-builtin-tools`: Disable built-in tools (load-component, unload-component, etc.) -# Use Server-Sent Events (SSE) transport -wassette serve --sse +### `wassette serve` + +Start the Wassette MCP server with HTTP transports for remote access. This is intended for remote deployment scenarios. + +**Server-Sent Events (SSE) transport:** +```bash +# Start server with SSE transport (default) +wassette serve -# Use custom bind address +# Use SSE with custom bind address wassette serve --sse --bind-address 0.0.0.0:8080 # Use environment variables for bind address @@ -93,12 +101,20 @@ export BIND_HOST=0.0.0.0 wassette serve --sse ``` +**Streamable HTTP transport:** +```bash +# Start server with streamable HTTP transport +wassette serve --streamable-http +``` + **Options:** -- `--stdio`: Use stdio transport (recommended for MCP clients) -- `--http`: Use HTTP transport on 127.0.0.1:9001 -- `--sse`: Use Server-Sent Events transport -- `--bind-address
`: Set bind address for HTTP-based transports (default: `127.0.0.1:9001`) +- `--sse`: Use Server-Sent Events transport (default) +- `--streamable-http`: Use streamable HTTP transport +- `--bind-address
`: Set bind address for HTTP transports (default: `127.0.0.1:9001`) - `--component-dir `: Set component storage directory (default: `$XDG_DATA_HOME/wassette/components`) +- `--env `: Set environment variables (can be specified multiple times) +- `--env-file `: Load environment variables from a file +- `--disable-builtin-tools`: Disable built-in tools (load-component, unload-component, etc.) ## Component Management diff --git a/docs/reference/environment-variables.md b/docs/reference/environment-variables.md index 228428eb..a59b2796 100644 --- a/docs/reference/environment-variables.md +++ b/docs/reference/environment-variables.md @@ -45,7 +45,7 @@ Default: `$XDG_CONFIG_HOME/wassette/config.toml` ```bash export OPENWEATHER_API_KEY="your_key" -wassette serve --stdio +wassette run wassette permission grant environment-variable weather-tool OPENWEATHER_API_KEY ``` diff --git a/src/commands.rs b/src/commands.rs index 65240889..de86a51e 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -47,7 +47,9 @@ pub struct Cli { #[derive(Subcommand, Debug)] pub enum Commands { - /// Start a MCP Server + /// Run locally with stdio transport (for local development and testing). + Run(Run), + /// Serve remotely over HTTP transports (SSE or StreamableHttp). Serve(Serve), /// Manage WebAssembly components. Component { @@ -95,6 +97,31 @@ pub enum Commands { }, } +/// Configuration for running locally with stdio transport +#[derive(Parser, Debug, Clone, Serialize, Deserialize)] +pub struct Run { + /// Directory where components are stored. Defaults to $XDG_DATA_HOME/wassette/components + #[arg(long)] + #[serde(skip_serializing_if = "Option::is_none")] + pub component_dir: Option, + + /// Set environment variables (KEY=VALUE format). Can be specified multiple times. + #[arg(long = "env", value_parser = crate::parse_env_var)] + #[serde(skip)] + pub env_vars: Vec<(String, String)>, + + /// Load environment variables from a file (supports .env format) + #[arg(long = "env-file")] + #[serde(skip)] + pub env_file: Option, + + /// Disable built-in tools (load-component, unload-component, list-components, etc.) + #[arg(long)] + #[serde(default)] + pub disable_builtin_tools: bool, +} + +/// Configuration for serving remotely over HTTP transports #[derive(Parser, Debug, Clone, Serialize, Deserialize)] pub struct Serve { /// Directory where components are stored. Defaults to $XDG_DATA_HOME/wassette/components @@ -103,7 +130,7 @@ pub struct Serve { pub component_dir: Option, #[command(flatten)] - pub transport: TransportFlags, + pub transport: HttpTransportFlags, /// Set environment variables (KEY=VALUE format). Can be specified multiple times. #[arg(long = "env", value_parser = crate::parse_env_var)] @@ -131,19 +158,15 @@ pub struct Serve { pub manifest: Option, } +/// HTTP transport options for the Serve command #[derive(Args, Debug, Clone, Serialize, Deserialize, Default)] #[group(required = false, multiple = false)] -pub struct TransportFlags { +pub struct HttpTransportFlags { /// Serving with SSE transport #[arg(long)] #[serde(skip)] pub sse: bool, - /// Serving with stdio transport - #[arg(long)] - #[serde(skip)] - pub stdio: bool, - /// Serving with streamable HTTP transport #[arg(long)] #[serde(skip)] @@ -153,17 +176,15 @@ pub struct TransportFlags { #[derive(Debug)] pub enum Transport { Sse, - Stdio, StreamableHttp, } -impl From<&TransportFlags> for Transport { - fn from(f: &TransportFlags) -> Self { - match (f.sse, f.stdio, f.streamable_http) { - (true, false, false) => Transport::Sse, - (false, true, false) => Transport::Stdio, - (false, false, true) => Transport::StreamableHttp, - _ => Transport::Stdio, // Default case: use stdio transport +impl From<&HttpTransportFlags> for Transport { + fn from(f: &HttpTransportFlags) -> Self { + match (f.sse, f.streamable_http) { + (true, false) => Transport::Sse, + (false, true) => Transport::StreamableHttp, + _ => Transport::Sse, // Default case: use SSE transport for serve } } } diff --git a/src/config.rs b/src/config.rs index ef365600..49bb8e36 100644 --- a/src/config.rs +++ b/src/config.rs @@ -9,7 +9,7 @@ use etcetera::BaseStrategy; use figment::providers::{Env, Format, Serialized, Toml}; use serde::{Deserialize, Serialize}; -use crate::commands::Serve; +use crate::commands::{Run, Serve}; /// Get the default component directory path based on the OS pub fn get_component_dir() -> Result { @@ -107,6 +107,37 @@ impl Config { .context("Unable to merge configs") } + /// Creates a new config from a Run struct for local stdio transport + pub fn from_run(run_config: &Run) -> Result { + // Start with the base config using existing logic + let mut config = Self::new(run_config)?; + + // Load environment variables from file if specified + if let Some(env_file) = &run_config.env_file { + let file_env_vars = crate::utils::load_env_file(env_file).with_context(|| { + format!("Failed to load environment file: {}", env_file.display()) + })?; + + // Merge file environment variables (they have lower precedence than CLI args) + for (key, value) in file_env_vars { + config.environment_vars.insert(key, value); + } + } + + // Apply CLI environment variables (highest precedence) + for (key, value) in &run_config.env_vars { + config.environment_vars.insert(key.clone(), value.clone()); + } + + // Also include system environment variables that aren't overridden + // This maintains backward compatibility + for (key, value) in std::env::vars() { + config.environment_vars.entry(key).or_insert(value); + } + + Ok(config) + } + /// Creates a new config from a Serve struct that includes environment variable handling pub fn from_serve(serve_config: &Serve) -> Result { // Start with the base config using existing logic @@ -148,6 +179,26 @@ mod tests { use super::*; + #[allow(dead_code)] + fn create_test_run_config() -> Run { + Run { + component_dir: Some(PathBuf::from("/test/component/dir")), + env_vars: vec![], + env_file: None, + disable_builtin_tools: false, + } + } + + #[allow(dead_code)] + fn empty_test_run_config() -> Run { + Run { + component_dir: None, + env_vars: vec![], + env_file: None, + disable_builtin_tools: false, + } + } + fn create_test_cli_config() -> Serve { Serve { component_dir: Some(PathBuf::from("/test/component/dir")), diff --git a/src/main.rs b/src/main.rs index 5601161b..7ebb2ddc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -79,7 +79,7 @@ async fn main() -> Result<()> { match &cli.command { Some(command) => match command { - Commands::Serve(cfg) => { + Commands::Run(cfg) => { // Configure logging - use stderr for stdio transport to avoid interfering with MCP protocol let env_filter = tracing_subscriber::EnvFilter::try_from_default_env() .unwrap_or_else(|_| { @@ -88,22 +88,84 @@ async fn main() -> Result<()> { .into() }); - let registry = tracing_subscriber::registry().with(env_filter); + tracing_subscriber::registry() + .with(env_filter) + .with( + tracing_subscriber::fmt::layer() + .with_writer(std::io::stderr) + .with_ansi(false), + ) + .init(); - // Initialize logging based on transport type - let transport: Transport = (&cfg.transport).into(); - match transport { - Transport::Stdio => { - registry - .with( - tracing_subscriber::fmt::layer() - .with_writer(std::io::stderr) - .with_ansi(false), - ) - .init(); + let config = + config::Config::from_run(cfg).context("Failed to load configuration")?; + + // Build the lifecycle manager without eagerly loading components so the + // background loader is the single source of tool registration. + let config::Config { + component_dir, + secrets_dir, + environment_vars, + bind_address: _, + } = config; + + let lifecycle_manager = LifecycleManager::builder(component_dir) + .with_environment_vars(environment_vars) + .with_secrets_dir(secrets_dir) + .with_oci_client(oci_client::Client::default()) + .with_http_client(reqwest::Client::default()) + .with_eager_loading(false) + .build() + .await?; + + let server = McpServer::new(lifecycle_manager.clone(), cfg.disable_builtin_tools); + + // Start background component loading + let server_clone = server.clone(); + let lifecycle_manager_clone = lifecycle_manager.clone(); + tokio::spawn(async move { + let notify_fn = move || { + // Notify clients when a new component is loaded (if peer is available) + if let Some(peer) = server_clone.get_peer() { + let peer_clone = peer.clone(); + tokio::spawn(async move { + if let Err(e) = peer_clone.notify_tool_list_changed().await { + tracing::warn!("Failed to notify tool list changed: {}", e); + } + }); + } + }; + + if let Err(e) = lifecycle_manager_clone + .load_existing_components_async(None, Some(notify_fn)) + .await + { + tracing::error!("Background component loading failed: {}", e); } - _ => registry.with(tracing_subscriber::fmt::layer()).init(), - } + }); + + tracing::info!("Starting MCP server with stdio transport. Components will load in the background."); + let transport = stdio_transport(); + let running_service = serve_server(server, transport).await?; + + tokio::signal::ctrl_c().await?; + let _ = running_service.cancel().await; + + tracing::info!("MCP server shutting down"); + } + Commands::Serve(cfg) => { + // Configure logging for HTTP-based transports + let env_filter = tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| { + "info,cranelift_codegen=warn,cranelift_entity=warn,cranelift_bforest=warn,cranelift_frontend=warn" + .to_string() + .into() + }); + + tracing_subscriber::registry() + .with(env_filter) + .with(tracing_subscriber::fmt::layer()) + .init(); let config = config::Config::from_serve(cfg).context("Failed to load configuration")?; @@ -194,15 +256,8 @@ async fn main() -> Result<()> { } }); + let transport: Transport = (&cfg.transport).into(); match transport { - Transport::Stdio => { - tracing::info!("Starting MCP server with stdio transport. Components will load in the background."); - let transport = stdio_transport(); - let running_service = serve_server(server, transport).await?; - - tokio::signal::ctrl_c().await?; - let _ = running_service.cancel().await; - } Transport::StreamableHttp => { tracing::info!( "Starting MCP server on {} with streamable HTTP transport. Components will load in the background.", @@ -928,7 +983,12 @@ mod cli_tests { let cli = Cli::try_parse_from(args).unwrap(); matches!(cli.command, Some(Commands::Permission { .. })); - // Test serve command still works + // Test run command (local stdio) + let args = vec!["wassette", "run"]; + let cli = Cli::try_parse_from(args).unwrap(); + matches!(cli.command, Some(Commands::Run(_))); + + // Test serve command (remote HTTP) let args = vec!["wassette", "serve", "--sse"]; let cli = Cli::try_parse_from(args).unwrap(); matches!(cli.command, Some(Commands::Serve(_))); diff --git a/tests/file_integration_test.rs b/tests/file_integration_test.rs index 173593a1..766ae0b9 100644 --- a/tests/file_integration_test.rs +++ b/tests/file_integration_test.rs @@ -48,7 +48,7 @@ async fn test_filesystem_component_integration() -> Result<()> { .join("target/debug/wassette"); let mut child = tokio::process::Command::new(&binary_path) - .args(["serve", "--stdio", &component_dir_arg]) + .args(["run", &component_dir_arg]) .env("RUST_LOG", "off") .stdin(Stdio::piped()) .stdout(Stdio::piped()) diff --git a/tests/structured_output_integration_test.rs b/tests/structured_output_integration_test.rs index b83fd924..c6659657 100644 --- a/tests/structured_output_integration_test.rs +++ b/tests/structured_output_integration_test.rs @@ -34,7 +34,7 @@ async fn test_structured_output_integration() -> Result<()> { // Start wassette mcp server with stdio transport (default) let mut child = Command::new(&binary_path) - .args(["serve", &component_dir_arg]) + .args(["run", &component_dir_arg]) .env("RUST_LOG", "off") // Disable logs to avoid stdout pollution .stdin(Stdio::piped()) .stdout(Stdio::piped()) diff --git a/tests/transport_integration_test.rs b/tests/transport_integration_test.rs index c75e1f38..d92e20cc 100644 --- a/tests/transport_integration_test.rs +++ b/tests/transport_integration_test.rs @@ -453,11 +453,9 @@ async fn test_mixed_transport_fails() -> Result<()> { .context("Failed to get current directory")? .join("target/debug/wassette"); - let combinations = [ - ["--sse", "--stdio"], - ["--stdio", "--streamable-http"], - ["--sse", "--streamable-http"], - ]; + // Test mixing HTTP transport flags (--sse and --streamable-http) + // Note: --stdio is no longer part of serve command, it's now in the run command + let combinations = [["--sse", "--streamable-http"]]; for combo in combinations { // Start the server with the current combination of transports (should fail) @@ -518,7 +516,7 @@ async fn test_stdio_transport() -> Result<()> { // Start the server with stdio transport (disable logs to avoid stdout pollution) let mut child = tokio::process::Command::new(&binary_path) - .args(["serve", &component_dir_arg]) + .args(["run", &component_dir_arg]) .env("RUST_LOG", "off") .stdin(Stdio::piped()) .stdout(Stdio::piped()) @@ -663,7 +661,7 @@ async fn test_tool_list_notification() -> Result<()> { // Start the server with stdio transport (disable logs to avoid stdout pollution) let mut child = tokio::process::Command::new(&binary_path) - .args(["serve", &component_dir_arg]) + .args(["run", &component_dir_arg]) .env("RUST_LOG", "off") .stdin(Stdio::piped()) .stdout(Stdio::piped()) @@ -957,15 +955,15 @@ async fn test_default_stdio_transport() -> Result<()> { .context("Failed to get current directory")? .join("target/debug/wassette"); - // Start the server without any transport flags (should default to stdio) + // Start the server with run command (uses stdio transport) let mut child = tokio::process::Command::new(&binary_path) - .args(["serve", &component_dir_arg]) + .args(["run", &component_dir_arg]) .env("RUST_LOG", "off") .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn() - .context("Failed to start wassette with default transport")?; + .context("Failed to start wassette with stdio transport")?; let stdin = child.stdin.take().context("Failed to get stdin handle")?; let stdout = child.stdout.take().context("Failed to get stdout handle")?; @@ -1063,7 +1061,7 @@ async fn test_disable_builtin_tools() -> Result<()> { // Start the server with stdio transport and disable-builtin-tools flag let mut child = tokio::process::Command::new(&binary_path) - .args(["serve", &component_dir_arg, "--disable-builtin-tools"]) + .args(["run", &component_dir_arg, "--disable-builtin-tools"]) .env("RUST_LOG", "off") .stdin(Stdio::piped()) .stdout(Stdio::piped())