Skip to content

Commit 6495da1

Browse files
committed
feat: client subscriptions/listen; ADR-025 shim cut; final release-prep reconciliation
- Client-side subscriptions/listen: McpClient::subscriptions_listen(filter) on 2026 connections, via the new additive Transport::send_request_streaming (Streamable HTTP implements it; default is unsupported for non-HTTP transports). Consumes/validates the mandatory acknowledgement first; SubscriptionStream exposes the honored filter + subscription id and yields notifications; drop = close = cancellation. e2e: real client <-> real server, cross-request broadcast, only the opted-in type arrives, stamped with the subscription id. - ADR-025 framework shim cut: the four non-frozen framework crates (+ the 2026-default middleware-auth-lambda example) depend on turul-rpc directly (146 path swaps; shim mirrors turul-rpc paths 1:1, identical types — no public API change; cargo tree confirms turul-mcp-server is shim-free). Shim stays in-workspace for the frozen 2025 snapshots and 2025-pinned test/example crates only, manifest repinned to the terminal 0.3.47 (the 0.4.0 sweep value was never publishable). ADR-025 revision log records the cut; ADR-027 records the 2025 regression lane as the per-crate matrix (workspace-wide flag sweeps trip the spec mutex by design — closure by revision with rationale). - Release-readiness review: final status banner with the true remaining exceptions only (audit-journal P2 lows, requestState integrity = tool-author concern, cyclic/non-local $ref = documented rejections, client-side CIMD = future OAuth client flow, final disposition = maintainer's call per the Branch Lock).
1 parent d6be368 commit 6495da1

35 files changed

Lines changed: 454 additions & 229 deletions

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3737

3838
### Added (2026-06-10, release-prep sweep)
3939

40+
- **Client-side `subscriptions/listen`**`McpClient::subscriptions_listen(filter)` (2026 connections only) opens the long-lived stream via a new additive `Transport::send_request_streaming` (Streamable HTTP implements it; other transports default to unsupported). The client consumes and validates the mandatory acknowledgement first and returns a `SubscriptionStream` exposing the honored filter subset and the subscription id, then yields each notification; dropping the stream closes it (= cancellation per Streamable HTTP). e2e: real client ↔ real server — open stream, trigger server-wide broadcasts from a second request, receive only the opted-in type stamped with the subscription id.
41+
- **ADR-025 framework shim cut landed**`turul-mcp-server`/`turul-http-mcp-server`/`turul-mcp-builders`/`turul-mcp-aws-lambda` now depend on `turul-rpc` directly (146 path swaps; the shim mirrors `turul-rpc` paths 1:1 and re-exports the same types, so no public API changed). The shim remains in-workspace solely for the frozen 2025 protocol snapshots and 2025-pinned test/example crates, with its manifest restored to the terminal `0.3.47` (the mechanical 0.4.0 sweep value was never publishable). ADR-025/ADR-027 revision logs updated; the 2025 regression lane is recorded as the per-crate matrix (a workspace-wide flag sweep trips the spec mutex by design).
42+
4043
- **MRTR on `resources/read` and `prompts/get`** — completes the SEP-2322 triple (the only methods permitted to return `input_required`). The conversion + client-capability gate is now one shared helper (`handlers::input_required_to_result`, also adopted by `tools/call`). Resources surface the retry's `inputResponses`/`requestState` via the session extensions (same as tools); prompts receive them in the render args under reserved `io.modelcontextprotocol/*` keys, because `McpPrompt::render` has no session parameter and changing it would break the public trait (documented on the trait; reserved-namespace keys cannot collide with wire prompt arguments, which are plain strings). Tests: two-leg round trips for both methods + a `-32003`/400 capability-gate case on `resources/read` (all real-HTTP; the handlers previously leaked the sentinel to `-32603`).
4144
- **`resources.subscribe` capability truthfulness** — with `subscriptions/listen` serving per-URI `resources/updated`, both capability-construction sites now advertise `subscribe: true` on the 2026 lane (still `false` on 2025, which has no `resources/subscribe` handler). Wire test asserts the `server/discover` advertisement.
4245
- **`completion/complete` e2e coverage** — sessionless dispatch + `CompleteResult` wire shape (`completion.values`) + capability advertisement, closing the last zero-e2e core method on the 2026 path.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ turul-rpc = { version = "0.2", default-features = false }
211211
# path+version resolution. Frozen historical protocol crates (`2025-06-18`,
212212
# `2025-11-25`) stay at their last 0.3.x publish — everything else moves on
213213
# its own cadence.
214-
turul-mcp-json-rpc-server = { version = "0.4.0", path = "crates/turul-mcp-json-rpc-server" }
214+
turul-mcp-json-rpc-server = { version = "0.3.47", path = "crates/turul-mcp-json-rpc-server" }
215215
turul-mcp-protocol-2025-06-18 = { version = "0.3.47", path = "crates/turul-mcp-protocol-2025-06-18" }
216216
turul-mcp-protocol-2025-11-25 = { version = "0.3.47", path = "crates/turul-mcp-protocol-2025-11-25" }
217217
turul-mcp-protocol-2026-07-28 = { version = "0.4.0", path = "crates/turul-mcp-protocol-2026-07-28" }

crates/turul-http-mcp-server/Cargo.toml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,7 @@ protocol-2026-07-28 = ["turul-mcp-protocol/protocol-2026-07-28", "turul-mcp-sess
1515
sse = [] # Server-Sent Events support
1616

1717
[dependencies]
18-
turul-mcp-json-rpc-server = { workspace = true, features = [
19-
"streams",
20-
] }
18+
turul-rpc = { workspace = true, features = ["async", "streams"] }
2119
turul-mcp-protocol = { path = "../turul-mcp-protocol", version = "0.4.0", default-features = false }
2220
turul-mcp-session-storage = { path = "../turul-mcp-session-storage", version = "0.4.0", default-features = false }
2321
serde.workspace = true

crates/turul-http-mcp-server/src/json_rpc_responses.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,7 @@ use hyper::{Response, StatusCode, header};
99
use serde_json::Value;
1010
use tracing::error;
1111

12-
use turul_mcp_json_rpc_server::{
13-
JsonRpcError, JsonRpcResponse, error::JsonRpcErrorObject, types::RequestId,
14-
};
12+
use turul_rpc::{JsonRpcError, JsonRpcResponse, error::JsonRpcErrorObject, types::RequestId};
1513

1614
/// HTTP body type for JSON-RPC responses
1715
type JsonRpcBody = Full<Bytes>;

crates/turul-http-mcp-server/src/lib.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,10 @@ pub trait ToolChangeNotifier: Send + Sync {
9393
}
9494

9595
// Re-export foundational types
96-
/// JSON-RPC 2.0 dispatcher and handler trait for protocol operations
97-
pub use turul_mcp_json_rpc_server::{JsonRpcDispatcher, JsonRpcHandler};
9896
/// Core MCP protocol types and error handling
9997
pub use turul_mcp_protocol::*;
98+
/// JSON-RPC 2.0 dispatcher and handler trait for protocol operations
99+
pub use turul_rpc::{JsonRpcDispatcher, JsonRpcHandler};
100100

101101
/// Result type for HTTP MCP operations
102102
pub type Result<T> = std::result::Result<T, HttpMcpError>;
@@ -108,7 +108,7 @@ pub enum HttpMcpError {
108108
Http(#[from] hyper::Error),
109109

110110
#[error("JSON-RPC error: {0}")]
111-
JsonRpc(#[from] turul_mcp_json_rpc_server::JsonRpcError),
111+
JsonRpc(#[from] turul_rpc::JsonRpcError),
112112

113113
#[error("MCP protocol error: {0}")]
114114
Mcp(#[from] turul_mcp_protocol::McpError),

crates/turul-http-mcp-server/src/notification_bridge.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,12 @@ use std::sync::Arc;
1818
use tracing::{debug, error, info};
1919

2020
use crate::StreamManager;
21-
use turul_mcp_json_rpc_server::JsonRpcNotification;
2221
use turul_mcp_protocol::notifications::{
2322
CancelledNotification, LoggingMessageNotification, ProgressNotification,
2423
PromptListChangedNotification, ResourceListChangedNotification, ResourceUpdatedNotification,
2524
ToolListChangedNotification,
2625
};
26+
use turul_rpc::JsonRpcNotification;
2727

2828
/// MCP-compliant notification broadcaster trait for sending ALL notification types over SSE
2929
///

crates/turul-http-mcp-server/src/server.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ use std::sync::Arc;
1414
use tokio::net::TcpListener;
1515
use tracing::{debug, error, info, warn};
1616

17-
use turul_mcp_json_rpc_server::{JsonRpcDispatcher, JsonRpcHandler};
1817
use turul_mcp_protocol::McpError;
1918
use turul_mcp_session_storage::InMemorySessionStorage;
19+
use turul_rpc::{JsonRpcDispatcher, JsonRpcHandler};
2020

2121
use crate::streamable_http::{McpProtocolVersion, StreamableHttpHandler};
2222
use crate::{CorsLayer, Result, SessionMcpHandler, StreamConfig, StreamManager};

crates/turul-http-mcp-server/src/session_handler.rs

Lines changed: 45 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,15 @@ use hyper::{Method, Request, Response, StatusCode};
2323

2424
use crate::middleware::bearer::{extract_bearer_token, is_bearer_scheme};
2525
use chrono;
26-
use turul_mcp_json_rpc_server::{
26+
use turul_mcp_protocol::McpError;
27+
use turul_mcp_protocol::ServerCapabilities;
28+
use turul_mcp_session_storage::{InMemorySessionStorage, SessionView};
29+
use turul_rpc::{
2730
JsonRpcDispatcher,
2831
r#async::SessionContext,
2932
dispatch::{JsonRpcMessage, JsonRpcMessageResult, parse_json_rpc_message},
3033
error::{JsonRpcError, JsonRpcErrorObject},
3134
};
32-
use turul_mcp_protocol::McpError;
33-
use turul_mcp_protocol::ServerCapabilities;
34-
use turul_mcp_session_storage::{InMemorySessionStorage, SessionView};
3535
use uuid::Uuid;
3636

3737
use crate::{
@@ -517,13 +517,12 @@ impl SessionMcpHandler {
517517
error!("Failed to create session during initialize: {}", err);
518518
// Return error response using proper JSON-RPC error format
519519
let error_msg = format!("Session creation failed: {}", err);
520-
let error_response =
521-
turul_mcp_json_rpc_server::JsonRpcResponse::error(
522-
turul_mcp_json_rpc_server::JsonRpcError::internal_error(
523-
Some(request.id),
524-
Some(error_msg),
525-
),
526-
);
520+
let error_response = turul_rpc::JsonRpcResponse::error(
521+
turul_rpc::JsonRpcError::internal_error(
522+
Some(request.id),
523+
Some(error_msg),
524+
),
525+
);
527526
(error_response, None, Vec::new())
528527
}
529528
}
@@ -646,12 +645,10 @@ impl SessionMcpHandler {
646645

647646
// Convert JsonRpcResponse to JsonRpcMessageResult
648647
let message_result = match response {
649-
turul_mcp_json_rpc_server::JsonRpcResponse::Success(resp) => {
648+
turul_rpc::JsonRpcResponse::Success(resp) => {
650649
JsonRpcMessageResult::Response(resp)
651650
}
652-
turul_mcp_json_rpc_server::JsonRpcResponse::Error(err) => {
653-
JsonRpcMessageResult::Error(err)
654-
}
651+
turul_rpc::JsonRpcResponse::Error(err) => JsonRpcMessageResult::Error(err),
655652
};
656653
(
657654
message_result,
@@ -745,9 +742,7 @@ impl SessionMcpHandler {
745742
self.stream_manager
746743
.create_post_sse_stream_with_notifications(
747744
response_session_id.clone().unwrap(),
748-
turul_mcp_json_rpc_server::JsonRpcResponse::Success(
749-
response.clone(),
750-
),
745+
turul_rpc::JsonRpcResponse::Success(response.clone()),
751746
collected_notifications,
752747
)
753748
.await
@@ -756,9 +751,7 @@ impl SessionMcpHandler {
756751
self.stream_manager
757752
.create_post_sse_stream(
758753
response_session_id.clone().unwrap(),
759-
turul_mcp_json_rpc_server::JsonRpcResponse::Success(
760-
response.clone(),
761-
),
754+
turul_rpc::JsonRpcResponse::Success(response.clone()),
762755
)
763756
.await
764757
};
@@ -774,7 +767,7 @@ impl SessionMcpHandler {
774767
e
775768
);
776769
Ok(jsonrpc_response_with_session(
777-
turul_mcp_json_rpc_server::JsonRpcResponse::Success(response),
770+
turul_rpc::JsonRpcResponse::Success(response),
778771
response_session_id,
779772
)?
780773
.map(convert_to_unified_body))
@@ -786,7 +779,7 @@ impl SessionMcpHandler {
786779
accept_mode, method_name
787780
);
788781
Ok(jsonrpc_response_with_session(
789-
turul_mcp_json_rpc_server::JsonRpcResponse::Success(response),
782+
turul_rpc::JsonRpcResponse::Success(response),
790783
response_session_id,
791784
)?
792785
.map(convert_to_unified_body))
@@ -1127,12 +1120,12 @@ impl SessionMcpHandler {
11271120
/// Shared logic between StreamableHttpHandler and SessionMcpHandler
11281121
async fn run_middleware_and_dispatch(
11291122
&self,
1130-
request: turul_mcp_json_rpc_server::JsonRpcRequest,
1123+
request: turul_rpc::JsonRpcRequest,
11311124
headers: HashMap<String, String>,
1132-
session: turul_mcp_json_rpc_server::SessionContext,
1125+
session: turul_rpc::SessionContext,
11331126
pre_session_extensions: Option<HashMap<String, serde_json::Value>>,
11341127
) -> (
1135-
turul_mcp_json_rpc_server::JsonRpcResponse,
1128+
turul_rpc::JsonRpcResponse,
11361129
Option<crate::middleware::SessionInjection>,
11371130
) {
11381131
// Fast path: if middleware stack is empty, dispatch directly
@@ -1157,10 +1150,10 @@ impl SessionMcpHandler {
11571150

11581151
// Convert params to Option<Value>
11591152
let params = request.params.clone().map(|p| match p {
1160-
turul_mcp_json_rpc_server::RequestParams::Object(map) => {
1153+
turul_rpc::RequestParams::Object(map) => {
11611154
serde_json::Value::Object(map.into_iter().collect())
11621155
}
1163-
turul_mcp_json_rpc_server::RequestParams::Array(arr) => serde_json::Value::Array(arr),
1156+
turul_rpc::RequestParams::Array(arr) => serde_json::Value::Array(arr),
11641157
});
11651158
let mut ctx = crate::middleware::RequestContext::new(&method, params);
11661159

@@ -1228,15 +1221,15 @@ impl SessionMcpHandler {
12281221
// Execute after_dispatch
12291222
// Convert JsonRpcMessage to DispatcherResult for middleware
12301223
let mut dispatcher_result = match &result {
1231-
turul_mcp_json_rpc_server::JsonRpcResponse::Success(resp) => match &resp.result {
1232-
turul_mcp_json_rpc_server::response::ResponseResult::Success(val) => {
1224+
turul_rpc::JsonRpcResponse::Success(resp) => match &resp.result {
1225+
turul_rpc::response::ResponseResult::Success(val) => {
12331226
crate::middleware::DispatcherResult::Success(val.clone())
12341227
}
1235-
turul_mcp_json_rpc_server::response::ResponseResult::Null => {
1228+
turul_rpc::response::ResponseResult::Null => {
12361229
crate::middleware::DispatcherResult::Success(serde_json::Value::Null)
12371230
}
12381231
},
1239-
turul_mcp_json_rpc_server::JsonRpcResponse::Error(err) => {
1232+
turul_rpc::JsonRpcResponse::Error(err) => {
12401233
crate::middleware::DispatcherResult::Error(err.error.message.clone())
12411234
}
12421235
};
@@ -1265,39 +1258,33 @@ impl SessionMcpHandler {
12651258
/// - Error → Success: middleware recovered (only when error has request ID)
12661259
/// - Error → Error: error message mutated
12671260
fn apply_dispatcher_result(
1268-
result: turul_mcp_json_rpc_server::JsonRpcResponse,
1261+
result: turul_rpc::JsonRpcResponse,
12691262
dispatcher_result: crate::middleware::DispatcherResult,
1270-
) -> turul_mcp_json_rpc_server::JsonRpcResponse {
1263+
) -> turul_rpc::JsonRpcResponse {
12711264
match dispatcher_result {
12721265
crate::middleware::DispatcherResult::Success(val) => match result {
1273-
turul_mcp_json_rpc_server::JsonRpcResponse::Success(mut resp) => {
1274-
resp.result = turul_mcp_json_rpc_server::response::ResponseResult::Success(val);
1275-
turul_mcp_json_rpc_server::JsonRpcResponse::Success(resp)
1266+
turul_rpc::JsonRpcResponse::Success(mut resp) => {
1267+
resp.result = turul_rpc::response::ResponseResult::Success(val);
1268+
turul_rpc::JsonRpcResponse::Success(resp)
12761269
}
1277-
turul_mcp_json_rpc_server::JsonRpcResponse::Error(err) => {
1270+
turul_rpc::JsonRpcResponse::Error(err) => {
12781271
// Error→Success recovery: only when error has a request ID
12791272
match err.id {
1280-
Some(id) => {
1281-
turul_mcp_json_rpc_server::JsonRpcResponse::success(id, val.into())
1282-
}
1283-
None => turul_mcp_json_rpc_server::JsonRpcResponse::Error(err),
1273+
Some(id) => turul_rpc::JsonRpcResponse::success(id, val.into()),
1274+
None => turul_rpc::JsonRpcResponse::Error(err),
12841275
}
12851276
}
12861277
},
12871278
crate::middleware::DispatcherResult::Error(msg) => match result {
1288-
turul_mcp_json_rpc_server::JsonRpcResponse::Success(resp) => {
1289-
turul_mcp_json_rpc_server::JsonRpcResponse::Error(
1290-
turul_mcp_json_rpc_server::error::JsonRpcError::new(
1291-
Some(resp.id),
1292-
turul_mcp_json_rpc_server::error::JsonRpcErrorObject::internal_error(
1293-
Some(msg),
1294-
),
1295-
),
1296-
)
1279+
turul_rpc::JsonRpcResponse::Success(resp) => {
1280+
turul_rpc::JsonRpcResponse::Error(turul_rpc::error::JsonRpcError::new(
1281+
Some(resp.id),
1282+
turul_rpc::error::JsonRpcErrorObject::internal_error(Some(msg)),
1283+
))
12971284
}
1298-
turul_mcp_json_rpc_server::JsonRpcResponse::Error(mut err) => {
1285+
turul_rpc::JsonRpcResponse::Error(mut err) => {
12991286
err.error.message = msg;
1300-
turul_mcp_json_rpc_server::JsonRpcResponse::Error(err)
1287+
turul_rpc::JsonRpcResponse::Error(err)
13011288
}
13021289
},
13031290
}
@@ -1306,8 +1293,8 @@ impl SessionMcpHandler {
13061293
/// Map MiddlewareError to JSON-RPC error with semantic error codes
13071294
fn map_middleware_error_to_jsonrpc(
13081295
err: crate::middleware::MiddlewareError,
1309-
request_id: turul_mcp_json_rpc_server::RequestId,
1310-
) -> turul_mcp_json_rpc_server::JsonRpcResponse {
1296+
request_id: turul_rpc::RequestId,
1297+
) -> turul_rpc::JsonRpcResponse {
13111298
use crate::middleware::MiddlewareError;
13121299
use crate::middleware::error::error_codes;
13131300

@@ -1332,22 +1319,16 @@ impl SessionMcpHandler {
13321319
};
13331320

13341321
let error_obj = if let Some(d) = data {
1335-
turul_mcp_json_rpc_server::error::JsonRpcErrorObject::server_error(
1336-
code,
1337-
&message,
1338-
Some(d),
1339-
)
1322+
turul_rpc::error::JsonRpcErrorObject::server_error(code, &message, Some(d))
13401323
} else {
1341-
turul_mcp_json_rpc_server::error::JsonRpcErrorObject::server_error(
1324+
turul_rpc::error::JsonRpcErrorObject::server_error(
13421325
code,
13431326
&message,
13441327
None::<serde_json::Value>,
13451328
)
13461329
};
13471330

1348-
turul_mcp_json_rpc_server::JsonRpcResponse::Error(
1349-
turul_mcp_json_rpc_server::JsonRpcError::new(Some(request_id), error_obj),
1350-
)
1331+
turul_rpc::JsonRpcResponse::Error(turul_rpc::JsonRpcError::new(Some(request_id), error_obj))
13511332
}
13521333
}
13531334

crates/turul-http-mcp-server/src/stream_manager.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -686,7 +686,7 @@ impl StreamManager {
686686
pub async fn create_post_sse_stream(
687687
&self,
688688
session_id: String,
689-
response: turul_mcp_json_rpc_server::JsonRpcResponse,
689+
response: turul_rpc::JsonRpcResponse,
690690
) -> Result<
691691
hyper::Response<
692692
http_body_util::combinators::BoxBody<bytes::Bytes, std::convert::Infallible>,
@@ -791,7 +791,7 @@ impl StreamManager {
791791
pub async fn create_post_sse_stream_with_notifications(
792792
&self,
793793
session_id: String,
794-
response: turul_mcp_json_rpc_server::JsonRpcResponse,
794+
response: turul_rpc::JsonRpcResponse,
795795
notifications: Vec<SseEvent>,
796796
) -> Result<
797797
hyper::Response<

0 commit comments

Comments
 (0)