Skip to content
Closed
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),

### Added

- MCP logging capability declaration and `logging/setLevel` request handler that allows MCP clients to control structured log output according to the MCP protocol specification (2025-06-18) ([#340](https://github.com/microsoft/wassette/pull/340))
- Comprehensive Docker documentation and Dockerfile for running Wassette in containers with enhanced security isolation, including examples for mounting components, secrets, configuration files, and production deployment patterns with Docker Compose
- `rust-toolchain.toml` file specifying Rust 1.90 as the stable toolchain version, ensuring consistent Rust version across development environments and CI/CD pipelines
- AI agent development guides (`AGENTS.md` and `Claude.md`) that consolidate development guidelines from `.github/instructions/` into accessible documentation for AI agents working on the project
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ license-file = "LICENSE"

[workspace.dependencies]
anyhow = "1.0"
chrono = "0.4"
component2json = { path = "crates/component2json" }
etcetera = "0.10"
futures = "0.3"
Expand Down Expand Up @@ -45,6 +46,7 @@ wasmtime-wasi-config = "36"
[dependencies]
anyhow = { workspace = true }
axum = "0.8"
chrono = { workspace = true }
clap = { version = "4.5", features = ["derive"] }
etcetera = { workspace = true }
figment = { version = "0.10", features = ["env", "toml"] }
Expand Down
1 change: 1 addition & 0 deletions docs/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

- [CLI](./reference/cli.md)
- [Permissions](./reference/permissions.md)
- [MCP Logging](./mcp-logging.md)

# Design & Architecture

Expand Down
179 changes: 179 additions & 0 deletions docs/mcp-logging.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
# MCP Logging

Wassette implements the Model Context Protocol (MCP) logging specification, allowing MCP clients to receive structured log messages from the server.

## Overview

The MCP logging feature provides:

- **Structured log output**: Log messages are sent as JSON-RPC notifications to MCP clients
- **Level filtering**: Clients can set a minimum log level to control verbosity
- **Syslog severity levels**: Supports all standard syslog levels (Emergency, Alert, Critical, Error, Warning, Notice, Info, Debug)
- **Automatic forwarding**: Internal tracing logs are automatically converted to MCP notifications

## Logging Capability

Wassette declares the `logging` capability in its server capabilities:

```json
{
"capabilities": {
"logging": {},
"tools": { "listChanged": true }
}
}
```

## Setting Log Level

Clients can control log verbosity using the `logging/setLevel` request:

```json
{
"jsonrpc": "2.0",
"method": "logging/setLevel",
"params": {
"level": "info"
},
"id": 2
}
```

**Supported levels** (in order of decreasing severity):
- `emergency` - System is unusable
- `alert` - Action must be taken immediately
- `critical` - Critical conditions
- `error` - Error conditions
- `warning` - Warning conditions
- `notice` - Normal but significant events
- `info` - General informational messages
- `debug` - Detailed debugging information

## Log Message Notifications

After setting a log level, the server sends log messages as `notifications/message` notifications:

```json
{
"jsonrpc": "2.0",
"method": "notifications/message",
"params": {
"level": "info",
"logger": "wassette",
"data": {
"message": "Component loaded successfully",
"target": "wassette::lifecycle",
"timestamp": "2025-01-09T12:34:56.789Z"
}
}
}
```

## Log Level Filtering

Only log messages at or above the configured minimum level are sent to clients. For example, if the level is set to `info`:

- ✅ `emergency`, `alert`, `critical`, `error`, `warning`, `notice`, and `info` messages are sent
- ❌ `debug` messages are filtered out

## Implementation Details

### Architecture

Wassette uses a custom `tracing` subscriber layer (`McpLoggingLayer`) that:

1. Intercepts log events from the `tracing` framework
2. Converts them to MCP `LoggingMessageNotificationParam` structures
3. Filters based on the client-configured minimum level
4. Sends notifications to connected MCP clients

### Level Mapping

Tracing levels are mapped to MCP levels as follows:

| Tracing Level | MCP Level |
|---------------|-----------|
| `ERROR` | `error` |
| `WARN` | `warning` |
| `INFO` | `info` |
| `DEBUG` | `debug` |
| `TRACE` | `debug` |

### Message Structure

Each log notification includes:

- **level**: The log severity level
- **logger**: The source module or component (optional)
- **data**: A JSON object containing:
- `message`: The log message text
- `target`: The tracing target (typically module path)
- `timestamp`: RFC3339-formatted timestamp

## Example Usage

### With MCP Inspector

```bash
# Start Wassette server
cargo run -- serve --sse

# In another terminal, connect with MCP inspector
npx @modelcontextprotocol/inspector --cli http://127.0.0.1:9001/sse

# Set log level to info
# (Use inspector UI to send logging/setLevel request)
```

### Programmatic Example

```javascript
// Connect to Wassette MCP server
const client = new MCPClient(transport);

// Initialize connection
await client.initialize({
protocolVersion: "2024-11-05",
clientInfo: { name: "my-client", version: "1.0.0" }
});

// Set log level
await client.request({
method: "logging/setLevel",
params: { level: "info" }
});

// Listen for log notifications
client.on("notifications/message", (notification) => {
const { level, logger, data } = notification.params;
console.log(`[${level}] ${logger}: ${data.message}`);
});
```

## Security Considerations

- Log messages should not contain sensitive information (credentials, tokens, etc.)
- The logging layer automatically filters out logs when no client has set a log level
- Each client can set their own log level independently

## Troubleshooting

### No log messages received

1. Verify the logging capability is declared in server capabilities
2. Ensure you've sent a `logging/setLevel` request
3. Check that the log level is appropriate (e.g., `debug` for maximum verbosity)
4. Verify the MCP client is listening for `notifications/message`

### Too many/too few log messages

Adjust the log level:
- Use `debug` for detailed troubleshooting
- Use `info` for normal operation
- Use `warning` or `error` for production monitoring

## Reference

- [MCP Logging Specification](https://modelcontextprotocol.io/specification/2025-06-18/server/utilities/logging)
- [Tracing Documentation](https://docs.rs/tracing/latest/tracing/)
- [Syslog Severity Levels (RFC 5424)](https://datatracker.ietf.org/doc/html/rfc5424#section-6.2.1)
65 changes: 47 additions & 18 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ use mcp_server::{
};
use rmcp::model::{
CallToolRequestParam, CallToolResult, ErrorData, ListPromptsResult, ListResourcesResult,
ListToolsResult, PaginatedRequestParam, ServerCapabilities, ServerInfo, ToolsCapability,
ListToolsResult, LoggingLevel, PaginatedRequestParam, ServerCapabilities, ServerInfo,
SetLevelRequestParam, ToolsCapability,
};
use rmcp::service::{serve_server, RequestContext, RoleServer};
use rmcp::transport::streamable_http_server::session::local::LocalSessionManager;
Expand All @@ -37,6 +38,7 @@ use tracing_subscriber::util::SubscriberInitExt as _;
mod commands;
mod config;
mod format;
mod mcp_logging;

use commands::{
Cli, Commands, ComponentCommands, GrantPermissionCommands, PermissionCommands, PolicyCommands,
Expand Down Expand Up @@ -194,6 +196,7 @@ const BIND_ADDRESS: &str = "127.0.0.1:9001";
pub struct McpServer {
lifecycle_manager: LifecycleManager,
peer: Arc<Mutex<Option<rmcp::Peer<rmcp::RoleServer>>>>,
log_level: Arc<Mutex<Option<LoggingLevel>>>,
}

/// Handle CLI tool commands by creating appropriate tool call requests
Expand Down Expand Up @@ -301,6 +304,7 @@ impl McpServer {
Self {
lifecycle_manager,
peer: Arc::new(Mutex::new(None)),
log_level: Arc::new(Mutex::new(None)),
}
}

Expand All @@ -326,6 +330,7 @@ impl ServerHandler for McpServer {
tools: Some(ToolsCapability {
list_changed: Some(true),
}),
logging: Some(serde_json::Map::new()),
..Default::default()
},
instructions: Some(
Expand Down Expand Up @@ -421,6 +426,22 @@ Key points:
}
})
}

#[allow(clippy::manual_async_fn)]
fn set_level(
&self,
params: SetLevelRequestParam,
_ctx: RequestContext<RoleServer>,
) -> impl Future<Output = Result<(), ErrorData>> + Send + '_ {
async move {
// Store the requested log level
let mut log_level = self.log_level.lock().unwrap();
*log_level = Some(params.level);

tracing::info!("MCP logging level set to: {:?}", params.level);
Ok(())
}
}
}

/// Formats build information similar to agentgateway's version output
Expand Down Expand Up @@ -482,23 +503,6 @@ async fn main() -> Result<()> {
.into()
});

let registry = tracing_subscriber::registry().with(env_filter);

// 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();
}
_ => registry.with(tracing_subscriber::fmt::layer()).init(),
}

let config =
config::Config::from_serve(cfg).context("Failed to load configuration")?;

Expand All @@ -521,6 +525,31 @@ async fn main() -> Result<()> {

let server = McpServer::new(lifecycle_manager.clone());

// Create MCP logging layer that can forward logs to MCP clients
let mcp_layer = mcp_logging::McpLoggingLayer::new(
server.peer.clone(),
server.log_level.clone(),
);

let registry = tracing_subscriber::registry()
.with(env_filter)
.with(mcp_layer);

// 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();
}
_ => registry.with(tracing_subscriber::fmt::layer()).init(),
}

// Start background component loading
let server_clone = server.clone();
let lifecycle_manager_clone = lifecycle_manager.clone();
Expand Down
Loading
Loading