Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Copyright 2026-2026 the original author or authors.
*/

package io.modelcontextprotocol.server.transport;

import java.io.IOException;
import java.io.PrintWriter;

import io.modelcontextprotocol.json.McpJsonMapper;
import io.modelcontextprotocol.spec.McpError;
import io.modelcontextprotocol.spec.McpSchema;

import jakarta.servlet.http.HttpServletResponse;

/**
* Writes JSON-RPC error response bodies for servlet transports.
*
* @author Taewoong Kim
*/
final class HttpServletJsonRpcErrorWriter {

private static final String UTF_8 = "UTF-8";

private static final String APPLICATION_JSON = "application/json";

private HttpServletJsonRpcErrorWriter() {
}

static void writeError(McpJsonMapper jsonMapper, HttpServletResponse response, int httpStatus, Object requestId,
McpError mcpError) throws IOException {
writeError(jsonMapper, response, httpStatus, requestId, mcpError.getJsonRpcError());
}

static void writeError(McpJsonMapper jsonMapper, HttpServletResponse response, int httpStatus, Object requestId,
McpSchema.JSONRPCResponse.JSONRPCError error) throws IOException {
response.setContentType(APPLICATION_JSON);
response.setCharacterEncoding(UTF_8);
response.setStatus(httpStatus);

String jsonErrorResponse = jsonMapper.writeValueAsString(jsonRpcErrorResponse(requestId, error));
PrintWriter writer = response.getWriter();
writer.write(jsonErrorResponse);
writer.flush();
}

private static Object jsonRpcErrorResponse(Object requestId, McpSchema.JSONRPCResponse.JSONRPCError error) {
if (requestId != null) {
return McpSchema.JSONRPCResponse.error(requestId, error);
}

// McpSchema.JSONRPCResponse requires a non-null id, but servlet transport
// errors can be generated before a JSON-RPC request id is available. The MCP
// JSONRPCErrorResponse schema permits omitting id in that case.
return new JsonRpcErrorResponse(McpSchema.JSONRPC_VERSION, error);
}

private record JsonRpcErrorResponse(String jsonrpc, McpSchema.JSONRPCResponse.JSONRPCError error) {
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -358,33 +358,24 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response)
// Get the session ID from the request parameter
String sessionId = request.getParameter("sessionId");
if (sessionId == null) {
response.setContentType(APPLICATION_JSON);
response.setCharacterEncoding(UTF_8);
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
String jsonError = jsonMapper.writeValueAsString(McpError.builder(McpSchema.ErrorCodes.METHOD_NOT_FOUND)
.message("Session ID missing in message endpoint")
.build());
PrintWriter writer = response.getWriter();
writer.write(jsonError);
writer.flush();
this.responseError(response, HttpServletResponse.SC_BAD_REQUEST, null,
McpError.builder(McpSchema.ErrorCodes.METHOD_NOT_FOUND)
.message("Session ID missing in message endpoint")
.build());
return;
}

// Get the session from the sessions map
McpServerSession session = sessions.get(sessionId);
if (session == null) {
response.setContentType(APPLICATION_JSON);
response.setCharacterEncoding(UTF_8);
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
String jsonError = jsonMapper.writeValueAsString(McpError.builder(McpSchema.ErrorCodes.INTERNAL_ERROR)
.message("Session not found: " + sessionId)
.build());
PrintWriter writer = response.getWriter();
writer.write(jsonError);
writer.flush();
this.responseError(response, HttpServletResponse.SC_NOT_FOUND, null,
McpError.builder(McpSchema.ErrorCodes.INTERNAL_ERROR)
.message("Session not found: " + sessionId)
.build());
return;
}

Object requestId = null;
try {
BufferedReader reader = request.getReader();
StringBuilder body = new StringBuilder();
Expand All @@ -395,6 +386,7 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response)

final McpTransportContext transportContext = this.contextExtractor.extract(request);
McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(jsonMapper, body.toString());
requestId = requestId(message);

// Process the message through the session's handle method
// Block for Servlet compatibility
Expand All @@ -408,13 +400,7 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response)
McpError mcpError = McpError.builder(McpSchema.ErrorCodes.INTERNAL_ERROR)
.message(e.getMessage())
.build();
response.setContentType(APPLICATION_JSON);
response.setCharacterEncoding(UTF_8);
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
String jsonError = jsonMapper.writeValueAsString(mcpError);
PrintWriter writer = response.getWriter();
writer.write(jsonError);
writer.flush();
this.responseError(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, requestId, mcpError);
}
catch (IOException ex) {
logger.error(FAILED_TO_SEND_ERROR_RESPONSE, ex.getMessage());
Expand Down Expand Up @@ -461,6 +447,15 @@ private void sendEvent(PrintWriter writer, String eventType, String data) throws
}
}

private void responseError(HttpServletResponse response, int httpCode, Object requestId, McpError mcpError)
throws IOException {
HttpServletJsonRpcErrorWriter.writeError(this.jsonMapper, response, httpCode, requestId, mcpError);
}

private static Object requestId(McpSchema.JSONRPCMessage message) {
return (message instanceof McpSchema.JSONRPCRequest request) ? request.id() : null;
}

