Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,17 @@ ENV HOME=/home/wassette
ENV XDG_DATA_HOME=/home/wassette/.local/share
ENV XDG_CONFIG_HOME=/home/wassette/.config

# Twelve-factor app compliance: support PORT and BIND_HOST environment variables
# Default PORT is 9001; default BIND_HOST in containers is 0.0.0.0
# (required for external connections; differs from non-containerized default of 127.0.0.1)
ENV PORT=9001
ENV BIND_HOST=0.0.0.0

# Switch to the non-root user
USER wassette
WORKDIR /home/wassette

# Expose the default HTTP port (when using --http or --sse)
# Expose the default HTTP port (configurable via PORT env var)
EXPOSE 9001

# Default command: start Wassette with streamable-http transport
Expand Down
44 changes: 42 additions & 2 deletions docs/deployment/docker.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,12 +123,33 @@ docker run --rm -p 9001:9001 \
Pass environment variables to the container:

```bash
docker run --rm -p 9001:9001 \
docker run --rm -p 8080:8080 \
-e PORT=8080 \
-e BIND_HOST=0.0.0.0 \
-e RUST_LOG=debug \
-e OPENWEATHER_API_KEY=your_api_key \
wassette:latest
```

**Twelve-Factor App Compliance**: Wassette supports `PORT` and `BIND_HOST` environment variables for flexible port binding. The Docker image defaults to `BIND_HOST=0.0.0.0` to allow external connections.

#### Server Configuration Variables

These environment variables set the default bind address when not specified via CLI (`--bind-address`) or config file:

- **PORT**: Port number to listen on (default: 9001)
- **BIND_HOST**: Host address to bind to (default: 127.0.0.1; Docker image overrides to 0.0.0.0)

**Precedence:** CLI > Config file > PORT/BIND_HOST > Defaults (127.0.0.1:9001)

Example with custom port:

```bash
docker run --rm -p 3000:3000 \
-e PORT=3000 \
wassette:latest
```

See the [Environment Variables reference](../reference/environment-variables.md) for comprehensive examples and best practices.

### Using a Configuration File
Expand Down Expand Up @@ -284,7 +305,13 @@ FROM your-custom-base:latest

### Health Checks

Add health checks when running with HTTP/SSE transport:
Wassette provides health and readiness endpoints when running with StreamableHttp transport:

- **`/health`**: Returns 200 OK if server is running
- **`/ready`**: Returns JSON with readiness status
- **`/info`**: Returns version and build information

Add health checks in Docker Compose:

```yaml
# docker-compose.yml
Expand All @@ -299,6 +326,19 @@ services:
start_period: 40s
```

Or with Docker CLI:

```bash
docker run --rm -p 9001:9001 \
--health-cmd="curl -f http://localhost:9001/health || exit 1" \
--health-interval=30s \
--health-timeout=10s \
--health-retries=3 \
wassette:latest
```

**Note**: Health endpoints are only available with `--streamable-http` transport (the default for the Docker image). SSE transport (`--sse`) is designed solely for event streaming and does not expose standard HTTP endpoints like `/health`.

### Persistent Component Storage

For persistent component storage across container restarts:
Expand Down
66 changes: 62 additions & 4 deletions docs/deployment/operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,11 +165,69 @@ index=wassette tool_name=* | stats count by tool_name

### Health Checks

Monitor Wassette's health by checking:
When running with StreamableHttp transport, Wassette provides health and readiness endpoints:

1. **Process running**: Ensure the wassette process is active
2. **HTTP endpoint**: For SSE/HTTP transports, verify the endpoint responds
3. **Log activity**: Monitor for error patterns in logs
#### Endpoints

- **`/health`**: Returns HTTP 200 OK if the server is running
- **`/ready`**: Returns HTTP 200 with JSON `{"status":"ready"}` when the server is ready to accept requests
- **`/info`**: Returns version and build information as JSON

**Example Usage:**

```bash
# Check if server is running
curl -f http://localhost:9001/health

# Check readiness
curl http://localhost:9001/ready

# Get version and build info
curl http://localhost:9001/info | jq .
```

