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).
437 lines
12 KiB
TOML
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"
|