# mcp-host

Production-grade Rust crate for building Model Context Protocol (MCP) servers.

## Features

- **MCP Protocol 2025-11-25**: Full JSON-RPC 2.0 with protocol version negotiation
- **Tools, Resources, Prompts**: All three core MCP primitives
- **Elicitation**: Request structured user input with JSON Schema validation
- **Tasks**: Async task execution with status tracking
- **Bidirectional Communication**: Server→Client requests (roots/list, sampling/createMessage)
- **Contextual Visibility**: Per-session tool/resource/prompt filtering
- **Transport Abstraction**: STDIO, in-process, and HTTP transports
- **Middleware Chain**: Extensible request processing pipeline
- **Session Management**: Thread-safe multi-session support with lifecycle state machine
- **Background Notifications**: Send notifications from background tasks
- **Fluent Builder API**: Ergonomic server construction

### Resilience Patterns

- **Circuit Breakers**: Per-tool failure protection using `breaker-machines`
- **Retry with Backoff**: Exponential backoff with jitter via `chrono-machines`
- **Rate Limiting**: GCRA algorithm from `throttle-machines`
- **Structured Logging**: MCP-compliant `notifications/message` for LLM visibility

## Requirements

- Rust 1.92+ (edition 2024)
- Tokio runtime

## Installation

```toml
[dependencies]
mcp-host = "0.1"
```

## Quick Start

```rust
use mcp_host::prelude::*;
use serde_json::Value;

// Define a tool
struct EchoTool;

impl Tool for EchoTool {
    fn name(&self) -> &str { "echo" }

    fn description(&self) -> Option<&str> {
        Some("Echoes back the input message")
    }

    fn input_schema(&self) -> Value {
        serde_json::json!({
            "type": "object",
            "properties": {
                "message": { "type": "string" }
            },
            "required": ["message"]
        })
    }

    fn execute<'a>(&'a self, ctx: ExecutionContext<'a>) -> ToolFuture<'a> {
        Box::pin(async move {
            let msg = ctx.params["message"].as_str().unwrap_or("?");
            Ok(ToolOutput::text(format!("Echo: {}", msg)))
        })
    }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let server = Server::new("my-server", "0.1.0");
    server.tool_registry().register(EchoTool);

    server.set_capabilities(ServerCapabilities {
        tools: Some(mcp_host::protocol::capabilities::ToolsCapability {
            list_changed: Some(false),
        }),
        ..Default::default()
    });

    server.run(StdioTransport::new()).await
}
```

## Session Persistence (Hot Swap)

To survive process swaps, provide a durable `SessionStore` (DB/Redis/etc.). In-memory
stores do not survive restarts. For a file-backed example, see
`examples/prometheus_project.rs`.

```rust
use std::sync::Arc;
use mcp_host::prelude::*;

struct MyStore;

impl SessionStore for MyStore {
    fn load(&self, session_id: &str) -> Option<SessionSnapshot> {
        // fetch and deserialize from your DB/Redis
        let _ = session_id;
        None
    }

    fn save(&self, snapshot: SessionSnapshot) {
        // persist snapshot (enqueue if async)
        let _ = snapshot;
    }

    fn delete(&self, session_id: &str) {
        // remove from storage
        let _ = session_id;
    }
}

let server = server("my-server", "0.1.0")
    .with_session_store(Arc::new(MyStore))
    .build();
```

## Architecture

### Core Components

| Component | Description |
|-----------|-------------|
| `Server` | Main MCP server with request routing and session management |
| `ToolRegistry` | Thread-safe registry for tool implementations |
| `ResourceManager` | Manages URI-addressable resources |
| `PromptManager` | Manages reusable prompt templates |
| `MiddlewareChain` | Request processing pipeline |

### Traits

```rust
// Implement custom tools
pub trait Tool: Send + Sync {
    fn name(&self) -> &str;
    fn description(&self) -> Option<&str>;
    fn input_schema(&self) -> Value;

    // Optional: contextual visibility (default: always visible)
    fn is_visible(&self, ctx: &VisibilityContext) -> bool { true }

    // Execute with full session context
    fn execute<'a>(&'a self, ctx: ExecutionContext<'a>) -> ToolFuture<'a>;
}

// Implement custom resources
pub trait Resource: Send + Sync {
    fn uri(&self) -> &str;
    fn name(&self) -> &str;
    fn description(&self) -> Option<&str>;
    fn mime_type(&self) -> Option<&str>;

    fn is_visible(&self, ctx: &VisibilityContext) -> bool { true }

    fn read<'a>(&'a self, ctx: ExecutionContext<'a>) -> ResourceReadFuture<'a>;
}

// Implement custom prompts
pub trait Prompt: Send + Sync {
    fn name(&self) -> &str;
    fn description(&self) -> Option<&str>;
    fn arguments(&self) -> Option<Vec<PromptArgument>>;

    fn is_visible(&self, ctx: &VisibilityContext) -> bool { true }

    fn get<'a>(&'a self, ctx: ExecutionContext<'a>) -> PromptFuture<'a>;
}
```

