Skip to content

Commit 54516e2

Browse files
committed
Merge remote-tracking branch 'upstream/main'
Brings in upstream v1.6.1..266f870 (15 commits): protocol-version negotiation/init-mismatch fixes, input/outputSchema strip+validate (modelcontextprotocol#856/modelcontextprotocol#860), tool-argument errors as tool results (modelcontextprotocol#894), OAuth issuer validation + OIDC application_type + offline_access reauth, roots/ sampling/logging deprecation (SEP-2577), empty-2xx notification handling. Conflict resolutions (fork changes preserved): - handler/server/common.rs: kept both test blocks. Upstream's validate_and_strip / schema_for_input layer composes on top of the fork's ReplaceBoolSchemas normalization in schema_for_type. - handler/server/router/tool.rs: combined upstream's into_tool_argument_error dispatch with the anthropic-ext normalize_call_tool_result post-processing (result kept mutable). Restored .githooks/commit-msg, which upstream modelcontextprotocol#898 deleted incidentally in an unrelated "progress demo" change; the hook gates our conventional- commit workflow and skips merge commits.
2 parents 3f249a5 + 266f870 commit 54516e2

41 files changed

Lines changed: 1584 additions & 178 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ jobs:
8585
cargo semver-checks \
8686
--package rmcp \
8787
--baseline-rev ${{ github.event.pull_request.base.sha }} \
88+
--release-type minor \
8889
--only-explicit-features \
8990
--features default
9091
@@ -97,6 +98,7 @@ jobs:
9798
cargo semver-checks \
9899
--package rmcp \
99100
--baseline-rev ${{ github.event.pull_request.base.sha }} \
101+
--release-type minor \
100102
--only-explicit-features \
101103
--features "$FEATURES"
102104

README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,8 @@ async fn main() -> anyhow::Result<()> {
169169
}
170170
```
171171

172+
The generated tool `inputSchema` and `outputSchema` are derived from the fields of `T`. The type name and documentation on `T` are ignored; only field names, field types, and field documentation are used.
173+
172174
When you need custom server metadata or multiple capabilities (tools + prompts), use explicit `#[tool_handler]`:
173175

174176
```rust,ignore
@@ -469,6 +471,8 @@ context.peer.notify_prompt_list_changed().await?;
469471

470472
## Sampling
471473

474+
> **Deprecated (SEP-2577):** Sampling is deprecated and will be removed in a future release. It remains fully functional for now. See [SEP-2577](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577).
475+
472476
Sampling flips the usual direction: the server asks the client to run an LLM completion. The server sends a `create_message` request, the client processes it through its LLM, and returns the result.
473477

474478
**MCP Spec:** [Sampling](https://modelcontextprotocol.io/specification/2025-11-25/client/sampling)
@@ -542,6 +546,8 @@ impl ClientHandler for MyClient {
542546

543547
## Roots
544548

549+
> **Deprecated (SEP-2577):** Roots is deprecated and will be removed in a future release. It remains fully functional for now. See [SEP-2577](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577).
550+
545551
Roots tell servers which directories or projects the client is working in. A root is a URI (typically `file://`) pointing to a workspace or repository. Servers can query roots to know where to look for files and how to scope their work.
546552

547553
**MCP Spec:** [Roots](https://modelcontextprotocol.io/specification/2025-11-25/client/roots)
@@ -610,6 +616,8 @@ client.notify_roots_list_changed().await?;
610616

611617
## Logging
612618

619+
> **Deprecated (SEP-2577):** Logging is deprecated and will be removed in a future release. It remains fully functional for now. See [SEP-2577](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577).
620+
613621
Servers can send structured log messages to clients. The client sets a minimum severity level, and the server sends messages through the peer notification interface.
614622

615623
**MCP Spec:** [Logging](https://modelcontextprotocol.io/specification/2025-11-25/server/utilities/logging)
@@ -1012,9 +1020,10 @@ See [Oauth_support](docs/OAUTH_SUPPORT.md) for details.
10121020
- [spreadsheet-mcp](https://github.com/PSU3D0/spreadsheet-mcp) - Token-efficient MCP server for spreadsheet analysis with automatic region detection, recalculation, screenshot, and editing support for LLM agents
10131021
- [hyper-mcp](https://github.com/hyper-mcp-rs/hyper-mcp) - A fast, secure MCP server that extends its capabilities through WebAssembly (WASM) plugins
10141022
- [rudof-mcp](https://github.com/rudof-project/rudof/tree/master/rudof_mcp) - RDF validation and data processing MCP server with ShEx/SHACL validation, SPARQL queries, and format conversion. Supports stdio and streamable HTTP transports with full MCP capabilities (tools, prompts, resources, logging, completions, tasks)
1023+
- [MCPMate](https://github.com/loocor/MCPMate) - Desktop app for progressive MCP management: start with guided server import, then grow into multi-client profiles and Unify meta tools to keep tool exposure, token use, and runtime state under control, with more options for efficiency, cost, and reliability
10151024
- [McpMux](https://github.com/mcpmux/mcp-mux) - Desktop app to configure MCP servers once at McpMux, connect every AI client (Cursor, Claude Desktop, VS Code, Windsurf) through a single encrypted local gateway with Spaces for project organization, FeatureSets to switch toolsets per client, and a built-in server registry
10161025
- [systemprompt-template](https://github.com/systempromptio/systemprompt-template) - Single-binary Rust runtime providing MCP governance — authentication, authorisation, rate-limiting, audit trails, and cost tracking for AI agents. Self-hosted, air-gap capable, 3,300+ req/s with sub-5ms governance overhead
1017-
1026+
- [jilebi-mcp](https://github.com/datron/jilebi) - an extensible MCP server through plugins in Javascript with a secure permissions model
10181027

10191028
## Development
10201029

client-metadata.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@
33
"redirect_uris": ["http://127.0.0.1:8080/callback"],
44
"grant_types": ["authorization_code", "refresh_token"],
55
"response_types": ["code"],
6-
"token_endpoint_auth_method": "none"
6+
"token_endpoint_auth_method": "none",
7+
"application_type": "native"
78
}

conformance/src/bin/client.rs

Lines changed: 37 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ use rmcp::{
33
model::*,
44
service::RequestContext,
55
transport::{
6-
AuthClient, AuthorizationManager, StreamableHttpClientTransport, auth::OAuthState,
6+
AuthClient, AuthorizationManager, StreamableHttpClientTransport,
7+
auth::{AuthorizationCallback, OAuthState},
78
streamable_http_client::StreamableHttpClientTransportConfig,
89
},
910
};
@@ -221,20 +222,16 @@ async fn perform_oauth_flow(
221222
.and_then(|v| v.to_str().ok())
222223
.ok_or_else(|| anyhow::anyhow!("No Location header in auth redirect"))?;
223224

224-
let redirect_url = url::Url::parse(location)?;
225-
let code = redirect_url
226-
.query_pairs()
227-
.find(|(k, _)| k == "code")
228-
.map(|(_, v)| v.to_string())
229-
.ok_or_else(|| anyhow::anyhow!("No code in redirect URL"))?;
230-
let state = redirect_url
231-
.query_pairs()
232-
.find(|(k, _)| k == "state")
233-
.map(|(_, v)| v.to_string())
234-
.ok_or_else(|| anyhow::anyhow!("No state in redirect URL"))?;
225+
let callback = AuthorizationCallback::from_redirect_url(location)?;
235226

236227
tracing::debug!("Got auth code, exchanging for token...");
237-
oauth.handle_callback(&code, &state).await?;
228+
oauth
229+
.handle_callback_with_issuer(
230+
&callback.code,
231+
&callback.csrf_token,
232+
callback.issuer.as_deref(),
233+
)
234+
.await?;
238235

239236
let am = oauth
240237
.into_authorization_manager()
@@ -334,8 +331,14 @@ async fn run_auth_scope_step_up_client(
334331
.await?;
335332

336333
let auth_url = oauth.get_authorization_url().await?;
337-
let (code, state) = headless_authorize(&auth_url).await?;
338-
oauth.handle_callback(&code, &state).await?;
334+
let callback = headless_authorize(&auth_url).await?;
335+
oauth
336+
.handle_callback_with_issuer(
337+
&callback.code,
338+
&callback.csrf_token,
339+
callback.issuer.as_deref(),
340+
)
341+
.await?;
339342

340343
let am = oauth
341344
.into_authorization_manager()
@@ -380,8 +383,14 @@ async fn run_auth_scope_step_up_client(
380383
)
381384
.await?;
382385
let auth_url2 = oauth2.get_authorization_url().await?;
383-
let (code2, state2) = headless_authorize(&auth_url2).await?;
384-
oauth2.handle_callback(&code2, &state2).await?;
386+
let callback2 = headless_authorize(&auth_url2).await?;
387+
oauth2
388+
.handle_callback_with_issuer(
389+
&callback2.code,
390+
&callback2.csrf_token,
391+
callback2.issuer.as_deref(),
392+
)
393+
.await?;
385394

386395
let am2 = oauth2.into_authorization_manager().unwrap();
387396
let auth_client2 = AuthClient::new(reqwest::Client::default(), am2);
@@ -422,8 +431,14 @@ async fn run_auth_scope_retry_limit_client(
422431
)
423432
.await?;
424433
let auth_url = oauth.get_authorization_url().await?;
425-
let (code, state) = headless_authorize(&auth_url).await?;
426-
oauth.handle_callback(&code, &state).await?;
434+
let callback = headless_authorize(&auth_url).await?;
435+
oauth
436+
.handle_callback_with_issuer(
437+
&callback.code,
438+
&callback.csrf_token,
439+
callback.issuer.as_deref(),
440+
)
441+
.await?;
427442

428443
let am = oauth.into_authorization_manager().unwrap();
429444
let auth_client = AuthClient::new(reqwest::Client::default(), am);
@@ -696,8 +711,8 @@ async fn run_cross_app_access_client(
696711

697712
// ─── Helpers ────────────────────────────────────────────────────────────────
698713

699-
/// Fetch an authorization URL headlessly, returning (code, state).
700-
async fn headless_authorize(auth_url: &str) -> anyhow::Result<(String, String)> {
714+
/// Fetch an authorization URL headlessly, returning the callback parameters.
715+
async fn headless_authorize(auth_url: &str) -> anyhow::Result<AuthorizationCallback> {
701716
let http = reqwest::Client::builder()
702717
.redirect(reqwest::redirect::Policy::none())
703718
.build()?;
@@ -707,18 +722,7 @@ async fn headless_authorize(auth_url: &str) -> anyhow::Result<(String, String)>
707722
.get("location")
708723
.and_then(|v| v.to_str().ok())
709724
.ok_or_else(|| anyhow::anyhow!("No Location header in auth redirect"))?;
710-
let redirect_url = url::Url::parse(location)?;
711-
let code = redirect_url
712-
.query_pairs()
713-
.find(|(k, _)| k == "code")
714-
.map(|(_, v)| v.to_string())
715-
.ok_or_else(|| anyhow::anyhow!("No code in redirect URL"))?;
716-
let state = redirect_url
717-
.query_pairs()
718-
.find(|(k, _)| k == "state")
719-
.map(|(_, v)| v.to_string())
720-
.ok_or_else(|| anyhow::anyhow!("No state in redirect URL"))?;
721-
Ok((code, state))
725+
AuthorizationCallback::from_redirect_url(location).map_err(Into::into)
722726
}
723727

724728
/// Build a `CallToolRequestParams` for a tool, optionally with arguments.

conformance/src/bin/server.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
#![allow(deprecated)]
12
use std::{collections::HashSet, sync::Arc};
23

34
use rmcp::{

crates/rmcp-macros/src/tool.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,14 @@ pub fn tool(attr: TokenStream, input: TokenStream) -> syn::Result<TokenStream> {
237237
if let Some(params_ty) = params_ty {
238238
// if found, use the Parameters schema
239239
syn::parse2::<Expr>(quote! {
240-
rmcp::handler::server::common::schema_for_type::<#params_ty>()
240+
rmcp::handler::server::common::schema_for_input::<#params_ty>()
241+
.unwrap_or_else(|e| {
242+
panic!(
243+
"Invalid input schema for `{}`: {}",
244+
std::any::type_name::<#params_ty>(),
245+
e
246+
)
247+
})
241248
})?
242249
} else {
243250
// if not found, use a default empty JSON schema object

crates/rmcp/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,11 @@ name = "test_streamable_http_json_response"
275275
required-features = ["server", "client", "transport-streamable-http-server", "reqwest"]
276276
path = "tests/test_streamable_http_json_response.rs"
277277

278+
[[test]]
279+
name = "test_streamable_http_protocol_version"
280+
required-features = ["server", "client", "transport-streamable-http-server", "reqwest"]
281+
path = "tests/test_streamable_http_protocol_version.rs"
282+
278283
[[test]]
279284
name = "test_streamable_http_4xx_error_body"
280285
required-features = ["transport-streamable-http-client", "transport-streamable-http-client-reqwest"]

crates/rmcp/src/handler/server.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -184,9 +184,7 @@ macro_rules! server_handler_methods {
184184
request: InitializeRequestParams,
185185
context: RequestContext<RoleServer>,
186186
) -> impl Future<Output = Result<InitializeResult, McpError>> + MaybeSendFuture + '_ {
187-
if context.peer.peer_info().is_none() {
188-
context.peer.set_peer_info(request);
189-
}
187+
context.peer.set_peer_info(request);
190188
std::future::ready(Ok(self.get_info()))
191189
}
192190
fn complete(

0 commit comments

Comments
 (0)