rust-sdk/crates/rmcp/Cargo.toml
Amey Pawar bc2c5f3c4a
fix(transport): cancel in-flight request on stateless streamable-HTTP client disconnect (#857) (#967)
A stateless streamable-HTTP request is one-shot (no session, no resumption),
so if the client drops the response before the handler finishes, the request
is terminal and should be cancelled. Previously the handler kept running with
its `RequestContext::ct` never firing, so long-running or destructive tools
could not observe a client disconnect.

Give each stateless request its own cancellation token via
`serve_directly_with_ct` and cancel it when the client disconnects. This covers
both stateless paths: `serve_negotiated_request_directly` (per-request version
negotiation) and the non-negotiated path. In each:

- A disconnect while the handler is still producing its first message cancels
  it. The guard is disarmed once the handler emits anything, so a normal
  response is never cancelled.
- The SSE response stream is wrapped in a guard that cancels the handler if the
  stream is dropped before it ends naturally.
- When a negotiated request replies directly (JSON mode, or a non-OK status),
  the receiver is dropped, so a still-running handler is cancelled rather than
  left emitting into a closed channel. Without this its terminal send fails
  before adding the termination permit and the serve loop parks forever.

Stateful (resumable) mode is intentionally left unchanged: there a disconnect
may be recovered via `Last-Event-ID`, so cancelling on disconnect would break
resumption.

Adds a regression test covering both stateless sub-modes (SSE and JSON).
2026-07-17 09:06:44 -04:00

437 lines
12 KiB
TOML

[package]
name = "rmcp"
license = { workspace = true }
version = { workspace = true }
edition = { workspace = true }
repository = { workspace = true }
homepage = { workspace = true }
readme = { workspace = true }
description = "Rust SDK for Model Context Protocol"
documentation = "https://docs.rs/rmcp"
[lints.clippy]
exhaustive_structs = "warn"
exhaustive_enums = "warn"
[package.metadata.docs.rs]
features = [
"auth",
"auth-client-credentials-jwt",
"base64",
"client",
"client-side-sse",
"elicitation",
"macros",
"request-state",
"reqwest",
"reqwest-native-tls",
"reqwest-tls-no-provider",
"schemars",
"server",
"server-side-http",
"tower",
"transport-async-rw",
"transport-child-process",
"transport-io",
"transport-streamable-http-client",
"transport-streamable-http-client-reqwest",
"transport-streamable-http-client-unix-socket",
"transport-streamable-http-server",
"transport-streamable-http-server-session",
"transport-worker",
"uuid",
]
rustdoc-args = ["--cfg", "docsrs"]
[dependencies]
async-trait = "0.1.89"
serde = { version = "1.0", features = ["derive", "rc"] }
serde_json = "1.0"
thiserror = "2"
tokio = { version = "1", features = ["sync", "macros", "rt", "time"] }
futures = "0.3"
tracing = { version = "0.1" }
tokio-util = { version = "0.7" }
pin-project-lite = "0.2"
pastey = { version = "0.2.0", optional = true }
# oauth2 support
oauth2 = { version = "5.0", optional = true, default-features = false }
# JWT signing for client credentials (private_key_jwt)
jsonwebtoken = { version = "10", optional = true }
# for auto generate schema
schemars = { version = "1.0", optional = true, features = ["chrono04"] }
# for image encoding
base64 = { version = "0.22", optional = true }
# for SEP-2322 requestState integrity sealing (opt-in via the `request-state` feature)
hmac = { version = "0.13", optional = true }
sha2 = { version = "0.11", optional = true }
# for HTTP client
reqwest = { version = "0.13.2", default-features = false, features = [
"json",
"stream",
], optional = true }
sse-stream = { version = "0.2.4", optional = true }
http = { version = "1", optional = true }
url = { version = "2.4", optional = true }
# For tower compatibility
tower-service = { version = "0.3", optional = true }
# for child process transport
process-wrap = { version = "9.0", features = ["tokio1"], optional = true }
# for cross-platform executable path resolution
which = { version = "8", optional = true }
# for ws transport
# tokio-tungstenite ={ version = "0.26", optional = true }
# for http-server transport
rand = { version = "0.10", optional = true }
tokio-stream = { version = "0.1", optional = true }
uuid = { version = "1", features = ["v4"], optional = true }
http-body = { version = "1", optional = true }
http-body-util = { version = "0.1", optional = true }
bytes = { version = "1", optional = true }
# for unix socket transport
hyper = { version = "1", features = ["client", "http1"], optional = true }
hyper-util = { version = "0.1", features = ["tokio"], optional = true }
# macro
rmcp-macros = { workspace = true, optional = true }
[target.'cfg(not(all(target_family = "wasm", target_os = "unknown")))'.dependencies]
chrono = { version = "0.4.38", default-features = false, features = ["serde", "now"] }
[target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dependencies]
chrono = { version = "0.4.38", default-features = false, features = [
"serde",
"clock",
"std",
"oldtime",
] }
[features]
default = ["base64", "macros", "server"]
local = ["rmcp-macros?/local"]
client = ["dep:tokio-stream"]
server = ["transport-async-rw", "schemars", "dep:pastey"]
macros = ["dep:rmcp-macros", "dep:pastey"]
elicitation = ["dep:url"]
# SEP-2322 requestState integrity helper (HMAC-SHA256 seal/open codec)
request-state = ["dep:hmac", "dep:sha2", "base64"]
# reqwest http client
__reqwest = ["dep:reqwest"]
reqwest = ["__reqwest", "reqwest?/rustls"]
reqwest-tls-no-provider = ["__reqwest", "reqwest?/rustls-no-provider"]
reqwest-native-tls = ["__reqwest", "reqwest?/native-tls"]
server-side-http = [
"uuid",
"dep:rand",
"dep:tokio-stream",
"dep:http",
"dep:http-body",
"dep:http-body-util",
"dep:bytes",
"dep:sse-stream",
"tower",
"base64",
]
transport-worker = ["dep:tokio-stream"]
# SSE stream parsing utilities (used by streamable HTTP client for SSE-formatted responses)
client-side-sse = ["dep:sse-stream", "dep:http", "dep:bytes", "base64"]
# Streamable HTTP client
transport-streamable-http-client = ["client-side-sse", "transport-worker"]
transport-streamable-http-client-reqwest = ["transport-streamable-http-client", "__reqwest"]
transport-streamable-http-client-unix-socket = [
"transport-streamable-http-client",
"dep:hyper",
"dep:hyper-util",
"dep:http-body-util",
"dep:http",
"dep:bytes",
"tokio/net",
]
transport-async-rw = ["tokio/io-util", "tokio-util/codec"]
transport-io = ["transport-async-rw", "tokio/io-std"]
transport-child-process = [
"transport-async-rw",
"tokio/process",
"dep:process-wrap",
]
which-command = [
"transport-child-process",
"dep:which",
]
transport-streamable-http-server = [
"transport-streamable-http-server-session",
"server-side-http",
"transport-worker",
]
transport-streamable-http-server-session = [
"transport-async-rw",
"dep:tokio-stream",
]
# transport-ws = ["transport-io", "dep:tokio-tungstenite"]
tower = ["dep:tower-service"]
auth = ["dep:oauth2", "__reqwest", "dep:url"]
auth-client-credentials-jwt = ["auth", "dep:jsonwebtoken", "uuid"]
schemars = ["dep:schemars"]
[dev-dependencies]
tokio = { version = "1", features = ["full"] }
schemars = { version = "1.1.0", features = ["chrono04"] }
axum = { version = "0.8", default-features = false, features = ["http1", "tokio"] }
hyper = { version = "1", features = ["server", "http1"] }
hyper-util = { version = "0.1", features = ["tokio"] }
tower-service = "0.3"
url = "2.4"
anyhow = "1.0"
tracing-subscriber = { version = "0.3", features = [
"env-filter",
"std",
"fmt",
] }
async-trait = "0.1"
rstest = "0.26.1"
[[test]]
name = "test_tool_macros"
required-features = ["server", "client"]
path = "tests/test_tool_macros.rs"
[[test]]
name = "test_with_python"
required-features = [
"reqwest",
"server",
"client",
"transport-child-process",
]
path = "tests/test_with_python.rs"
[[test]]
name = "test_with_js"
required-features = [
"server",
"client",
"transport-child-process",
"transport-streamable-http-server",
"transport-streamable-http-client",
"__reqwest",
]
path = "tests/test_with_js.rs"
[[test]]
name = "test_notification"
required-features = ["server", "client"]
path = "tests/test_notification.rs"
[[test]]
name = "test_logging"
required-features = ["server", "client"]
path = "tests/test_logging.rs"
[[test]]
name = "test_message_protocol"
required-features = ["client"]
path = "tests/test_message_protocol.rs"
[[test]]
name = "test_message_schema"
required-features = ["server", "client", "schemars"]
path = "tests/test_message_schema.rs"
[[test]]
name = "test_progress_subscriber"
required-features = ["server", "client", "macros"]
path = "tests/test_progress_subscriber.rs"
[[test]]
name = "test_request_timeout_progress"
required-features = ["server", "client", "macros"]
path = "tests/test_request_timeout_progress.rs"
[[test]]
name = "test_elicitation"
required-features = ["elicitation", "client", "server"]
path = "tests/test_elicitation.rs"
[[test]]
name = "test_task"
required-features = ["server", "client", "macros"]
path = "tests/test_task.rs"
[[test]]
name = "test_streamable_http_priming"
required-features = ["server", "client", "transport-streamable-http-server", "reqwest"]
path = "tests/test_streamable_http_priming.rs"
[[test]]
name = "test_streamable_http_json_response"
required-features = ["server", "client", "transport-streamable-http-server", "reqwest"]
path = "tests/test_streamable_http_json_response.rs"
[[test]]
name = "test_streamable_http_protocol_version"
required-features = ["server", "client", "transport-streamable-http-server", "reqwest"]
path = "tests/test_streamable_http_protocol_version.rs"
[[test]]
name = "test_stateless_protocol_version"
required-features = ["server", "transport-streamable-http-server", "reqwest"]
path = "tests/test_stateless_protocol_version.rs"
[[test]]
name = "test_protocol_version_negotiation"
required-features = ["server", "client"]
path = "tests/test_protocol_version_negotiation.rs"
[[test]]
name = "test_client_lifecycle_modes"
required-features = ["client", "server"]
path = "tests/test_client_lifecycle_modes.rs"
[[test]]
name = "test_stateless_server_requests"
required-features = ["client", "server"]
path = "tests/test_stateless_server_requests.rs"
[[test]]
name = "test_discover_http_client_startup"
required-features = [
"client",
"reqwest",
"transport-streamable-http-client-reqwest",
"transport-streamable-http-server",
]
path = "tests/test_discover_http_client_startup.rs"
[[test]]
name = "test_streamable_http_standard_headers"
required-features = ["server", "client", "transport-streamable-http-server", "reqwest"]
path = "tests/test_streamable_http_standard_headers.rs"
[[test]]
name = "test_streamable_http_4xx_error_body"
required-features = ["transport-streamable-http-client", "transport-streamable-http-client-reqwest"]
path = "tests/test_streamable_http_4xx_error_body.rs"
[[test]]
name = "test_custom_request"
required-features = ["server", "client"]
path = "tests/test_custom_request.rs"
[[test]]
name = "test_trace_context"
required-features = ["server", "client"]
path = "tests/test_trace_context.rs"
[[test]]
name = "test_mrtr_behavior"
required-features = ["server", "client"]
path = "tests/test_mrtr_behavior.rs"
[[test]]
name = "test_prompt_macros"
required-features = ["server", "client"]
path = "tests/test_prompt_macros.rs"
[[test]]
name = "test_sampling"
required-features = ["server", "client"]
path = "tests/test_sampling.rs"
[[test]]
name = "test_close_connection"
required-features = ["server", "client"]
path = "tests/test_close_connection.rs"
[[test]]
name = "test_custom_headers"
required-features = [
"client",
"server",
"transport-streamable-http-client-reqwest",
"transport-streamable-http-server",
]
path = "tests/test_custom_headers.rs"
[[test]]
name = "test_sse_concurrent_streams"
required-features = ["server", "client", "transport-streamable-http-server", "transport-streamable-http-client", "reqwest"]
path = "tests/test_sse_concurrent_streams.rs"
[[test]]
name = "test_client_credentials"
required-features = ["auth"]
path = "tests/test_client_credentials.rs"
[[test]]
name = "test_unix_socket_transport"
required-features = [
"client",
"server",
"transport-streamable-http-client-unix-socket",
]
path = "tests/test_unix_socket_transport.rs"
[[test]]
name = "test_streamable_http_stale_session"
required-features = [
"server",
"client",
"transport-streamable-http-server",
"transport-streamable-http-client",
"transport-streamable-http-client-reqwest"
]
path = "tests/test_streamable_http_stale_session.rs"
[[test]]
name = "test_streamable_http_session_store"
required-features = [
"client",
"server",
"transport-streamable-http-client-reqwest",
"transport-streamable-http-server",
]
path = "tests/test_streamable_http_session_store.rs"
[[test]]
name = "test_streamable_http_connection_reuse"
required-features = [
"server",
"client",
"macros",
"schemars",
"transport-streamable-http-server",
"transport-streamable-http-client",
"transport-streamable-http-client-reqwest",
]
path = "tests/test_streamable_http_connection_reuse.rs"
[[test]]
name = "test_streamable_http_disconnect_cancel"
required-features = [
"server",
"transport-streamable-http-server",
"reqwest",
]
path = "tests/test_streamable_http_disconnect_cancel.rs"