Non-object outputSchema and structuredContent are sent to clients that negotiated 2025-06-18 / 2025-11-25, where an object is required
Los mantenedores suelen responder en 3 días
Nadie ha tomado este issue todavía.
Evaluación
- Dificultad
- 3/5
- Tiempo estimado
- 1-2 días
- Aptitud para principiantes
- 28/100
Línea de trabajo
Start with strip_output in crates/rmcp/src/handler/server/common.rs, which always keeps non-object output schemas, and with the version gating for result_type in crates/rmcp/src/handler/server.rs. Check that Json<Vec<T>> advertises an array outputSchema and returns a non-object structuredContent for a session negotiated to 2025-06-18. Done means that session omits both for non-object roots while 2026-07-28 keeps them. The issue lists three options, so confirm with maintainers which one to build first.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
Summary
A tool that returns Json<Vec<T>> (or any non-object type) makes the server advertise a non-object outputSchema, and return a non-object structuredContent, to every client, including clients that negotiated protocol 2025-06-18 or 2025-11-25. In those versions both must be objects, so such a client sees a schema that is invalid for the version it asked for.
This is not a request to undo SEP-2106 (#874); allowing arrays and primitives is right for 2026-07-28. The gap is that the choice does not depend on the negotiated version, although other 2026-07-28 behaviour in handler/server.rs is gated on it.
What the spec versions say
2025-06-18,schema.ts:outputSchema?: { type: "object"; properties?: ...; required?: string[] }2025-11-25,schema.ts:outputSchema?: { $schema?: string; type: "object"; ... }, andstructuredContent?: { [key: string]: unknown }2026-07-28(SEP-2106): any JSON Schema 2020-12 schema, any JSON value.
crates/rmcp/src/handler/server/common.rs (strip_output) says output schemas "are not restricted to type: "object" (per SEP-2106)" unconditionally.
Reproduction
rmcp 3.5.1, features = ["server", "macros", "transport-io"].
use rmcp::handler::server::router::tool::ToolRouter;
use rmcp::model::{Implementation, ServerCapabilities, ServerConfig};
use rmcp::{tool, tool_handler, tool_router, Json, ServerHandler, ServiceExt};
#[derive(Clone)]
struct Server { tool_router: ToolRouter<Self> }
#[tool_router]
impl Server {
#[tool(description = "Returns a list of names")]
async fn names(&self) -> Json<Vec<String>> {
Json(vec!["a".to_string(), "b".to_string()])
}
}
#[tool_handler]
impl ServerHandler for Server {
fn get_info(&self) -> ServerConfig {
ServerConfig::new(ServerCapabilities::builder().enable_tools().build())
.with_server_info(Implementation::new("repro", "0.1.0"))
}
}
#[tokio::main(flavor = "current_thread")]
async fn main() {
let server = Server { tool_router: Server::tool_router() };
server.serve(rmcp::transport::stdio()).await.unwrap().waiting().await.unwrap();
}
A client that asks for 2025-06-18:
{ printf '%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"probe","version":"1"}}}' \
'{"jsonrpc":"2.0","method":"notifications/initialized"}' \
'{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \
'{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"names","arguments":{}}}'; sleep 1; } | ./target/debug/repro
Output (trimmed):
{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-06-18", ...}}
{"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"names", ...,
"outputSchema":{"$schema":"https://json-schema.org/draft/2020-12/schema","items":{"type":"string"},"type":"array"}}]}}
{"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"[\"a\",\"b\"]"}],"structuredContent":["a","b"],"isError":false}}
The server answers protocolVersion: 2025-06-18 and then advertises outputSchema.type: "array" and returns structuredContent: ["a","b"]. The same happens when the client asks for 2025-11-25. When I asked for 2026-07-28 in initialize, the server answered 2025-11-25 and still advertised the array schema.
Why it matters
A client that validates tools/list against the schema of the negotiated version will reject the tool, and some validate the whole list at once, so one tool with a list result can make every tool unavailable. We hit this shipping an MCP server (metamug/plunger#97); the report there was that the output schema was not an object. I have not checked which specific clients reject it, only that the advertised schema is invalid for the version that was negotiated.
The workaround is to wrap every list in a struct (as suggested in #532), which works everywhere, but it is easy to miss because Json<Vec<T>> compiles and works with clients that are lenient or new.
Suggested behaviour
Any of these would help, in rough order of preference:
- For a session that negotiated a version before
2026-07-28, omitoutputSchemawhen its root type is not"object"(the call still works; the result is also incontent), and do the same forstructuredContent. This follows the version gating already used forresult_type. - Offer a wrapper (for example
Json<Wrapped<T>>, or an attribute on#[tool]) that produces{ "result": T }and an object schema in all versions, so authors have a supported way to be compatible without writing a struct for every list. - At minimum, document on
Json<T>and onschema_for_outputthat a non-objectTis only valid for peers on2026-07-28and later.
Related: #532 (the same symptom, closed before SEP-2106), #874 (SEP-2106).
- Lenguaje dominante
- Rust
- Estrellas
- 4k
- Forks
- 654
- Merge medio
- 3 d 15 h
- PR fusionados (30 d)
- 37
Preparar el entorno
Inicia el contenedor de desarrollo del proyecto en tu navegador, con tu propia cuenta de GitHub.
- Sin Dockerfile ni archivo de Docker Compose
- Sin plantilla de pull request
- Leer la guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de modelcontextprotocol/rust-sdk
-
bug P2 ready for work T-config
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
modelcontextprotocol/rust-sdk#1335 ·
Los mantenedores suelen responder en 3 días
-
auth: refresh_token() requests offline_access that was never granted, so refreshes fail with invalid_scopePosiblemente ocupada @jstar0 la tomó hace 2 días. Abiertobug P1 ready for work T-security T-transport
Dificultad 2/5 1-3 horas Aptitud para principiantes 65/100
modelcontextprotocol/rust-sdk#1330 ·
Los mantenedores suelen responder en 3 días
-
P3 question Stale T-documentation T-enhancement
Dificultad 1/5 Menos de una hora Aptitud para principiantes 86/100
modelcontextprotocol/rust-sdk#1155 · 1 comentario ·
Los mantenedores suelen responder en 3 días
-
enhancement P2 ready for work T-security T-transport
Dificultad 3/5 Medio día Aptitud para principiantes 40/100
modelcontextprotocol/rust-sdk#1334 ·
Los mantenedores suelen responder en 3 días
-
enhancement P3 ready for work T-documentation T-examples
Dificultad 2/5 Medio día Aptitud para principiantes 30/100
modelcontextprotocol/rust-sdk#1332 ·
Los mantenedores suelen responder en 3 días
Todos los issues de modelcontextprotocol/rust-sdk
Issues similares
-
good first issue help wanted
Dificultad 2/5 1-3 horas Aptitud para principiantes 72/100
-
documentation
Dificultad 2/5 1-3 horas Aptitud para principiantes 70/100
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 62/100
NuSkooler/enigma-bbs#907 ·
Los mantenedores suelen responder en 1 día
-
bug
Dificultad 2/5 1-3 horas Aptitud para principiantes 62/100
Los mantenedores suelen responder en 1 día
-
bug pixi-build-r
Dificultad 2/5 1-3 horas Aptitud para principiantes 70/100
prefix-dev/pixi#7229 ·
Los mantenedores suelen responder en 1 día