use crate::error::Error;
use crate::types::{JSONRPC_VERSION, Message, RequestId};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
#[cfg(feature = "http-server")]
use http::HeaderMap;
pub use error_details::ErrorDetails;
pub use into_response::IntoResponse;
mod error_details;
mod into_response;
#[cfg(not(feature = "legacy-spec"))]
pub(crate) const RESULT_TYPE: &str = "resultType";
#[cfg(not(feature = "legacy-spec"))]
pub(crate) const COMPLETE: &str = "complete";
#[cfg(not(feature = "legacy-spec"))]
pub(crate) const INPUT_REQUIRED: &str = "input_required";
#[cfg(all(not(feature = "legacy-spec"), feature = "tasks"))]
pub(crate) const TASK: &str = "task";
#[cfg(not(feature = "legacy-spec"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ResultType {
#[serde(rename = "complete")]
Complete,
#[serde(rename = "input_required")]
InputRequired,
#[cfg(feature = "tasks")]
#[serde(rename = "task")]
Task,
}
#[cfg(not(feature = "legacy-spec"))]
#[inline]
pub(crate) fn tag_complete(mut result: Value) -> Value {
if let Value::Object(map) = &mut result
&& !map.contains_key(RESULT_TYPE)
{
map.insert(RESULT_TYPE.into(), Value::String(COMPLETE.into()));
}
result
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Response {
Ok(OkResponse),
Err(ErrorResponse),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OkResponse {
pub jsonrpc: String,
#[serde(default)]
pub id: RequestId,
pub result: Value,
#[serde(skip)]
pub session_id: Option<uuid::Uuid>,
#[serde(skip)]
#[cfg(feature = "http-server")]
pub headers: HeaderMap,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ErrorResponse {
pub jsonrpc: String,
#[serde(default)]
pub id: RequestId,
pub error: ErrorDetails,
#[serde(skip)]
pub session_id: Option<uuid::Uuid>,
#[serde(skip)]
#[cfg(feature = "http-server")]
pub headers: HeaderMap,
}
impl From<Response> for Message {
#[inline]
fn from(response: Response) -> Self {
Self::Response(response)
}
}
impl Response {
#[cfg_attr(
not(feature = "legacy-spec"),
doc = "",
doc = "Under MCP 2026-07-28 the result is stamped with",
doc = "`resultType: \"complete\"` unless it already carries a discriminator --",
doc = "which is how [`InputRequiredResult`](crate::types::mrtr::InputRequiredResult)",
doc = "keeps its own `\"input_required\"` on the way out."
)]
pub fn success(id: RequestId, result: Value) -> Self {
#[cfg(not(feature = "legacy-spec"))]
let result = tag_complete(result);
Response::Ok(OkResponse {
jsonrpc: JSONRPC_VERSION.to_string(),
session_id: None,
#[cfg(feature = "http-server")]
headers: HeaderMap::with_capacity(8),
id,
result,
})
}
pub fn empty(id: RequestId) -> Self {
#[cfg(not(feature = "legacy-spec"))]
let result = json!({ RESULT_TYPE: COMPLETE });
#[cfg(feature = "legacy-spec")]
let result = json!({});
Response::Ok(OkResponse {
jsonrpc: JSONRPC_VERSION.to_string(),
session_id: None,
#[cfg(feature = "http-server")]
headers: HeaderMap::new(),
id,
result,
})
}
pub fn error(id: RequestId, error: Error) -> Self {
Response::Err(ErrorResponse {
jsonrpc: JSONRPC_VERSION.to_string(),
session_id: None,
#[cfg(feature = "http-server")]
headers: HeaderMap::with_capacity(8),
id,
error: error.into(),
})
}
#[cfg(not(feature = "legacy-spec"))]
pub fn with_server_info(mut self, info: &crate::types::Implementation) -> Self {
const KEY: &str = "io.modelcontextprotocol/serverInfo";
if let Response::Ok(ok) = &mut self
&& let Value::Object(result) = &mut ok.result
{
let meta = result
.entry("_meta")
.or_insert_with(|| Value::Object(Default::default()));
if let Value::Object(meta) = meta
&& !meta.contains_key(KEY)
&& let Ok(info) = serde_json::to_value(info)
{
meta.insert(KEY.into(), info);
}
}
self
}
#[cfg(not(feature = "legacy-spec"))]
pub fn with_result_type(mut self) -> Self {
if let Response::Ok(ok) = &mut self {
ok.result = tag_complete(std::mem::take(&mut ok.result));
}
self
}
#[cfg(not(feature = "legacy-spec"))]
pub fn result_type(&self) -> Option<ResultType> {
let Response::Ok(ok) = self else {
return None;
};
Some(match ok.result.get(RESULT_TYPE).and_then(Value::as_str) {
Some(INPUT_REQUIRED) => ResultType::InputRequired,
#[cfg(feature = "tasks")]
Some(TASK) => ResultType::Task,
_ => ResultType::Complete,
})
}
pub fn id(&self) -> &RequestId {
match &self {
Response::Ok(ok) => &ok.id,
Response::Err(err) => &err.id,
}
}
pub fn full_id(&self) -> RequestId {
let id = self.id().clone();
if let Some(session_id) = self.session_id() {
id.concat(RequestId::Uuid(*session_id))
} else {
id
}
}
pub fn set_id(mut self, id: RequestId) -> Self {
match &mut self {
Response::Ok(ok) => ok.id = id,
Response::Err(err) => err.id = id,
}
self
}
#[inline]
pub fn session_id(&self) -> Option<&uuid::Uuid> {
match &self {
Response::Ok(ok) => ok.session_id.as_ref(),
Response::Err(err) => err.session_id.as_ref(),
}
}
pub fn set_session_id(mut self, id: uuid::Uuid) -> Self {
match &mut self {
Response::Ok(ok) => ok.session_id = Some(id),
Response::Err(err) => err.session_id = Some(id),
}
self
}
#[cfg(feature = "http-server")]
pub fn set_headers(mut self, headers: HeaderMap) -> Self {
match &mut self {
Response::Ok(ok) => ok.headers = headers,
Response::Err(err) => err.headers = headers,
}
self
}
pub fn into_result<T: DeserializeOwned>(self) -> Result<T, Error> {
match self {
Response::Ok(ok) => serde_json::from_value::<T>(ok.result).map_err(Into::into),
Response::Err(err) => Err(err.error.into()),
}
}
}
#[cfg(test)]
mod tests {
use super::Response;
use crate::{error::Error, types::RequestId};
#[test]
fn it_deserializes_successful_response_with_int_id_to_json() {
let resp = Response::success(RequestId::Number(42), serde_json::json!({ "key": "test" }));
let json = serde_json::to_string(&resp).unwrap();
#[cfg(feature = "legacy-spec")]
assert_eq!(json, r#"{"jsonrpc":"2.0","id":42,"result":{"key":"test"}}"#);
#[cfg(not(feature = "legacy-spec"))]
assert_eq!(
json,
r#"{"jsonrpc":"2.0","id":42,"result":{"key":"test","resultType":"complete"}}"#
);
}
#[test]
fn it_deserializes_error_response_with_string_id_to_json() {
let resp = Response::error(
RequestId::String("id".into()),
Error::new(-32603, "some error message"),
);
let json = serde_json::to_string(&resp).unwrap();
assert_eq!(
json,
r#"{"jsonrpc":"2.0","id":"id","error":{"code":-32603,"message":"some error message","data":null}}"#
);
}
}
#[cfg(test)]
#[cfg(not(feature = "legacy-spec"))]
mod result_type_tests {
use super::{Response, ResultType};
use crate::{error::Error, types::RequestId};
fn parse(raw: &str) -> Response {
serde_json::from_str(raw).expect("a well-formed JSON-RPC response")
}
#[test]
fn every_success_result_is_stamped_complete() {
let resp = Response::success(RequestId::Number(1), serde_json::json!({ "tools": [] }));
let Response::Ok(ok) = &resp else {
panic!("expected a success response")
};
assert_eq!(ok.result["resultType"], serde_json::json!("complete"));
assert_eq!(resp.result_type(), Some(ResultType::Complete));
}
#[test]
fn an_empty_result_is_stamped_too() {
let resp = Response::empty(RequestId::Number(1));
let Response::Ok(ok) = &resp else {
panic!("expected a success response")
};
assert_eq!(ok.result, serde_json::json!({ "resultType": "complete" }));
}
#[test]
fn an_existing_discriminator_is_never_overwritten() {
let resp = Response::success(
RequestId::Number(1),
serde_json::json!({ "resultType": "input_required", "requestState": "abc" }),
);
assert_eq!(resp.result_type(), Some(ResultType::InputRequired));
}
#[test]
fn a_non_object_result_is_passed_through() {
let resp = Response::success(RequestId::Number(1), serde_json::json!([1, 2, 3]));
let Response::Ok(ok) = &resp else {
panic!("expected a success response")
};
assert_eq!(ok.result, serde_json::json!([1, 2, 3]));
assert_eq!(resp.result_type(), Some(ResultType::Complete));
}
#[test]
fn a_legacy_shaped_result_without_the_field_reads_as_complete() {
let resp = parse(r#"{"jsonrpc":"2.0","id":1,"result":{"content":[]}}"#);
assert_eq!(resp.result_type(), Some(ResultType::Complete));
}
#[test]
fn an_unrecognized_discriminator_reads_as_complete() {
let resp = parse(r#"{"jsonrpc":"2.0","id":1,"result":{"resultType":"whatever"}}"#);
assert_eq!(resp.result_type(), Some(ResultType::Complete));
}
#[test]
fn server_info_is_stamped_into_result_meta() {
use crate::types::Implementation;
let resp = Response::success(RequestId::Number(1), serde_json::json!({ "tools": [] }))
.with_server_info(&Implementation {
name: "srv".into(),
version: "1.2.3".into(),
icons: None,
});
let Response::Ok(ok) = &resp else {
panic!("expected a success response")
};
let info = &ok.result["_meta"]["io.modelcontextprotocol/serverInfo"];
assert_eq!(info["name"], "srv");
assert_eq!(info["version"], "1.2.3");
}
#[test]
fn server_info_never_overwrites_an_existing_entry() {
use crate::types::Implementation;
let resp = Response::success(
RequestId::Number(1),
serde_json::json!({
"_meta": { "io.modelcontextprotocol/serverInfo": { "name": "kept", "version": "0" } }
}),
)
.with_server_info(&Implementation {
name: "srv".into(),
version: "1.2.3".into(),
icons: None,
});
let Response::Ok(ok) = &resp else {
panic!("expected a success response")
};
assert_eq!(
ok.result["_meta"]["io.modelcontextprotocol/serverInfo"]["name"],
"kept"
);
}
#[test]
fn an_error_response_carries_no_server_info() {
use crate::types::Implementation;
let resp = Response::error(RequestId::Number(1), Error::new(-32603, "boom"))
.with_server_info(&Implementation {
name: "srv".into(),
version: "1.2.3".into(),
icons: None,
});
assert!(matches!(resp, Response::Err(_)));
}
#[test]
fn a_preconstructed_response_gets_the_discriminator() {
let resp = parse(r#"{"jsonrpc":"2.0","id":1,"result":{"content":[]}}"#);
assert_eq!(
resp.result_type(),
Some(ResultType::Complete),
"an absent field already reads as complete"
);
let Response::Ok(ok) = resp.with_result_type() else {
panic!("a successful response")
};
assert_eq!(
ok.result["resultType"], "complete",
"...and it must also be written out"
);
}
#[test]
fn an_existing_discriminator_is_left_alone() {
let resp = parse(
r#"{"jsonrpc":"2.0","id":1,"result":{"resultType":"input_required","inputRequests":{}}}"#,
);
let Response::Ok(ok) = resp.with_result_type() else {
panic!("a successful response")
};
assert_eq!(ok.result["resultType"], "input_required");
}
#[test]
fn an_error_response_has_no_result_type() {
let resp = Response::error(RequestId::Number(1), Error::new(-32603, "boom"));
assert_eq!(resp.result_type(), None);
}
#[test]
fn the_discriminator_survives_a_wire_round_trip() {
let resp = Response::success(RequestId::Number(1), serde_json::json!({ "tools": [] }));
let back = parse(&serde_json::to_string(&resp).unwrap());
assert_eq!(back.result_type(), Some(ResultType::Complete));
}
}
#[cfg(test)]
#[cfg(all(feature = "server", not(feature = "legacy-spec")))]
mod result_type_per_type_tests {
use super::{Response, ResultType};
use crate::types::{IntoResponse, RequestId};
fn round_trip<T>(result: impl IntoResponse)
where
T: serde::de::DeserializeOwned,
{
let resp = result.into_response(RequestId::Number(1));
assert_eq!(
resp.result_type(),
Some(ResultType::Complete),
"result is missing the `complete` discriminator"
);
let wire = serde_json::to_string(&resp).unwrap();
let back: Response = serde_json::from_str(&wire).unwrap();
assert_eq!(back.result_type(), Some(ResultType::Complete));
back.into_result::<T>()
.expect("the typed result must still parse with `resultType` present");
}
#[test]
fn tools_results_carry_it() {
use crate::types::{CallToolResponse, ListToolsResult};
round_trip::<ListToolsResult>(ListToolsResult::default());
round_trip::<CallToolResponse>(CallToolResponse::new("ok"));
}
#[test]
fn prompts_results_carry_it() {
use crate::types::{GetPromptResult, ListPromptsResult};
round_trip::<ListPromptsResult>(ListPromptsResult::default());
round_trip::<GetPromptResult>(GetPromptResult::default());
}
#[test]
fn resources_results_carry_it() {
use crate::types::{ListResourceTemplatesResult, ListResourcesResult, ReadResourceResult};
round_trip::<ListResourcesResult>(ListResourcesResult::default());
round_trip::<ListResourceTemplatesResult>(ListResourceTemplatesResult::default());
round_trip::<ReadResourceResult>(ReadResourceResult::default());
}
#[test]
fn completion_results_carry_it() {
use crate::types::CompleteResult;
round_trip::<CompleteResult>(CompleteResult::default());
}
#[test]
fn discover_results_carry_it() {
use crate::app::options::McpOptions;
use crate::types::DiscoverResult;
round_trip::<DiscoverResult>(DiscoverResult::new(&McpOptions::default()));
}
#[cfg(feature = "tasks")]
#[test]
fn task_results_carry_it() {
use crate::types::{DetailedTask, Task, TaskPayload};
round_trip::<DetailedTask>(DetailedTask::from(Task::new()));
round_trip::<TaskPayload>(TaskPayload(serde_json::json!({ "content": [] })));
}
#[cfg(feature = "tasks")]
#[test]
fn a_created_task_is_tagged_task_not_complete() {
use crate::types::{CreateTaskResult, Task};
let resp = CreateTaskResult::new(Task::new()).into_response(RequestId::Number(1));
assert_eq!(resp.result_type(), Some(ResultType::Task));
let Response::Ok(ok) = &resp else {
panic!("expected a success response")
};
assert!(ok.result.get("taskId").is_some(), "got: {}", ok.result);
assert!(ok.result.get("task").is_none(), "must not be nested");
}
}