**Example Response from `/info`:**
```json
{
"version": "0.3.5",
"build_info": "0.3.5 version.BuildInfo{RustVersion:\"1.90.0\", BuildProfile:\"release\", BuildStatus:\"Clean\", GitTag:\"v0.3.5\", Version:\"abc1234\", GitRevision:\"abc1234\"}"
}
Comment on lines +189 to +194
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The example response for /info on line 193 shows "build_info": "0.3.4 version.BuildInfo{...}" which includes the version number twice (once in the JSON field "version" and once at the start of "build_info"). Looking at the format_build_info() implementation, it returns format!("{} version.BuildInfo{{...}}", built_info::PKG_VERSION, ...), so the example is correct. However, this redundancy might be confusing. Consider documenting why the version appears twice or simplify the format.

Copilot uses AI. Check for mistakes.
```

*Note: The version and build_info fields reflect the actual build and may differ from this example.*

#### Integration with Container Orchestration

Use health endpoints with Docker, Kubernetes, or other orchestration platforms:

**Docker:**
```bash
docker run --rm -p 9001:9001 \
--health-cmd="curl -f http://localhost:9001/health || exit 1" \
--health-interval=30s \
--health-timeout=10s \
--health-retries=3 \
wassette:latest
```

**Kubernetes:**
```yaml
livenessProbe:
httpGet:
path: /health
port: 9001
initialDelaySeconds: 10
periodSeconds: 30

readinessProbe:
httpGet:
path: /ready
port: 9001
initialDelaySeconds: 5
periodSeconds: 10
```

**Note**: Health endpoints are only available with `--streamable-http` transport. SSE transport (`--sse`) also uses HTTP but is designed solely for event streaming and does not provide a general HTTP request/response interface. For stdio or SSE transports, monitor the process status instead.

## Performance Tuning

Expand Down
17 changes: 14 additions & 3 deletions docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,9 @@ wassette serve --sse
# Use custom bind address
wassette serve --sse --bind-address 0.0.0.0:8080

# Use environment variable for bind address
export WASSETTE_BIND_ADDRESS=192.168.1.100:9001
# Use environment variables for bind address
export PORT=8080
export BIND_HOST=0.0.0.0
wassette serve --sse
```

Expand Down Expand Up @@ -641,10 +642,20 @@ component_dir = "/opt/wassette/components"

- **`WASSETTE_CONFIG_FILE`**: Override the default configuration file location
- **`WASSETTE_COMPONENT_DIR`**: Override the default component storage location
- **`WASSETTE_BIND_ADDRESS`**: Override the default bind address for HTTP-based transports
- **`PORT`**: Set the port number for HTTP-based transports (default: 9001)
- **`BIND_HOST`**: Set the host address to bind to (default: 127.0.0.1)
- **`XDG_CONFIG_HOME`**: Base directory for configuration files (Linux/macOS)
- **`XDG_DATA_HOME`**: Base directory for data storage (Linux/macOS)

Comment on lines +645 to 649
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documented configuration precedence (lines 615-619) lists "Environment variables prefixed with WASSETTE_" as having higher priority than "Configuration file". However, PORT and BIND_HOST (listed at lines 645-646) are NOT prefixed with WASSETTE_ and are actually evaluated in the default function, not in the figment precedence chain.

This creates confusion about their actual precedence. The documentation should clarify that PORT/BIND_HOST are only used when bind_address is not explicitly set via CLI, WASSETTE_ env vars, or config file. They serve as "enhanced defaults" rather than being part of the main precedence chain.

Update the documentation to:

### Bind Address Configuration

The bind address can be configured via:
1. CLI option `--bind-address` (highest priority)
2. Environment variable `WASSETTE_BIND_ADDRESS` (deprecated, removed)
3. Configuration file `bind_address` field
4. PORT and BIND_HOST environment variables (used as defaults when above are not set)
5. Built-in defaults: 127.0.0.1:9001 (or 0.0.0.0:9001 in Docker)
Suggested change
- **`PORT`**: Set the port number for HTTP-based transports (default: 9001)
- **`BIND_HOST`**: Set the host address to bind to (default: 127.0.0.1)
- **`XDG_CONFIG_HOME`**: Base directory for configuration files (Linux/macOS)
- **`XDG_DATA_HOME`**: Base directory for data storage (Linux/macOS)
- **`XDG_CONFIG_HOME`**: Base directory for configuration files (Linux/macOS)
- **`XDG_DATA_HOME`**: Base directory for data storage (Linux/macOS)
### Bind Address Configuration
The bind address can be configured via:
1. CLI option `--bind-address` (highest priority)
2. Environment variable `WASSETTE_BIND_ADDRESS` (deprecated, removed)
3. Configuration file `bind_address` field
4. PORT and BIND_HOST environment variables (used as defaults when above are not set)
5. Built-in defaults: 127.0.0.1:9001 (or 0.0.0.0:9001 in Docker)

Copilot uses AI. Check for mistakes.
#### Bind Address Configuration

The bind address can be configured via multiple methods with the following precedence:

1. CLI option `--bind-address` (highest priority)
2. Configuration file `bind_address` field
3. PORT and BIND_HOST environment variables (used as defaults when above are not set)
4. Built-in defaults: 127.0.0.1:9001 (or 0.0.0.0:9001 in Docker)

### Component Storage

By default, Wassette stores components in `$XDG_DATA_HOME/wassette/components` (typically `~/.local/share/wassette/components` on Linux/macOS). You can override this with the `--component-dir` option:
Expand Down
5 changes: 3 additions & 2 deletions docs/reference/configuration-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,9 @@ You can override any configuration value using environment variables with the `W
# Override component directory
export WASSETTE_COMPONENT_DIR=/custom/components

