Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ 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))
- MCP logging layer (`mcp_logging.rs`) that bridges tracing log events to MCP logging notifications with support for all syslog severity levels (Debug, Info, Notice, Warning, Error, Critical, Alert, Emergency)
- 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
- Comprehensive installation guide page consolidating all installation methods (one-liner script, Homebrew, Nix, WinGet) organized by platform (Linux, macOS, Windows) with verification steps and troubleshooting sections
- Cookbook section in documentation with language-specific guides for building Wasm components in JavaScript/TypeScript, Python, Rust, and Go ([#328](https://github.com/microsoft/wassette/pull/328))
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 @@ -18,6 +18,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