pub mod adapter;
pub mod builder;
pub mod error;
pub mod handler;
pub mod prelude;
pub mod server;
#[cfg(feature = "cors")]
pub mod cors;
#[cfg(feature = "sse")]
pub mod streaming;
pub use builder::LambdaMcpServerBuilder;
pub use error::{LambdaError, Result};
pub use handler::LambdaMcpHandler;
pub use server::LambdaMcpServer;
#[cfg(feature = "cors")]
pub use cors::CorsConfig;
#[derive(Debug)]
enum RuntimeEventClassification {
ApiGatewayEvent(Box<lambda_http::request::LambdaRequest>),
StreamingCompletion,
UnrecognizedEvent,
}
fn classify_runtime_event(payload: serde_json::Value) -> RuntimeEventClassification {
if let Ok(request) =
serde_json::from_value::<lambda_http::request::LambdaRequest>(payload.clone())
{
return RuntimeEventClassification::ApiGatewayEvent(Box::new(request));
}
if payload.get("invokeCompletionStatus").is_some() {
return RuntimeEventClassification::StreamingCompletion;
}
RuntimeEventClassification::UnrecognizedEvent
}
type StreamBody = http_body_util::combinators::UnsyncBoxBody<bytes::Bytes, hyper::Error>;
type StreamResult = lambda_runtime::StreamResponse<http_body_util::BodyDataStream<StreamBody>>;
struct HandleResult {
response: StreamResult,
event_type: &'static str,
}
async fn handle_runtime_payload<F, Fut>(
payload: serde_json::Value,
context: lambda_runtime::Context,
dispatch: F,
) -> std::result::Result<HandleResult, lambda_http::Error>
where
F: FnOnce(lambda_http::Request) -> Fut,
Fut: std::future::Future<
Output = std::result::Result<http::Response<StreamBody>, lambda_http::Error>,
>,
{
match classify_runtime_event(payload) {
RuntimeEventClassification::ApiGatewayEvent(lambda_request) => {
use lambda_http::RequestExt;
let request: lambda_http::Request = (*lambda_request).into();
let request = request.with_lambda_context(context);
let response = dispatch(request).await?;
Ok(HandleResult {
response: into_lambda_stream_response(response),
event_type: "api_gateway_event",
})
}
RuntimeEventClassification::StreamingCompletion => Ok(HandleResult {
response: into_lambda_stream_response(empty_streaming_response()),
event_type: "streaming_completion",
}),
RuntimeEventClassification::UnrecognizedEvent => Ok(HandleResult {
response: into_lambda_stream_response(empty_streaming_response()),
event_type: "unrecognized_lambda_payload",
}),
}
}
fn event_log_level(event_type: &str) -> Option<tracing::Level> {
match event_type {
"streaming_completion" => Some(tracing::Level::DEBUG),
"unrecognized_lambda_payload" => Some(tracing::Level::WARN),
_ => None,
}
}
pub async fn run_streaming(
handler: LambdaMcpHandler,
) -> std::result::Result<(), lambda_http::Error> {
use lambda_runtime::{LambdaEvent, service_fn};
lambda_runtime::run(service_fn(move |event: LambdaEvent<serde_json::Value>| {
let handler = handler.clone();
async move {
let result = handle_runtime_payload(event.payload, event.context, |req| {
handler.handle_streaming(req)
})
.await?;
match event_log_level(result.event_type) {
Some(level) if level == tracing::Level::WARN => {
tracing::warn!(
event_type = result.event_type,
"Received unrecognized Lambda invocation payload"
);
}
Some(_) => {
tracing::debug!(
event_type = result.event_type,
"Acknowledging streaming completion"
);
}
None => {}
}
Ok::<_, lambda_http::Error>(result.response)
}
}))
.await
}
pub async fn run_streaming_with<F, Fut>(dispatch: F) -> std::result::Result<(), lambda_http::Error>
where
F: Fn(lambda_http::Request) -> Fut + Clone + Send + 'static,
Fut: std::future::Future<
Output = std::result::Result<http::Response<StreamBody>, lambda_http::Error>,
> + Send,
{
use lambda_runtime::{LambdaEvent, service_fn};
lambda_runtime::run(service_fn(move |event: LambdaEvent<serde_json::Value>| {
let dispatch = dispatch.clone();
async move {
let result = handle_runtime_payload(event.payload, event.context, dispatch).await?;
match event_log_level(result.event_type) {
Some(level) if level == tracing::Level::WARN => {
tracing::warn!(
event_type = result.event_type,
"Received unrecognized Lambda invocation payload"
);
}
Some(_) => {
tracing::debug!(
event_type = result.event_type,
"Acknowledging streaming completion"
);
}
None => {}
}
Ok::<_, lambda_http::Error>(result.response)
}
}))
.await
}
fn into_lambda_stream_response<B>(
response: http::Response<B>,
) -> lambda_runtime::StreamResponse<http_body_util::BodyDataStream<B>>
where
B: http_body::Body + Unpin + Send + 'static,
{
let (parts, body) = response.into_parts();
let mut headers = parts.headers;
let cookies = headers
.get_all(http::header::SET_COOKIE)
.iter()
.map(|c| String::from_utf8_lossy(c.as_bytes()).to_string())
.collect::<Vec<_>>();
headers.remove(http::header::SET_COOKIE);
lambda_runtime::StreamResponse {
metadata_prelude: lambda_runtime::MetadataPrelude {
headers,
status_code: parts.status,
cookies,
},
stream: http_body_util::BodyDataStream::new(body),
}
}
fn empty_streaming_response()
-> http::Response<http_body_util::combinators::UnsyncBoxBody<bytes::Bytes, hyper::Error>> {
use http_body_util::{BodyExt, Full};
let body = Full::new(bytes::Bytes::new())
.map_err(|e: std::convert::Infallible| match e {})
.boxed_unsync();
http::Response::builder().status(200).body(body).unwrap()
}
#[cfg(test)]
mod streaming_completion_tests {
use super::*;
use serde_json::json;
fn load_fixture(name: &str) -> serde_json::Value {
let json_str = match name {
"apigw_v1" => include_str!("fixtures/apigw_v1_proxy_event.json"),
"apigw_v2" => include_str!("fixtures/apigw_v2_http_api_event.json"),
"completion_success" => include_str!("fixtures/streaming_completion_success.json"),
"completion_failure" => include_str!("fixtures/streaming_completion_failure.json"),
"completion_extra" => include_str!("fixtures/streaming_completion_extra_fields.json"),
"completion_api_like" => {
include_str!("fixtures/completion_with_api_like_fields.json")
}
other => panic!("Unknown fixture: {other}"),
};
serde_json::from_str(json_str).unwrap_or_else(|e| panic!("Bad fixture {name}: {e}"))
}
#[test]
fn test_classify_api_gateway_v1_event() {
let payload = load_fixture("apigw_v1");
assert!(
matches!(
classify_runtime_event(payload),
RuntimeEventClassification::ApiGatewayEvent(_)
),
"API Gateway v1 proxy event must classify as ApiGatewayEvent"
);
}
#[test]
fn test_classify_api_gateway_v2_event() {
let payload = load_fixture("apigw_v2");
assert!(
matches!(
classify_runtime_event(payload),
RuntimeEventClassification::ApiGatewayEvent(_)
),
"API Gateway v2 HTTP API event must classify as ApiGatewayEvent"
);
}
#[test]
fn test_classify_streaming_completion() {
let payload = load_fixture("completion_success");
assert!(matches!(
classify_runtime_event(payload),
RuntimeEventClassification::StreamingCompletion
));
}
#[test]
fn test_classify_completion_failure_status() {
let payload = load_fixture("completion_failure");
assert!(matches!(
classify_runtime_event(payload),
RuntimeEventClassification::StreamingCompletion
));
}
#[test]
fn test_classify_completion_extra_fields() {
let payload = load_fixture("completion_extra");
assert!(matches!(
classify_runtime_event(payload),
RuntimeEventClassification::StreamingCompletion
));
}
#[test]
fn test_classify_completion_with_api_like_fields() {
let payload = load_fixture("completion_api_like");
assert!(matches!(
classify_runtime_event(payload),
RuntimeEventClassification::StreamingCompletion
));
}
#[test]
fn test_classify_empty_object() {
assert!(matches!(
classify_runtime_event(json!({})),
RuntimeEventClassification::UnrecognizedEvent
));
}
#[test]
fn test_classify_random_object() {
assert!(matches!(
classify_runtime_event(json!({"foo": "bar", "baz": 123})),
RuntimeEventClassification::UnrecognizedEvent
));
}
#[test]
fn test_classify_null_payload() {
assert!(matches!(
classify_runtime_event(json!(null)),
RuntimeEventClassification::UnrecognizedEvent
));
}
#[test]
fn test_classify_string_payload() {
assert!(matches!(
classify_runtime_event(json!("hello")),
RuntimeEventClassification::UnrecognizedEvent
));
}
#[test]
fn test_classify_array_payload() {
assert!(matches!(
classify_runtime_event(json!([1, 2, 3])),
RuntimeEventClassification::UnrecognizedEvent
));
}
#[test]
fn test_classify_nested_invoke_status() {
let payload = json!({
"data": {"invokeCompletionStatus": "Success"}
});
assert!(matches!(
classify_runtime_event(payload),
RuntimeEventClassification::UnrecognizedEvent
));
}
#[test]
fn test_classify_never_panics_on_arbitrary_json() {
let payloads = vec![
json!(null),
json!(true),
json!(false),
json!(42),
json!(-1.5),
json!(""),
json!("some string"),
json!([]),
json!([1, "two", null, false]),
json!({}),
json!({"a": 1}),
json!({"requestContext": null}),
json!({"requestContext": "not-an-object"}),
json!({"httpMethod": "POST"}),
json!({"version": "2.0"}),
json!({"version": "2.0", "routeKey": "GET /"}),
json!({"resource": "/", "httpMethod": "GET"}),
json!({"deeply": {"nested": {"invokeCompletionStatus": "Success"}}}),
serde_json::Value::Object((0..100).map(|i| (format!("key_{i}"), json!(i))).collect()),
];
for payload in payloads {
let _result = classify_runtime_event(payload);
}
}
#[test]
fn test_classify_invoke_completion_status_always_wins() {
let payloads = vec![
json!({"invokeCompletionStatus": "Success"}),
json!({"invokeCompletionStatus": "Failure"}),
json!({"invokeCompletionStatus": "Unknown"}),
json!({"invokeCompletionStatus": 42}),
json!({"invokeCompletionStatus": null}),
json!({"invokeCompletionStatus": "Success", "requestId": "abc-123"}),
json!({"invokeCompletionStatus": "Success", "extra": "field", "nested": {"a": 1}}),
];
for payload in payloads {
let result = classify_runtime_event(payload.clone());
assert!(
matches!(result, RuntimeEventClassification::StreamingCompletion),
"Payload with top-level invokeCompletionStatus must be StreamingCompletion: {payload}"
);
}
}
#[test]
fn test_unrecognized_logs_at_warn_level() {
assert_eq!(
event_log_level("unrecognized_lambda_payload"),
Some(tracing::Level::WARN)
);
}
#[test]
fn test_completion_logs_at_debug_level() {
assert_eq!(
event_log_level("streaming_completion"),
Some(tracing::Level::DEBUG)
);
}
#[test]
fn test_api_gateway_has_no_extra_logging() {
assert_eq!(event_log_level("api_gateway_event"), None);
}
#[tokio::test]
async fn test_handle_completion_does_not_dispatch() {
let dispatched = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let dispatched_clone = dispatched.clone();
let result = handle_runtime_payload(
load_fixture("completion_success"),
lambda_runtime::Context::default(),
|_req| {
let d = dispatched_clone.clone();
async move {
d.store(true, std::sync::atomic::Ordering::SeqCst);
Ok(empty_streaming_response())
}
},
)
.await
.expect("handle should succeed");
assert!(
!dispatched.load(std::sync::atomic::Ordering::SeqCst),
"Completion events must not dispatch to handler"
);
assert_eq!(result.event_type, "streaming_completion");
assert_eq!(result.response.metadata_prelude.status_code, 200);
}
#[tokio::test]
async fn test_handle_unrecognized_does_not_dispatch() {
let dispatched = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let dispatched_clone = dispatched.clone();
let result = handle_runtime_payload(
json!({"foo": "bar"}),
lambda_runtime::Context::default(),
|_req| {
let d = dispatched_clone.clone();
async move {
d.store(true, std::sync::atomic::Ordering::SeqCst);
Ok(empty_streaming_response())
}
},
)
.await
.expect("handle should succeed");
assert!(
!dispatched.load(std::sync::atomic::Ordering::SeqCst),
"Unrecognized events must not dispatch to handler"
);
assert_eq!(result.event_type, "unrecognized_lambda_payload");
assert_eq!(result.response.metadata_prelude.status_code, 200);
}
#[tokio::test]
async fn test_handle_apigw_v1_dispatches() {
let dispatched = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let dispatched_clone = dispatched.clone();
let result = handle_runtime_payload(
load_fixture("apigw_v1"),
lambda_runtime::Context::default(),
|_req| {
let d = dispatched_clone.clone();
async move {
d.store(true, std::sync::atomic::Ordering::SeqCst);
Ok(empty_streaming_response())
}
},
)
.await
.expect("handle should succeed");
assert!(
dispatched.load(std::sync::atomic::Ordering::SeqCst),
"API Gateway v1 events must dispatch to handler"
);
assert_eq!(result.event_type, "api_gateway_event");
}
#[tokio::test]
async fn test_handle_apigw_v2_dispatches() {
let dispatched = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let dispatched_clone = dispatched.clone();
let result = handle_runtime_payload(
load_fixture("apigw_v2"),
lambda_runtime::Context::default(),
|_req| {
let d = dispatched_clone.clone();
async move {
d.store(true, std::sync::atomic::Ordering::SeqCst);
Ok(empty_streaming_response())
}
},
)
.await
.expect("handle should succeed");
assert!(
dispatched.load(std::sync::atomic::Ordering::SeqCst),
"API Gateway v2 events must dispatch to handler"
);
assert_eq!(result.event_type, "api_gateway_event");
}
#[tokio::test]
async fn test_handle_unrecognized_surfaces_distinct_event_type() {
let result = handle_runtime_payload(
json!({"unknown": true}),
lambda_runtime::Context::default(),
|_req| async { Ok(empty_streaming_response()) },
)
.await
.expect("handle should succeed");
assert_eq!(result.event_type, "unrecognized_lambda_payload");
}
#[test]
fn test_empty_streaming_response() {
let resp = empty_streaming_response();
assert_eq!(resp.status(), 200);
}
#[test]
fn test_into_lambda_stream_response_preserves_metadata() {
use http_body_util::{BodyExt, Full};
let response = http::Response::builder()
.status(401)
.header("WWW-Authenticate", "Bearer realm=\"mcp\"")
.header("X-Custom", "test")
.body(
Full::new(bytes::Bytes::from("Unauthorized"))
.map_err(|e: std::convert::Infallible| match e {})
.boxed_unsync(),
)
.unwrap();
let stream_resp = into_lambda_stream_response(response);
assert_eq!(stream_resp.metadata_prelude.status_code, 401);
assert_eq!(
stream_resp
.metadata_prelude
.headers
.get("WWW-Authenticate")
.unwrap(),
"Bearer realm=\"mcp\""
);
assert_eq!(
stream_resp
.metadata_prelude
.headers
.get("X-Custom")
.unwrap(),
"test"
);
}
#[test]
fn test_into_lambda_stream_response_extracts_cookies() {
use http_body_util::{BodyExt, Full};
let response = http::Response::builder()
.status(200)
.header("Set-Cookie", "session=abc; Path=/")
.header("Set-Cookie", "theme=dark")
.body(
Full::new(bytes::Bytes::new())
.map_err(|e: std::convert::Infallible| match e {})
.boxed_unsync(),
)
.unwrap();
let stream_resp = into_lambda_stream_response(response);
assert_eq!(stream_resp.metadata_prelude.cookies.len(), 2);
assert!(
stream_resp
.metadata_prelude
.cookies
.contains(&"session=abc; Path=/".to_string())
);
assert!(
stream_resp
.metadata_prelude
.cookies
.contains(&"theme=dark".to_string())
);
assert!(
stream_resp
.metadata_prelude
.headers
.get("Set-Cookie")
.is_none()
);
}
#[tokio::test]
async fn test_run_streaming_with_dispatches_apigw_events() {
let dispatched = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let dispatched_clone = dispatched.clone();
let dispatch = move |_req: lambda_http::Request| {
let d = dispatched_clone.clone();
async move {
d.store(true, std::sync::atomic::Ordering::SeqCst);
Ok(empty_streaming_response())
}
};
let result = handle_runtime_payload(
load_fixture("apigw_v1"),
lambda_runtime::Context::default(),
dispatch,
)
.await
.expect("handle should succeed");
assert!(
dispatched.load(std::sync::atomic::Ordering::SeqCst),
"run_streaming_with dispatch must be called for API Gateway events"
);
assert_eq!(result.event_type, "api_gateway_event");
}
#[tokio::test]
async fn test_run_streaming_with_acks_completion_without_dispatch() {
let dispatched = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let dispatched_clone = dispatched.clone();
let dispatch = move |_req: lambda_http::Request| {
let d = dispatched_clone.clone();
async move {
d.store(true, std::sync::atomic::Ordering::SeqCst);
Ok(empty_streaming_response())
}
};
let result = handle_runtime_payload(
load_fixture("completion_success"),
lambda_runtime::Context::default(),
dispatch,
)
.await
.expect("handle should succeed");
assert!(
!dispatched.load(std::sync::atomic::Ordering::SeqCst),
"run_streaming_with dispatch must NOT be called for completion events"
);
assert_eq!(result.event_type, "streaming_completion");
assert_eq!(result.response.metadata_prelude.status_code, 200);
}
}