# Override bind address
export WASSETTE_BIND_ADDRESS=0.0.0.0:8080
# Override bind address using PORT and BIND_HOST
export PORT=8080
export BIND_HOST=0.0.0.0

# Override config file location
export WASSETTE_CONFIG_FILE=/etc/wassette/config.toml
Expand Down
41 changes: 40 additions & 1 deletion docs/reference/environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,46 @@

Pass environment variables to Wassette components using shell exports or config files. Components need explicit permission to access variables.

## Quick Start
## Server Configuration

Wassette supports the following environment variables for server configuration (following the [twelve-factor app](https://12factor.net/) methodology):

### PORT
Sets the port number for HTTP-based transports (SSE and StreamableHttp) when `bind_address` is not specified via CLI or config file.

```bash
PORT=8080 wassette serve --streamable-http
```

Default: `9001`

**Precedence:** CLI (`--bind-address`) > Config file (`bind_address`) > PORT/BIND_HOST > Default (127.0.0.1:9001)

### BIND_HOST
Sets the host address to bind to for HTTP-based transports when `bind_address` is not specified via CLI or config file.

```bash
BIND_HOST=0.0.0.0 wassette serve --streamable-http
```

Default: `127.0.0.1` (localhost only)

**Note:** In Docker containers, use `BIND_HOST=0.0.0.0` to allow external connections.

**Precedence:** CLI (`--bind-address`) > Config file (`bind_address`) > PORT/BIND_HOST > Default (127.0.0.1:9001)

### WASSETTE_CONFIG_FILE
Path to custom configuration file.

```bash
WASSETTE_CONFIG_FILE=/path/to/config.toml wassette serve
```

Default: `$XDG_CONFIG_HOME/wassette/config.toml`

## Component Environment Variables

### Quick Start

```bash
export OPENWEATHER_API_KEY="your_key"
Expand Down
74 changes: 42 additions & 32 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,11 @@ fn default_secrets_dir() -> PathBuf {
}

fn default_bind_address() -> String {
"127.0.0.1:9001".to_string()
// Default bind address using PORT and BIND_HOST environment variables (twelve-factor app compliance).
// This is only used when bind_address is not set via CLI, config file, or other higher-precedence sources.
let host = std::env::var("BIND_HOST").unwrap_or_else(|_| "127.0.0.1".to_string());
let port = std::env::var("PORT").unwrap_or_else(|_| "9001".to_string());
format!("{}:{}", host, port)
}

/// Configuration for the Wasette MCP server
Expand All @@ -57,7 +61,8 @@ pub struct Config {
pub environment_vars: HashMap<String, String>,

/// Bind address for HTTP-based transports (SSE and StreamableHttp)
#[serde(default = "default_bind_address")]
/// Configured via PORT and BIND_HOST environment variables or CLI/config file
#[serde(default = "default_bind_address", rename = "bind_address")]
pub bind_address: String,
}

Expand Down Expand Up @@ -89,9 +94,14 @@ impl Config {
cli_config: &T,
config_file_path: impl AsRef<Path>,
) -> Result<Self, anyhow::Error> {
// Build figment config, excluding bind_address from WASSETTE_ environment variables.
// Instead, bind_address uses PORT and BIND_HOST env vars as defaults (via default_bind_address())
// when not explicitly set via CLI or config file.
let env_provider = Env::prefixed("WASSETTE_").filter(|key| key != "bind_address");

figment::Figment::new()
.admerge(Toml::file(config_file_path))
.admerge(Env::prefixed("WASSETTE_"))
.admerge(env_provider)
.admerge(Serialized::defaults(cli_config))
.extract()
.context("Unable to merge configs")
Expand Down Expand Up @@ -321,7 +331,7 @@ policy_file = "custom_policy.yaml"

#[test]
fn test_bind_address_default() {
temp_env::with_vars_unset(vec!["WASSETTE_BIND_ADDRESS"], || {
temp_env::with_vars_unset(vec!["PORT", "BIND_HOST"], || {
let temp_dir = TempDir::new().unwrap();
let non_existent_config = temp_dir.path().join("non_existent_config.toml");

Expand All @@ -335,7 +345,7 @@ policy_file = "custom_policy.yaml"

#[test]
fn test_bind_address_from_config_file() {
temp_env::with_vars_unset(vec!["WASSETTE_BIND_ADDRESS"], || {
temp_env::with_vars_unset(vec!["PORT", "BIND_HOST"], || {
let temp_dir = TempDir::new().unwrap();
let config_file = temp_dir.path().join("config.toml");

Expand Down Expand Up @@ -381,47 +391,47 @@ bind_address = "0.0.0.0:8080"
}

#[test]
fn test_bind_address_env_var() {
temp_env::with_var("WASSETTE_BIND_ADDRESS", Some("10.0.0.1:3000"), || {
fn test_port_env_var() {
temp_env::with_vars(vec![("PORT", Some("8080")), ("BIND_HOST", None)], || {
let temp_dir = TempDir::new().unwrap();
let non_existent_config = temp_dir.path().join("non_existent_config.toml");

let config = Config::new_from_path(&empty_test_cli_config(), &non_existent_config)
.expect("Failed to create config");

// Environment variable should be used
assert_eq!(config.bind_address, "10.0.0.1:3000");
// PORT environment variable should be used with default host
assert_eq!(config.bind_address, "127.0.0.1:8080");
});
}

#[test]
fn test_bind_address_precedence() {
temp_env::with_var("WASSETTE_BIND_ADDRESS", Some("10.0.0.1:3000"), || {
fn test_bind_host_env_var() {
temp_env::with_vars(vec![("BIND_HOST", Some("0.0.0.0")), ("PORT", None)], || {
let temp_dir = TempDir::new().unwrap();
let config_file = temp_dir.path().join("config.toml");

// Config file sets bind address
let toml_content = r#"
bind_address = "0.0.0.0:8080"
"#;
fs::write(&config_file, toml_content).unwrap();
let non_existent_config = temp_dir.path().join("non_existent_config.toml");

// CLI provides bind address
let serve_config = Serve {
component_dir: None,
transport: Default::default(),
env_vars: vec![],
env_file: None,
disable_builtin_tools: false,
bind_address: Some("192.168.1.100:9090".to_string()),
manifest: None,
};

let config = Config::new_from_path(&serve_config, &config_file)
let config = Config::new_from_path(&empty_test_cli_config(), &non_existent_config)
.expect("Failed to create config");

// CLI value should take highest precedence
assert_eq!(config.bind_address, "192.168.1.100:9090");
// BIND_HOST should be used with default port
assert_eq!(config.bind_address, "0.0.0.0:9001");
});
}

#[test]
fn test_port_and_bind_host_env_vars() {
temp_env::with_vars(
vec![("PORT", Some("3000")), ("BIND_HOST", Some("0.0.0.0"))],
|| {
let temp_dir = TempDir::new().unwrap();
let non_existent_config = temp_dir.path().join("non_existent_config.toml");

let config = Config::new_from_path(&empty_test_cli_config(), &non_existent_config)
.expect("Failed to create config");

// Both PORT and BIND_HOST should be used together
assert_eq!(config.bind_address, "0.0.0.0:3000");
},
);
}
}
Loading
Loading