/**
* Cleans up resources when the servlet is being destroyed.
* <p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,10 +181,15 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response)
}
catch (Exception e) {
logger.error("Failed to handle request: {}", e.getMessage());
this.responseError(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
McpError.builder(McpSchema.ErrorCodes.INTERNAL_ERROR)
.message("Failed to handle request: " + e.getMessage())
.build());
if (e instanceof McpError mcpError) {
this.responseError(response, HttpServletResponse.SC_OK, jsonrpcRequest.id(), mcpError);
}
else {
this.responseError(response, HttpServletResponse.SC_OK, jsonrpcRequest.id(),
McpError.builder(McpSchema.ErrorCodes.INTERNAL_ERROR)
.message("Failed to handle request: " + e.getMessage())
.build());
}
}
}
else if (message instanceof McpSchema.JSONRPCNotification jsonrpcNotification) {
Expand Down Expand Up @@ -231,13 +236,12 @@ else if (message instanceof McpSchema.JSONRPCNotification jsonrpcNotification) {
* @throws IOException If an I/O error occurs
*/
private void responseError(HttpServletResponse response, int httpCode, McpError mcpError) throws IOException {
response.setContentType(APPLICATION_JSON);
response.setCharacterEncoding(UTF_8);
response.setStatus(httpCode);
String jsonError = jsonMapper.writeValueAsString(mcpError);
PrintWriter writer = response.getWriter();
writer.write(jsonError);
writer.flush();
this.responseError(response, httpCode, null, mcpError);
}

private void responseError(HttpServletResponse response, int httpCode, Object requestId, McpError mcpError)
throws IOException {
HttpServletJsonRpcErrorWriter.writeError(this.jsonMapper, response, httpCode, requestId, mcpError);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,7 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response)

McpTransportContext transportContext = this.contextExtractor.extract(request);

McpSchema.JSONRPCMessage message = null;
try {
BufferedReader reader = request.getReader();
StringBuilder body = new StringBuilder();
Expand All @@ -435,14 +436,14 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response)
body.append(line);
}

McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(jsonMapper, body.toString());
message = McpSchema.deserializeJsonRpcMessage(jsonMapper, body.toString());

// Handle initialization request
if (message instanceof McpSchema.JSONRPCRequest jsonrpcRequest
&& jsonrpcRequest.method().equals(McpSchema.METHOD_INITIALIZE)) {
if (!badRequestErrors.isEmpty()) {
String combinedMessage = String.join("; ", badRequestErrors);
this.responseError(response, HttpServletResponse.SC_BAD_REQUEST,
this.responseError(response, HttpServletResponse.SC_BAD_REQUEST, jsonrpcRequest.id(),
McpError.builder(McpSchema.ErrorCodes.METHOD_NOT_FOUND).message(combinedMessage).build());
return;
}
Expand Down Expand Up @@ -472,7 +473,7 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response)
}
catch (Exception e) {
logger.error("Failed to initialize session: {}", e.getMessage());
this.responseError(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
this.responseError(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, jsonrpcRequest.id(),
McpError.builder(McpSchema.ErrorCodes.INTERNAL_ERROR)
.message("Failed to initialize session: " + e.getMessage())
.build());
Expand All @@ -488,15 +489,15 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response)

if (!badRequestErrors.isEmpty()) {
String combinedMessage = String.join("; ", badRequestErrors);
this.responseError(response, HttpServletResponse.SC_BAD_REQUEST,
this.responseError(response, HttpServletResponse.SC_BAD_REQUEST, requestId(message),
McpError.builder(McpSchema.ErrorCodes.METHOD_NOT_FOUND).message(combinedMessage).build());
return;
}

McpStreamableServerSession session = this.sessions.get(sessionId);

if (session == null) {
this.responseError(response, HttpServletResponse.SC_NOT_FOUND,
this.responseError(response, HttpServletResponse.SC_NOT_FOUND, requestId(message),
McpError.builder(McpSchema.ErrorCodes.INTERNAL_ERROR)
.message("Session not found: " + sessionId)
.build());
Expand Down Expand Up @@ -539,21 +540,21 @@ else if (message instanceof McpSchema.JSONRPCRequest jsonrpcRequest) {
}
}
else {
this.responseError(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
this.responseError(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, requestId(message),
McpError.builder(McpSchema.ErrorCodes.INVALID_REQUEST).message("Unknown message type").build());
}
}
catch (IllegalArgumentException | IOException e) {
logger.error("Failed to deserialize message: {}", e.getMessage());
this.responseError(response, HttpServletResponse.SC_BAD_REQUEST,
this.responseError(response, HttpServletResponse.SC_BAD_REQUEST, requestId(message),
McpError.builder(McpSchema.ErrorCodes.INVALID_REQUEST)
.message("Invalid message format: " + e.getMessage())
.build());
}
catch (Exception e) {
logger.error("Error handling message: {}", e.getMessage());
try {
this.responseError(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
this.responseError(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, requestId(message),
McpError.builder(McpSchema.ErrorCodes.INTERNAL_ERROR)
.message("Error processing message: " + e.getMessage())
.build());
Expand Down Expand Up @@ -638,14 +639,16 @@ protected void doDelete(HttpServletRequest request, HttpServletResponse response
}

public void responseError(HttpServletResponse response, int httpCode, McpError mcpError) throws IOException {
response.setContentType(APPLICATION_JSON);
response.setCharacterEncoding(UTF_8);
response.setStatus(httpCode);
String jsonError = jsonMapper.writeValueAsString(mcpError);
PrintWriter writer = response.getWriter();
writer.write(jsonError);
writer.flush();
return;
this.responseError(response, httpCode, null, mcpError);
}

private void responseError(HttpServletResponse response, int httpCode, Object requestId, McpError mcpError)
throws IOException {
HttpServletJsonRpcErrorWriter.writeError(this.jsonMapper, response, httpCode, requestId, mcpError);
}

private static Object requestId(McpSchema.JSONRPCMessage message) {
return (message instanceof McpSchema.JSONRPCRequest request) ? request.id() : null;
}

/**
Expand Down
Loading