MCP Authentication
By default the MCP endpoint (POST /mcp) is open to any caller. Call .require_bearer(token) to gate every request behind a static Bearer token, or .require_oauth(provider, audience) (sso feature) for full per-user OAuth 2.0 / OIDC bearer JWT verification — see OAuth 2.0 authorization below.
require_bearer
use rust_web_server::mcp::McpServer;
let mcp = McpServer::new("my-server", "1.0") .require_bearer(std::env::var("MCP_TOKEN").expect("MCP_TOKEN not set"));Every POST /mcp request must include:
Authorization: Bearer <token>A missing or incorrect token produces an immediate 401 Unauthorized before any JSON-RPC processing:
HTTP/1.1 401 UnauthorizedWWW-Authenticate: BearerContent-Type: text/plain
UnauthorizedLoading the token from an environment variable
Never hard-code the token in source code. The conventional variable is MCP_TOKEN:
use rust_web_server::app::App;use rust_web_server::core::New;
let mcp = App::new() .mcp("my-server", "1.0") .require_bearer( std::env::var("MCP_TOKEN").expect("MCP_TOKEN env var not set"), );Set the variable before starting the server:
export MCP_TOKEN="$(openssl rand -hex 32)"cargo runThe bundled rws binary reads MCP_TOKEN automatically when the config file enables the built-in MCP server.
Configuring Claude Desktop
Add the Authorization header to the server entry in claude_desktop_config.json:
{ "mcpServers": { "my-server": { "url": "http://localhost:7878/mcp", "headers": { "Authorization": "Bearer <your-token-here>" } } }}Replace <your-token-here> with the value of MCP_TOKEN.
Full example
use rust_web_server::server::Server;use rust_web_server::mcp::{McpServer, McpContent};
# #[cfg(not(feature = "http2"))]# fn main() {let token = std::env::var("MCP_TOKEN") .expect("Set MCP_TOKEN before starting the server");
let mcp = McpServer::new("secure-server", "1.0") .require_bearer(token) .tool( "ping", "Returns pong", r#"{"type":"object","properties":{}}"#, |_| Ok(McpContent::text("pong")), );
let (listener, pool) = Server::setup().unwrap();Server::run(listener, pool, mcp);# }CORS preflight
OPTIONS /mcp requests always receive 200 OK without checking the Bearer token. This allows browser-based MCP clients to complete the CORS preflight before attaching credentials.
OAuth 2.0 authorization (MCP 2025-03-26)
The 2025-03-26 revision of the MCP spec defines an OAuth 2.0 authorization flow for multi-tenant or enterprise deployments, where each connecting user authenticates independently rather than sharing one static token. .require_oauth(provider, audience) (sso feature) implements this by verifying the client’s bearer token as a signed JWT against a live JWKS endpoint — reusing sso::jwks::JwksCache, the same verifier OidcAuth and AuthServer use elsewhere in this crate.
[dependencies]rust-web-server = { version = "17", features = ["sso"] }use rust_web_server::app::App;use rust_web_server::core::New;use rust_web_server::sso::OidcProvider;
let app = App::new() .mcp("my-server", "1.0") .require_oauth(OidcProvider::google(), "my-mcp-client-id");provider supplies the issuer, JWKS URI, and (for the metadata endpoint below) the authorization/token endpoints — use a preset (OidcProvider::google(), ::okta(domain), ::keycloak(base, realm), etc.) or discover them live:
use rust_web_server::sso::OidcProvider;
let provider = OidcProvider::discover("https://accounts.example.com")?;let app = mcp.require_oauth(provider, "my-mcp-client-id");A request with a missing, malformed, or invalid-signature bearer token gets the same 401 Unauthorized / WWW-Authenticate: Bearer response .require_bearer() returns — only the verification underneath differs.
Reading verified claims
On success, the verified claims (a serialized OidcClaims) populate McpContext.auth_claims as a JSON string, readable from any .tool_with_context() handler:
use rust_web_server::mcp::{McpContent, McpServer};
let mcp = McpServer::new("my-server", "1.0") .require_oauth(rust_web_server::sso::OidcProvider::google(), "my-mcp-client-id") .tool_with_context("whoami", "Report the caller", "{}", |ctx, _args| { let claims = ctx.auth_claims.unwrap_or_default(); Ok(McpContent::text(claims)) });Discovery endpoint
GET /.well-known/oauth-authorization-server is served automatically whenever .require_oauth() is configured:
{ "issuer": "https://accounts.google.com", "authorization_endpoint": "https://accounts.google.com/o/oauth2/v2/auth", "token_endpoint": "https://oauth2.googleapis.com/token", "jwks_uri": "https://www.googleapis.com/oauth2/v3/certs", "response_types_supported": ["code"]}If .require_oauth() isn’t configured, this path isn’t handled specially — it falls through to the fallback app (or the built-in App’s 404) like any other non-MCP path.
Combining with require_bearer
If both .require_bearer(token) and .require_oauth(provider, audience) are configured on the same server, OAuth verification takes precedence and the static token is never checked. This isn’t a supported combination to actually run in production — pick one — it’s just an unambiguous tie-break so the behavior isn’t undefined if both happen to be set.