### Bidirectional Communication

Request data from clients:

```rust
// Request workspace roots (requires client roots capability)
let roots = server.request_roots("session-id", None).await?;
for root in roots {
    println!("Root: {} ({})", root.name.unwrap_or_default(), root.uri);
}

// Request LLM completion (requires client sampling capability)
let params = CreateMessageParams {
    messages: vec![SamplingMessage {
        role: "user".to_string(),
        content: SamplingContent::Text { text: "Hello!".to_string() },
    }],
    max_tokens: 1000,
    ..Default::default()
};
let result = server.request_sampling("session-id", params, None).await?;
```

### In-Process Transport

For embedded servers, prefer `InProcessTransport` over piping stdio through a
thread. It keeps messages in memory and sends typed JSON-RPC values over Tokio
channels.

```rust
use mcp_host::prelude::*;

let server = Server::new("embedded", "0.1.0");
let (server_transport, mut client_transport) = InProcessTransport::pair();

let server_task = tokio::spawn({
    let server = server.clone();
    async move { server.run(server_transport).await.unwrap() }
});

client_transport
    .send_request(JsonRpcClientRequest::new(
        "init",
        "initialize",
        Some(serde_json::json!({
            "protocolVersion": "2025-11-25",
            "capabilities": {},
            "clientInfo": {"name": "embedded-client", "version": "0.1.0"}
        })),
    ))
    .await?;
```

### Visibility Context

Filter tools/resources/prompts based on session state:

```rust
struct GitCommitTool;

impl Tool for GitCommitTool {
    // Only visible when in a git repo with uncommitted changes
    fn is_visible(&self, ctx: &VisibilityContext) -> bool {
        ctx.environment
            .map(|e| e.has_git_repo() && !e.git_is_clean())
            .unwrap_or(false)
    }
    // ...
}

struct AdminOnlyResource;

impl Resource for AdminOnlyResource {
    // Only visible to admin users
    fn is_visible(&self, ctx: &VisibilityContext) -> bool {
        ctx.is_admin()
    }
    // ...
}
```

### Custom Context for Macros

`#[mcp_tool]`, `#[mcp_prompt]`, and resource macros can accept any context type
that implements `FromExecutionContext`. This lets you add your own helpers while
still accessing the built-in `Ctx` API.

```rust
use std::ops::Deref;
use mcp_host::prelude::*;

#[derive(Clone)]
struct MyCtx<'a> {
    inner: Ctx<'a>,
}

impl<'a> FromExecutionContext<'a> for MyCtx<'a> {
    fn from_execution_context(ctx: &'a ExecutionContext<'a>) -> Self {
        Self { inner: Ctx::from_execution_context(ctx) }
    }
}

impl<'a> Deref for MyCtx<'a> {
    type Target = Ctx<'a>;
    fn deref(&self) -> &Self::Target { &self.inner }
}

struct MyServer;

impl MyServer {
    #[mcp_tool(name = "echo")]
    async fn echo(&self, ctx: MyCtx<'_>, _params: Parameters<()>) -> ToolResult {
        if !ctx.is_admin() { /* ... */ }
        Ok(ToolOutput::text("ok"))
    }
}
```

### Rust 2024 Module Layout (One Handler Per File)

The PROMETHEUS example uses a Rails-style, one-handler-per-file layout.
This keeps tools/prompts/resources small and composable.

```text
examples/
  prometheus_project.rs
  prometheus_project/
    tools.rs
    prompts.rs
    resources.rs
    tools/
      echo.rs
      read_file.rs
      ...
    prompts/
      greeting.rs
      ...
    resources/
      server_config.rs
      timeline_data.rs
      ...
```

