Skip to content

Structured tool output schema does not follow MCP 2025-06-18. #532

@oriyadid

Description

@oriyadid

Describe the bug
According to the MCP schema (version 2025-06-18), a tool’s outputSchema must have a root schema of type "object".
The Rust SDK currently serializes any structured output into a JSON schema without ensuring that the root type is "object". This behavior is non-compliant with the specification.

To Reproduce

  1. Implement a tool handler whose return type is Json<non-object> or Result<Json<non-object>, E>.
    An example of this exists in this very repository calculator.rs#L47.
    Here is a minimal file with that tool:
use rmcp::{
  Json, ServerHandler, ServiceExt,
  handler::server::{tool::ToolRouter, wrapper::Parameters},
  model::{ServerCapabilities, ServerInfo},
  schemars, tool, tool_handler, tool_router,
  transport::stdio,
};

#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct SubRequest {
  #[schemars(description = "the left hand side number")]
  pub a: i32,
  #[schemars(description = "the right hand side number")]
  pub b: i32,
}

#[derive(Debug, Clone)]
pub struct Calculator {
  tool_router: ToolRouter<Self>,
}

#[tool_router]
impl Calculator {
  pub fn new() -> Self {
    Self {
      tool_router: Self::tool_router(),
    }
  }

  #[tool(description = "Calculate the difference of two numbers")]
  fn sub(&self, Parameters(SubRequest { a, b }): Parameters<SubRequest>) -> Json<i32> {
    Json(a - b)
  }
}

#[tool_handler]
impl ServerHandler for Calculator {
  fn get_info(&self) -> ServerInfo {
    ServerInfo {
      instructions: Some("A simple calculator".into()),
      capabilities: ServerCapabilities::builder().enable_tools().build(),
      ..Default::default()
    }
  }
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
  let service: rmcp::service::RunningService<rmcp::RoleServer, Calculator> =
    Calculator::new().serve(stdio()).await.inspect_err(|e| {
      tracing::error!("serving error: {:?}", e);
    })?;

  service.waiting().await?;

  Ok(())
}
  1. Query the available tools.
  • {"jsonrpc": "2.0","id": 1,"method": "initialize","params": {"protocolVersion": "2025-06-18","capabilities": {"elicitation": {}},"clientInfo": {"name": "example-client","version": "1.0.0"}}}
  • {"jsonrpc": "2.0", "id": 2, "method": "notifications/initialized"}
  • {"jsonrpc": "2.0", "id": 3, "method": "tools/list"}
  1. Observe the schema in the response - the root of outputSchema is of type "integer", when it should be "object"
{"outputSchema":{"$schema":"http://json-schema.org/draft-07/schema#","format":"int32","title":"int32","type":"integer"}}

Expected behavior
The generated outputSchema should comply with the specification by having a root type of "object", and all serialized outputs should match this schema.

Logs
Here are the full logs from running my example program.

{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-03-26","capabilities":{"tools":{}},"serverInfo":{"name":"rmcp","version":"0.8.5"},"instructions":"A simple calculator"}}
{"jsonrpc":"2.0","id":3,"result":{"tools":[{"name":"sub","description":"Calculate the difference of two numbers","inputSchema":{"$schema":"http://json-schema.org/draft-07/schema#","properties":{"a":{"description":"the left hand side number","format":"int32","type":"integer"},"b":{"description":"the right hand side number","format":"int32","type":"integer"}},"required":["a","b"],"title":"SubRequest","type":"object"},"outputSchema":{"$schema":"http://json-schema.org/draft-07/schema#","format":"int32","title":"int32","type":"integer"}}]}}

Additional context
The team behind this SDK has previously acknowledged incomplete compatibility with the 2025-06-18 schema version (#496 (comment)). This report highlights a specific instance which I have noticed.

This issue also appears to underlie #491, as the MCP schema expects outputSchema definitions with root type "object", rather than an empty definition {} or one where the root is of a primitive type.

As a temporary workaround for anyone interested, wrapping non-object outputs resolves the issue:

use serde::{Serialize, Deserialize};
use rmcp::{schemars, schemars::JsonSchema};

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
// Useful in situations where the result is not of type object by itself
pub struct Wrapper<T> {
    pub result: T,
}

impl<T> Wrapper<T> {
    pub fn new(result: T) -> Self {
        Self { result }
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions