feat: align mcp transport with streamable http

This commit is contained in:
a.tolmachev
2026-04-06 09:49:22 +03:00
parent 633af39c82
commit d841cd0dda
5 changed files with 459 additions and 28 deletions
+207
View File
@@ -370,6 +370,213 @@ mod tests {
assert_eq!(tools_list["error"]["code"], -32002);
}
#[tokio::test]
async fn initialize_can_return_sse_response_when_client_prefers_event_stream() {
let registry = test_registry().await;
publish_agent_with_bindings(&registry, "sales-sse-init", vec![]).await;
let api_key = create_platform_api_key(
&registry,
"mcp-sse-init",
&[PlatformApiKeyScope::Read, PlatformApiKeyScope::Write],
)
.await;
let base_url = spawn_mcp_server(build_app(
registry,
Duration::from_millis(0),
Some("https://crank.example.com".to_owned()),
))
.await;
let client = reqwest::Client::new();
let response = client
.post(agent_mcp_url(&base_url, "sales-sse-init"))
.header(header::ACCEPT, "text/event-stream, application/json")
.header(header::AUTHORIZATION, format!("Bearer {api_key}"))
.json(&json!({
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-11-25"
}
}))
.send()
.await
.unwrap();
assert_eq!(response.status(), reqwest::StatusCode::OK);
assert_eq!(
response
.headers()
.get(header::CONTENT_TYPE)
.unwrap()
.to_str()
.unwrap(),
"text/event-stream"
);
assert!(response.headers().get("MCP-Session-Id").is_some());
let body = response.text().await.unwrap();
assert!(body.contains("\"jsonrpc\":\"2.0\""));
assert!(body.contains("\"protocolVersion\":\"2025-11-25\""));
}
#[tokio::test]
async fn get_opens_sse_stream_for_initialized_session() {
let registry = test_registry().await;
publish_agent_with_bindings(&registry, "sales-get-sse", vec![]).await;
let api_key = create_platform_api_key(
&registry,
"mcp-get-sse",
&[PlatformApiKeyScope::Read, PlatformApiKeyScope::Write],
)
.await;
let base_url = spawn_mcp_server(build_app(
registry,
Duration::from_millis(0),
Some("https://crank.example.com".to_owned()),
))
.await;
let client = reqwest::Client::new();
let mcp_url = agent_mcp_url(&base_url, "sales-get-sse");
let initialized_session = initialize_session(&client, &mcp_url, &api_key).await;
let response = client
.get(&mcp_url)
.header(header::ACCEPT, "text/event-stream")
.header(header::AUTHORIZATION, format!("Bearer {api_key}"))
.header("MCP-Session-Id", &initialized_session)
.header("MCP-Protocol-Version", "2025-11-25")
.send()
.await
.unwrap();
assert_eq!(response.status(), reqwest::StatusCode::OK);
assert_eq!(
response
.headers()
.get(header::CONTENT_TYPE)
.unwrap()
.to_str()
.unwrap(),
"text/event-stream"
);
assert_eq!(
response
.headers()
.get("MCP-Session-Id")
.unwrap()
.to_str()
.unwrap(),
initialized_session
);
}
#[tokio::test]
async fn get_requires_session_header() {
let registry = test_registry().await;
publish_agent_with_bindings(&registry, "sales-get-sse-missing", vec![]).await;
let api_key = create_platform_api_key(
&registry,
"mcp-get-sse-missing",
&[PlatformApiKeyScope::Read],
)
.await;
let base_url = spawn_mcp_server(build_app(
registry,
Duration::from_millis(0),
Some("https://crank.example.com".to_owned()),
))
.await;
let client = reqwest::Client::new();
let response = client
.get(agent_mcp_url(&base_url, "sales-get-sse-missing"))
.header(header::ACCEPT, "text/event-stream")
.header(header::AUTHORIZATION, format!("Bearer {api_key}"))
.send()
.await
.unwrap();
assert_eq!(response.status(), reqwest::StatusCode::BAD_REQUEST);
}
#[tokio::test]
async fn delete_terminates_transport_session() {
let registry = test_registry().await;
publish_agent_with_bindings(&registry, "sales-delete-session", vec![]).await;
let api_key = create_platform_api_key(
&registry,
"mcp-delete-session",
&[PlatformApiKeyScope::Read],
)
.await;
let base_url = spawn_mcp_server(build_app(
registry,
Duration::from_millis(0),
Some("https://crank.example.com".to_owned()),
))
.await;
let client = reqwest::Client::new();
let mcp_url = agent_mcp_url(&base_url, "sales-delete-session");
let initialized_session = initialize_session(&client, &mcp_url, &api_key).await;
let delete_response = client
.delete(&mcp_url)
.header(header::AUTHORIZATION, format!("Bearer {api_key}"))
.header("MCP-Session-Id", &initialized_session)
.header("MCP-Protocol-Version", "2025-11-25")
.send()
.await
.unwrap();
assert_eq!(delete_response.status(), reqwest::StatusCode::NO_CONTENT);
let after_delete = client
.get(&mcp_url)
.header(header::ACCEPT, "text/event-stream")
.header(header::AUTHORIZATION, format!("Bearer {api_key}"))
.header("MCP-Session-Id", &initialized_session)
.header("MCP-Protocol-Version", "2025-11-25")
.send()
.await
.unwrap();
assert_eq!(after_delete.status(), reqwest::StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn rejects_get_with_protocol_version_mismatch() {
let registry = test_registry().await;
publish_agent_with_bindings(&registry, "sales-get-bad-version", vec![]).await;
let api_key = create_platform_api_key(
&registry,
"mcp-get-bad-version",
&[PlatformApiKeyScope::Read],
)
.await;
let base_url = spawn_mcp_server(build_app(
registry,
Duration::from_millis(0),
Some("https://crank.example.com".to_owned()),
))
.await;
let client = reqwest::Client::new();
let mcp_url = agent_mcp_url(&base_url, "sales-get-bad-version");
let initialized_session = initialize_session(&client, &mcp_url, &api_key).await;
let response = client
.get(&mcp_url)
.header(header::ACCEPT, "text/event-stream")
.header(header::AUTHORIZATION, format!("Bearer {api_key}"))
.header("MCP-Session-Id", &initialized_session)
.header("MCP-Protocol-Version", "2025-06-18")
.send()
.await
.unwrap();
assert_eq!(response.status(), reqwest::StatusCode::BAD_REQUEST);
}
#[tokio::test]
async fn refreshes_published_tools_without_restart() {
let registry = test_registry().await;