```rust
// examples/prometheus_project/tools/echo.rs
use super::super::*;
use mcp_host::registry::router::McpToolRouter;

impl PrometheusServer {
    #[mcp_tool(name = "echo", read_only = true, idempotent = true)]
    async fn echo(&self, _ctx: PrometheusCtx<'_>, params: Parameters<EchoParams>) -> ToolResult {
        Ok(ToolOutput::text(format!("Echo: {}", params.0.message)))
    }
}

pub fn mount(router: McpToolRouter<PrometheusServer>) -> McpToolRouter<PrometheusServer> {
    router.with_tool(PrometheusServer::echo_tool_info(), PrometheusServer::echo_handler, None)
}
```

```rust
// examples/prometheus_project/tools.rs
#[path = "tools/echo.rs"]
pub mod echo;

use mcp_host::registry::router::McpToolRouter;
use crate::PrometheusServer;

pub fn router() -> McpToolRouter<PrometheusServer> {
    let router = McpToolRouter::new();
    echo::mount(router)
}
```

```rust
// examples/prometheus_project.rs
#[path = "prometheus_project/tools.rs"]
mod tools;
#[path = "prometheus_project/prompts.rs"]
mod prompts;
#[path = "prometheus_project/resources.rs"]
mod resources;

impl PrometheusServer {
    fn router() -> mcp_host::registry::router::McpRouter<Self> {
        mcp_host::registry::router::McpRouter::new(
            tools::router(),
            prompts::router(),
            resources::router(),
            resources::template_router(),
        )
    }
}
```

### Server Builder

```rust
use mcp_host::prelude::*;

let server = server("my-server", "1.0.0")
    .with_tools(true)
    .with_resources(true, false)
    .with_prompts(true)
    .with_logging()
    .with_logging_middleware()
    .with_validation_middleware()
    .build();
```

### Resilience Configuration

```rust
use mcp_host::prelude::*;

let server = server("resilient-server", "1.0.0")
    .with_tools(true)
    .with_resources(true, false)
    // Circuit breaker: opens after 3 failures in 60s
    .with_circuit_breaker(ToolBreakerConfig {
        failure_threshold: 3,
        failure_window_secs: 60.0,
        half_open_timeout_secs: 10.0,
        success_threshold: 2,
    })
    // Retry: exponential backoff with full jitter
    .with_retry(ResourceRetryConfig {
        max_attempts: 3,
        base_delay_ms: 100,
        multiplier: 2.0,
        max_delay_ms: 5000,
        jitter_factor: 1.0,
    })
    // Rate limit: 100 req/s with burst of 20
    .with_rate_limit(100.0, 20)
    .build();
```

### MCP Logging (LLM-visible)

```rust
use mcp_host::prelude::*;

// Create logger with notification channel
let logger = McpLogger::new(notification_tx, "my-tool");

// Log messages are visible to the LLM via notifications/message
logger.info("Tool initialized successfully");
logger.warning("Rate limit approaching threshold");
logger.error("External API unavailable");
```

## Notifications

Send notifications from background tasks:

```rust
let server = Server::new("server", "1.0.0");
let notification_tx = server.notification_sender();

// Spawn background task
tokio::spawn(async move {
    notification_tx.send(JsonRpcNotification::new(
        "notifications/progress",
        Some(serde_json::json!({ "progress": 50 })),
    )).ok();
});
```

## Feature Flags

| Flag | Description | Default |
|------|-------------|---------|
| `stdio` | STDIO transport support | ✓ |
| `http` | HTTP transport via rama | |
| `macros` | Proc macros (`#[mcp_tool]`, `#[mcp_prompt]`, `#[mcp_resource]`, `#[mcp_router]`) | |
| `full` | All features enabled | |

```toml
# Minimal (STDIO only)
mcp-host = "0.1"

# With HTTP transport
mcp-host = { version = "0.1", features = ["http"] }

# With macros for ergonomic tool definition
mcp-host = { version = "0.1", features = ["macros"] }

# Everything
mcp-host = { version = "0.1", features = ["full"] }
```

## Supported MCP Methods

### Client→Server

- `initialize` / `ping`
- `tools/list` / `tools/call`
- `resources/list` / `resources/read` / `resources/subscribe` / `resources/unsubscribe`
- `prompts/list` / `prompts/get`
- `completion/complete`
- `tasks/list` / `tasks/get` / `tasks/cancel`

### Server→Client (Bidirectional)

- `roots/list` - Request workspace roots from client
- `sampling/createMessage` - Request LLM completion from client
- `elicitation/create` - Request structured user input with schema validation

## Examples

```bash
# Run the comprehensive PROMETHEUS example with all features
cargo run --example prometheus_project --features macros

# Run with HTTP transport (requires http feature)
cargo run --example prometheus_project --features "macros,http" -- --http --port 8080
```

## License

BSD-3-Clause
