rmcp-actix-web
Library Listed
actix-web transport implementations for RMCP (Rust Model Context Protocol)
- Downloads
- 27,030
- Dependents
- 5
- Latest release
- 0.13.2 ·
About
actix-web transport implementations for RMCP (Rust Model Context Protocol)
This crate provides actix-web-based transport implementations for the Model Context Protocol, offering a complete alternative to the default Axum-based transports in the main RMCP crate.
Overview
rmcp-actix-web provides:
- Streamable HTTP transport: Bidirectional communication with session management
- Framework-level composition: Mount MCP services at custom paths using actix-web Scope
- Full MCP compatibility: Implements the complete MCP protocol specification
- RMCP ecosystem alignment: APIs that follow RMCP patterns for maximum consistency
⚠️ Security Notice
This transport forwards Authorization headers to MCP services. If your MCP service passes these tokens to upstream APIs (proxy pattern), be aware this violates MCP specifications. See SECURITY.md for details.
Contributing
We welcome contributions to rmcp-actix-web! Please follow these guidelines:
Read the full README
How to Contribute
- Fork the repository on GitLab
- Create a feature branch from
main:git checkout -b feature/my-new-feature - Make your changes and ensure they follow the project's coding standards
- Add tests for your changes if applicable and run examples to verify functionality
- Run the test suite to ensure nothing is broken:
cargo test - Commit your changes with clear, descriptive commit messages
- Push to your fork and create a merge request
Development Setup
# Clone your fork
git clone https://gitlab.com/your-username/rmcp-actix-web.git
cd rmcp-actix-web
# Build the project
cargo build --workspace
# Run tests
cargo test
# Run examples
cargo run --example counter_streamable_http
Code Standards
- Follow Rust conventions and use
cargo fmtto format code - Run
cargo clippy --all-targetsto catch common mistakes - Add documentation for public APIs
- Include tests for new functionality
Reporting Issues
Found a bug or have a feature request? Please report it on our GitLab issue tracker.
Installation
Add this to your Cargo.toml:
[dependencies]
rmcp-actix-web = "0.13"
rmcp = "3"
actix-web = "4"
Feature Flags
| Feature | Default | Effect |
|---|---|---|
transport-streamable-http | yes | Enables the Streamable HTTP transport, which delegates every wire-protocol decision to rmcp's own StreamableHttpService. |
legacy-transport | no | Additionally exposes the hand-written transport that predates that delegation, at rmcp_actix_web::transport::legacy_streamable_http_server. |
authorization-token-passthrough | no | Forwards the Authorization header to the MCP service. Violates the MCP specification; see SECURITY.md. |
# Default: Streamable HTTP transport enabled
rmcp-actix-web = "0.13"
# Streamable HTTP transport (explicit)
rmcp-actix-web = { version = "0.13", default-features = false, features = ["transport-streamable-http"] }
# Forward Authorization headers to your MCP service (see SECURITY.md first)
rmcp-actix-web = { version = "0.13", features = ["authorization-token-passthrough"] }
legacy-transport
legacy-transport keeps the hand-written actix-web transport available alongside the
delegating one. Enable it only if you depend on its wire behaviour or on its flat
on_request extension shape, where handlers read hook-written values straight from
RequestContext::extensions. It is frozen and receives no new features, and it does not
support MCP 2026-07-28: its sessionless path serves every peer the legacy wire shape.
rmcp-actix-web = { version = "0.13", features = ["legacy-transport"] }
use rmcp_actix_web::transport::legacy_streamable_http_server::StreamableHttpService;
Compatibility Matrix
| rmcp-actix-web | rmcp |
|---|---|
| 0.13.x | 3.x |
| 0.6.1 | 0.6.3 |
| 0.4.2 | 0.6.1 |
| 0.2.2 | 0.3.0 |
| 0.2.x | 0.2.x |
| 0.1.x | 0.2.x |
Upgrading to 0.13
0.13 replaces this crate's hand-written Streamable HTTP transport with delegation to
rmcp's own StreamableHttpService. rmcp now makes every wire-protocol decision, and this
crate contributes actix-web Scope composition and the builder API. That makes rmcp's
responses the contract, which changes observable behaviour in the four areas below. All
of these changes are adopted deliberately; none of them are bugs.
Requests are now validated against Host
| Request | Before | Now |
|---|---|---|
Any request whose Host is outside rmcp's loopback-only default allow-list | served | 403 Forbidden, body Forbidden: Host header is not allowed |
Any request carrying no Host header at all | served | 400 Bad Request, body Bad Request: missing Host header |
This is a DNS-rebinding defence inherited from rmcp. Any deployment reachable under its
own hostname rejects every request until you configure .allowed_hosts(...). HTTP/1.1
clients always send Host, but actix's in-process test harness does not. See
Host and Origin Validation for how to configure both.
Session-handling status codes now follow rmcp
| Request | Before | Now | Why |
|---|---|---|---|
DELETE with an unknown session id | 404 Not Found | 202 Accepted, empty body | DELETE is idempotent: deleting a session that does not exist reaches the same end state as deleting one that does. |
DELETE with an empty Mcp-Session-Id | 400 Bad Request | 202 Accepted, empty body | Same idempotence, applied to an empty id: it is an id no session matches, not an absent one. |
POST or GET with an empty Mcp-Session-Id | 400 Bad Request | 404 Not Found, body Not Found: Session not found | An empty header value is not a missing header. It is a session id that no session matches, so the client can recover by re-initializing. |
POST with no session id that is not an initialize request | 400 Bad Request | 422 Unprocessable Entity, body Unexpected message, expect initialize request | The request is well-formed; it is the message that cannot be processed without a session. |
Two statuses are unchanged, but their bodies are rmcp's wording now: POST or GET
with an unknown non-empty session id still answers 404 Not Found, with the body
Not Found: Session not found rather than Session not found; GET or DELETE with no
session id at all still answers 400 Bad Request, with the body
Bad Request: Session ID is required rather than
Bad Request: Mcp-Session-Id header is required. Clients that match on body text rather
than status need updating.
Hook-written extensions are nested
rmcp hands your MCP service the whole http::request::Parts, so values written by the
on_request hook are reached one hop further in than before. Read them with
rmcp_actix_web::transport::on_request_extensions(&context.extensions) instead of
indexing context.extensions directly — see
Middleware Extension Propagation.
If you need the previous transport verbatim, including its flat extension shape, enable
the legacy-transport feature.
SSE keep-alive follows rmcp's default
A builder knob left unset is not written to rmcp's config, so it keeps whatever default
rmcp chose. Leaving sse_keep_alive unset therefore sends rmcp's own keep-alive interval.
| Request | Before | Now |
|---|---|---|
An SSE stream from a service that never set sse_keep_alive | no keep-alive comments | an empty SSE comment (: alone on a line) at rmcp's default interval |
An SSE stream from a service that called .maybe_sse_keep_alive(None) | no keep-alive comments | same as never setting it: rmcp's default interval |
Call .sse_keep_alive(Duration::from_secs(...)) for a specific interval, or
.disable_sse_keep_alive() for the previous behaviour of no keep-alive at all.
.maybe_sse_keep_alive(interval) still takes an Option<Duration> for a value that is
optional at runtime, but its None now means "leave the knob unset" rather than "no
keep-alive"; .disable_sse_keep_alive() is what turns keep-alive off. The same three-way
distinction applies to sse_retry, which gains .sse_retry(...),
.maybe_sse_retry(...) and .disable_sse_retry().
Quick Start
Framework-Level Composition
Mount MCP services at custom paths within existing actix-web applications:
use rmcp_actix_web::transport::{LocalSessionManager, StreamableHttpService};
use actix_web::{App, HttpServer, web};
use std::sync::Arc;
#[actix_web::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// StreamableHttp service with builder pattern (shared across workers)
let http_service = StreamableHttpService::builder()
.service_factory(Arc::new(|| Ok(MyMcpService::new())))
.session_manager(Arc::new(LocalSessionManager::default()))
.stateful_mode(true)
.build();
HttpServer::new(move || {
App::new()
// Your existing routes
.route("/health", web::get().to(|| async { "OK" }))
// Mount MCP service at custom path
.service(web::scope("/api/v1/mcp").service(http_service.clone().scope()))
})
.bind("127.0.0.1:8080")?
.run()
.await?;
Ok(())
}
Host and Origin Validation
By default, the transport accepts loopback hosts only — rmcp's
allowed_hosts default — and responds 403 Forbidden to
every other Host. This is a DNS-rebinding defence inherited from rmcp and is safe for
servers running on loopback, but it means any deployment reachable under its own
hostname rejects every request unless configured. Set .allowed_hosts(...) to the
hostnames or host:port authorities your deployment is reachable under:
let http_service = StreamableHttpService::builder()
.service_factory(Arc::new(|| Ok(MyMcpService::new())))
.session_manager(Arc::new(LocalSessionManager::default()))
.allowed_hosts(vec!["mcp.example.com".to_string()])
.build();
.allowed_origins(...) similarly restricts the inbound Origin header. Leaving it unset
inherits rmcp's own default, which performs no Origin validation.
The same defence also rejects a request that carries no Host header at all, with
400 Bad Request and the body Bad Request: missing Host header. Setting
allowed_hosts does not exempt a request from this: the header must be present for
there to be an authority to check. HTTP/1.1 clients always send Host, so this does not
affect ordinary traffic, but actix's in-process test harness does not — tests written
with actix_web::test::TestRequest must set the header explicitly:
let req = test::TestRequest::post()
.uri("/mcp/")
.insert_header(("host", "localhost"))
// ...
.to_request();
Both behaviours are new in 0.13; see Upgrading to 0.13 for the full list of changes an upgrade brings.
Examples
See the examples/ directory for complete working examples:
Basic Examples
counter_streamable_http.rs- Streamable HTTP server example
Composition Examples
composition_streamable_http_example.rs- StreamableHttp with custom mounting
Proxy Examples
authorization_proxy_example.rs- MCP service acting as a proxy using Authorization headers
Running Examples
# Basic StreamableHttp server
cargo run --example counter_streamable_http
# Framework composition with StreamableHttp
cargo run --example composition_streamable_http_example
# Authorization proxy example
cargo run --example authorization_proxy_example
Each example includes detailed documentation and curl commands for testing.
Key Features
Framework-Level Composition
- StreamableHttp:
StreamableHttpService::builder().build()with.scope()for composition - Custom Paths: Mount services at any path using actix-web's Scope system
- Builder API: Consistent builder pattern for service configuration
Protocol Support
- Full MCP Compatibility: Implements complete MCP protocol specification
- Bidirectional Communication: Both request/response and streaming patterns
- Session Management: Stateful and stateless modes for StreamableHttp
- Keep-Alive: Configurable keep-alive intervals for connection health
Integration
- Drop-in Replacement: Same service implementations work with Axum or actix-web
- Middleware Support: Full integration with actix-web middleware stack
- Custom Paths: Mount services at any path using actix-web's Scope system
- Built on actix-web: Leverages the mature actix-web framework
Middleware Extension Propagation
Use the on_request hook to propagate typed data from actix-web middleware to MCP request handlers. This is useful for passing JWT claims, user context, or other authentication data:
use rmcp_actix_web::transport::{LocalSessionManager, StreamableHttpService};
use actix_web::HttpMessage;
use std::sync::Arc;
#[derive(Clone)]
struct JwtClaims { user_id: String }
let http_service = StreamableHttpService::builder()
.service_factory(Arc::new(|| Ok(MyMcpService::new())))
.session_manager(Arc::new(LocalSessionManager::default()))
.on_request_fn(|http_req, ext| {
// Access data populated by actix-web middleware
if let Some(claims) = http_req.extensions().get::<JwtClaims>() {
ext.insert(claims.clone());
}
})
.build();
rmcp's transport nests the hook's extensions inside http::request::Parts, so read them back
in your MCP service handlers with rmcp_actix_web::transport::on_request_extensions(&context.extensions)
rather than reading context.extensions directly:
use rmcp_actix_web::transport::on_request_extensions;
async fn handle_request(
&self,
request: SomeRequest,
context: RequestContext<RoleServer>,
) -> Result<Response, McpError> {
if let Some(claims) = on_request_extensions(&context.extensions)
.and_then(|extensions| extensions.get::<JwtClaims>())
{
// ...
}
// ...
}
Proxy Support
- Authorization Forwarding: Bearer tokens from Authorization headers can be forwarded to MCP services (requires
authorization-token-passthroughfeature) - MCP Proxy Pattern: Enable MCP services to act as proxies to backend APIs
- Selective Header Forwarding: Only forwards Authorization header when feature is enabled
- Type-Safe Access: Read the forwarded header as an
AuthorizationHeaderviatransport::on_request_extensions(&context.extensions) - Security Notice: Token passthrough violates MCP specifications - see SECURITY.md for important details
License
MIT License - see LICENSE file for details.
References
Compatibility
- Runs on
- GPU support unknown
What shows it
- no dependency or feature matches a backend row
- Native code
- None found
- No native code found
- Python bindings
- No Python binding crate found
- License
- MIT
Compare with similar projects
Order: strongest tier first, then most downloads, then name, with unmeasured values last. A library counts downloads over 30 days. An app counts binary downloads per release. An app with none counts its crate downloads over 30 days. ML Rust runs this catalogue, so its closest project always gets a row.
| Project | Downloads | Dependents | Stars | Latest release | Runs on (GPU) | Native code | License |
|---|---|---|---|---|---|---|---|
|
actix-web transport implementations for RMCP (Rust Model Context Protocol) |
27,030 downloads over 30 days | 5 | Not measured | 0.13.2 | GPU support unknown | No native code found | MIT |
|
rmcp Featured
Rust SDK for Model Context Protocol |
12,149,426 downloads over 30 days | 2,400 | 4,101 | 3.9.0 | GPU support unknown | No native code found | Apache-2.0 |
|
agent-client-protocol Featured
Core protocol types and traits for the Agent Client Protocol |
941,949 downloads over 30 days | 145 | 245 | 5.0.0 | GPU support unknown | No native code found | Apache-2.0 |
|
rust-mcp-schema Featured
Type-safe implementation of the Model Context Protocol in Rust, designed to reduce errors and accelerate development with powerful utilities. |
107,187 downloads over 30 days | 19 | 79 | 2.0.0 | GPU support unknown | No native code found | MIT |
|
rust-mcp-sdk Featured
An asynchronous SDK and framework for building MCP-Servers and MCP-Clients, leveraging the rust-mcp-schema for type safe MCP Schema Objects. |
29,764 downloads over 30 days | 51 | 199 | 2.0.0 | GPU support unknown | No native code found | MIT |
|
Fast, lightweight Firecrawl and Tavily alternative in Rust for web scraping. |
444 binary downloads per release | Not applicable | 1,237 | 0.42.1 | GPU support unknown | Binds native code: btls-sys (feature impersonated) | AGPL-3.0 |
|
An open protocol that gives AI agents a secure, sandboxed place to run code and use tools — with lean context usage. AXP works alongside MCP |
Not measured | Not applicable | 3 | Not measured | Not measured | Not measured | Apache-2.0 |
Why ListedNo archive, New or Featured rule places it, so it is Listed.
What it lacks for Featured
- The check holds no value for GitHub stars, GitHub forks, Contributors with a GitHub account. A gate that lacks its signal does not pass.
What else the check found
- The last activity is on 2 Nov 2026, 7 days ago. The rules expect an update within 365 days. Dormancy rule
Not measured: GitHub stars, GitHub forks, Contributors with a GitHub account.
Check notes
- repo host gitlab.com has no facts source: repository signals unknown
- the repo manifest Cargo.toml declares the crate, and no release commit proves the link
Exact rules: Listed, Featured library. The check on applied thresholds version 9.
Every rule outcome, as the check records it
expected-interval: version 0.13.2 base 300, raised to the 365-day floor: E = 365 daysdormancy: last activity 2026-11-02, 7 days ago, E = 365: activefeatured: not featured: unknown stars, forks, contributorslisted: listed: no archive, new or featured rule applies
Measurements and sources
Each signal row shows the raw value, its source and the fetch date. The Sources list gathers every distinct source on this page. Movement needs more history.
Sources4 sources
- Description: the crates.io description
- crates.io database dump, fetched
- RustSec advisory database, fetched
- repo manifest, fetched
Adoption3 active dependents, 1 new dependents in 365 days1 not measured
| Signal | Value | Source | Fetched | Counts |
|---|---|---|---|---|
| Active dependents | 3 | crates.io database dump | Yes | |
| New dependents (365 days) | 1 | crates.io database dump | Yes | |
| Peak active dependents | 3 | crates.io database dump | Yes | |
| Active dependents as a share of the peak | 1 | crates.io database dump | Yes | |
| Binary downloads per release | Not measured | Yes |
MaintenanceNot measured
| Signal | Value | Source | Fetched | Counts |
|---|---|---|---|---|
| Latest human commit | Not measured | Yes | ||
| Latest release | 2026-11-02 | crates.io database dump | Yes | |
| First release | 2025-07-05 | crates.io database dump | Yes | |
| Latest version | 0.13.2 | crates.io database dump | Yes | |
| Days the oldest outside issue waits for a reply | Not measured | Yes | ||
| Maintainer replies over 365 days | Not measured | Yes | ||
| Repository archived | Not measured | Yes | ||
| Repository gone | Not measured | Yes | ||
| Release commit or manifest in the repo | true | repo manifest | Yes | |
| Published docs | true | crates.io database dump | Yes | |
| Latest release fails cargo check | Not measured | Yes | ||
| Build check date | Not measured | Yes | ||
| Build check toolchain | Not measured | Yes |
SecurityUnpatched vulnerabilities: none
| Signal | Value | Source | Fetched | Counts |
|---|---|---|---|---|
| Unpatched vulnerabilities past the grace window | none | RustSec advisory database | Yes | |
| RustSec unmaintained advisory | none | RustSec advisory database | Yes | |
| Deprecation notice | none | crates.io database dump | Yes | |
| Deprecated requirement | none | crates.io database dump | Yes |
Distribution69 releases, 27,030 downloads over 30 days5 not measured
| Signal | Value | Source | Fetched | Counts |
|---|---|---|---|---|
| Releases | 69 | crates.io database dump | Yes | |
| Downloads over 30 days | 27030 | crates.io database dump | Yes | |
| Binary downloads over the last 3 releases | Not measured | Yes | ||
| Homebrew installs on request over 365 days | Not measured | Yes | ||
| Repology repository families | Not measured | Yes | ||
| Docker pull growth over 90 days | Not measured | Yes | ||
| Docker pulls at the growth baseline | Not measured | Yes | ||
| Every version a prerelease | false | crates.io database dump | Yes | |
| Every version 0.0.x | false | crates.io database dump | Yes | |
| Every version yanked | false | crates.io database dump | Yes | |
| Crate deleted from crates.io | false | crates.io database dump | Yes |
RepositoryNot measured
| Signal | Value | Source | Fetched | Counts |
|---|---|---|---|---|
| GitHub stars | Not measured | Yes — one part of the repository route | ||
| GitHub forks | Not measured | Yes — one part of the repository route | ||
| Contributors with a GitHub account | Not measured | Yes — one part of the repository route | ||
| Commits on the default branch | Not measured | No | ||
| Repository's main crate left out of the entry | none | crates.io database dump | Yes |
Facts that never count
| Signal | Value | Source | Fetched | Counts |
|---|---|---|---|---|
| License | MIT | crates.io database dump | No | |
| Runs on, from listed crates and their direct dependencies | GPU support unknown | crates.io database dump | No | |
| What shows each backend | no dependency or feature matches a backend row | crates.io database dump | No | |
| Native code, from listed crates and their dependencies | No native code found | crates.io database dump | No | |
| Python bindings, from direct dependencies of listed crates | No Python binding crate found | crates.io database dump | No | |
| Crates that depend on a listed crate directly | 5 | crates.io database dump | No |
Corrections and review history
No correction applies to this entry.
Judgments
A judgment enters only through an approved issue and a merged pull request.
No judgment applies to this entry.