fix: validate protocol version header on initialization requests (#697)
Validate `mcp-protocol-version` HTTP header during `InitializeRequest` Fix `DEFAULT_NEGOTIATED_PROTOCOL_VERSION` to match the spec fixes #547 ## How Has This Been Tested? Updated existing test (removed TODO referencing #547) to verify init request ## Breaking Changes none ## Types of changes - [x] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [x] I have added appropriate error handling - [x] I have added or updated documentation as needed
This commit is contained in:
parent
eb24803547
commit
bce6811b66
3 changed files with 49 additions and 24 deletions
|
|
@ -12,7 +12,7 @@ import kotlinx.serialization.json.JsonObject
|
|||
public const val LATEST_PROTOCOL_VERSION: String = "2025-11-25"
|
||||
|
||||
/** The default protocol version used when negotiation is not performed. */
|
||||
public const val DEFAULT_NEGOTIATED_PROTOCOL_VERSION: String = "2025-06-18"
|
||||
public const val DEFAULT_NEGOTIATED_PROTOCOL_VERSION: String = "2025-03-26"
|
||||
|
||||
/** All MCP protocol versions supported by this SDK. */
|
||||
public val SUPPORTED_PROTOCOL_VERSIONS: List<String> = listOf(
|
||||
|
|
|
|||
|
|
@ -367,6 +367,7 @@ public class StreamableHttpServerTransport(private val configuration: Configurat
|
|||
)
|
||||
return
|
||||
}
|
||||
if (!validateProtocolVersion(call)) return
|
||||
if (messages.size > 1) {
|
||||
call.reject(
|
||||
HttpStatusCode.BadRequest,
|
||||
|
|
@ -393,11 +394,23 @@ public class StreamableHttpServerTransport(private val configuration: Configurat
|
|||
return
|
||||
}
|
||||
|
||||
// Extract protocol version for priming event decision.
|
||||
// For initialize requests, get from request params.
|
||||
// For other requests, get from header (already validated).
|
||||
val clientProtocolVersion = if (isInitializationRequest) {
|
||||
val initRequest = messages.first() as JSONRPCRequest
|
||||
(initRequest.params as? JsonObject)?.get("protocolVersion")
|
||||
?.let { McpJson.decodeFromJsonElement<String>(it) }
|
||||
?: DEFAULT_NEGOTIATED_PROTOCOL_VERSION
|
||||
} else {
|
||||
call.request.header(MCP_PROTOCOL_VERSION_HEADER) ?: DEFAULT_NEGOTIATED_PROTOCOL_VERSION
|
||||
}
|
||||
|
||||
val streamId = Uuid.random().toString()
|
||||
if (!configuration.enableJsonResponse) {
|
||||
call.appendSseHeaders()
|
||||
flushSse(session) // flush headers immediately
|
||||
maybeSendPrimingEvent(streamId, session, call.request.header(MCP_PROTOCOL_VERSION_HEADER))
|
||||
maybeSendPrimingEvent(streamId, session, clientProtocolVersion)
|
||||
}
|
||||
|
||||
streamMutex.withLock {
|
||||
|
|
@ -456,7 +469,9 @@ public class StreamableHttpServerTransport(private val configuration: Configurat
|
|||
// SSE headers (Content-Type, Cache-Control, Connection) are already set by the framework's SSE handler
|
||||
flushSse(sseSession)
|
||||
streamsMapping[STANDALONE_SSE_STREAM_ID] = SessionContext(sseSession, call)
|
||||
maybeSendPrimingEvent(STANDALONE_SSE_STREAM_ID, sseSession, call.request.header(MCP_PROTOCOL_VERSION_HEADER))
|
||||
val clientProtocolVersion =
|
||||
call.request.header(MCP_PROTOCOL_VERSION_HEADER) ?: DEFAULT_NEGOTIATED_PROTOCOL_VERSION
|
||||
maybeSendPrimingEvent(STANDALONE_SSE_STREAM_ID, sseSession, clientProtocolVersion)
|
||||
sseSession.coroutineContext.job.invokeOnCompletion {
|
||||
streamsMapping.remove(STANDALONE_SSE_STREAM_ID)
|
||||
}
|
||||
|
|
@ -568,9 +583,9 @@ public class StreamableHttpServerTransport(private val configuration: Configurat
|
|||
return false
|
||||
}
|
||||
|
||||
val sessionHeaderValues = call.request.headers.getAll(MCP_SESSION_ID_HEADER)
|
||||
val headerId = call.request.header(MCP_SESSION_ID_HEADER)
|
||||
|
||||
if (sessionHeaderValues.isNullOrEmpty()) {
|
||||
if (headerId == null) {
|
||||
call.reject(
|
||||
HttpStatusCode.BadRequest,
|
||||
RPCError.ErrorCode.CONNECTION_CLOSED,
|
||||
|
|
@ -579,17 +594,6 @@ public class StreamableHttpServerTransport(private val configuration: Configurat
|
|||
return false
|
||||
}
|
||||
|
||||
if (sessionHeaderValues.size > 1) {
|
||||
call.reject(
|
||||
HttpStatusCode.BadRequest,
|
||||
RPCError.ErrorCode.CONNECTION_CLOSED,
|
||||
"Bad Request: Mcp-Session-Id header must be a single value",
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
val headerId = sessionHeaderValues.single()
|
||||
|
||||
return when (headerId) {
|
||||
sessionId -> true
|
||||
|
||||
|
|
@ -605,8 +609,7 @@ public class StreamableHttpServerTransport(private val configuration: Configurat
|
|||
}
|
||||
|
||||
private suspend fun validateProtocolVersion(call: ApplicationCall): Boolean {
|
||||
val protocolVersions = call.request.headers.getAll(MCP_PROTOCOL_VERSION_HEADER)
|
||||
val version = protocolVersions?.lastOrNull() ?: DEFAULT_NEGOTIATED_PROTOCOL_VERSION
|
||||
val version = call.request.headers[MCP_PROTOCOL_VERSION_HEADER] ?: return true
|
||||
|
||||
return when (version) {
|
||||
!in SUPPORTED_PROTOCOL_VERSIONS -> {
|
||||
|
|
@ -715,14 +718,14 @@ public class StreamableHttpServerTransport(private val configuration: Configurat
|
|||
private suspend fun maybeSendPrimingEvent(
|
||||
streamId: String,
|
||||
session: ServerSSESession?,
|
||||
clientProtocolVersion: String? = null,
|
||||
clientProtocolVersion: String,
|
||||
) {
|
||||
val store = configuration.eventStore
|
||||
if (store == null || session == null) return
|
||||
// Priming events have empty data which older clients cannot handle.
|
||||
// Only send priming events to clients with protocol version >= 2025-11-25
|
||||
// which includes the fix for handling empty SSE data.
|
||||
if (clientProtocolVersion != null && clientProtocolVersion < MIN_PRIMING_EVENT_PROTOCOL_VERSION) return
|
||||
if (clientProtocolVersion < MIN_PRIMING_EVENT_PROTOCOL_VERSION) return
|
||||
try {
|
||||
val primingEventId = store.storeEvent(streamId, JSONRPCEmptyMessage)
|
||||
session.send(
|
||||
|
|
|
|||
|
|
@ -176,6 +176,31 @@ class StreamableHttpServerTransportTest {
|
|||
secondResponse.status shouldBe HttpStatusCode.BadRequest
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `init request with unsupported protocol version returns an HTTP error`() = testApplication {
|
||||
configTestServer()
|
||||
|
||||
val client = createTestClient()
|
||||
|
||||
val transport = StreamableHttpServerTransport(enableJsonResponse = true)
|
||||
transport.onMessage { message ->
|
||||
if (message is JSONRPCRequest) {
|
||||
transport.send(JSONRPCResponse(message.id, EmptyResult()))
|
||||
}
|
||||
}
|
||||
|
||||
configureTransportEndpoint(transport)
|
||||
|
||||
val initResponse = client.post(path) {
|
||||
addStreamableHeaders()
|
||||
header("mcp-protocol-version", "1900-01-01")
|
||||
setBody(buildInitializeRequestPayload())
|
||||
}
|
||||
|
||||
initResponse.status shouldBe HttpStatusCode.BadRequest
|
||||
initResponse.headers[MCP_SESSION_ID_HEADER] shouldBe null
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `request with unsupported protocol version returns an HTTP error`() = testApplication {
|
||||
configTestServer()
|
||||
|
|
@ -191,18 +216,15 @@ class StreamableHttpServerTransportTest {
|
|||
|
||||
configureTransportEndpoint(transport)
|
||||
|
||||
val initPayload = buildInitializeRequestPayload()
|
||||
val initResponse = client.post(path) {
|
||||
addStreamableHeaders()
|
||||
setBody(initPayload)
|
||||
setBody(buildInitializeRequestPayload())
|
||||
}
|
||||
|
||||
initResponse.status shouldBe HttpStatusCode.OK
|
||||
val sessionId = initResponse.headers[MCP_SESSION_ID_HEADER]
|
||||
assertNotNull(sessionId)
|
||||
|
||||
// TODO When https://github.com/modelcontextprotocol/kotlin-sdk/issues/547 is fixed,
|
||||
// check the incompatible mcp-protocol-version in the InitializeRequest and delete the part below
|
||||
val response = client.post(path) {
|
||||
addStreamableHeaders()
|
||||
header("mcp-session-id", sessionId)
|
||||
|
|
|
|||
Loading…
Reference in a new issue