Skip to content

Commit bce6811

Browse files
authored
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
1 parent eb24803 commit bce6811

File tree

3 files changed

+49
-24
lines changed

3 files changed

+49
-24
lines changed

kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/common.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import kotlinx.serialization.json.JsonObject
1212
public const val LATEST_PROTOCOL_VERSION: String = "2025-11-25"
1313

1414
/** The default protocol version used when negotiation is not performed. */
15-
public const val DEFAULT_NEGOTIATED_PROTOCOL_VERSION: String = "2025-06-18"
15+
public const val DEFAULT_NEGOTIATED_PROTOCOL_VERSION: String = "2025-03-26"
1616

1717
/** All MCP protocol versions supported by this SDK. */
1818
public val SUPPORTED_PROTOCOL_VERSIONS: List<String> = listOf(

kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,7 @@ public class StreamableHttpServerTransport(private val configuration: Configurat
367367
)
368368
return
369369
}
370+
if (!validateProtocolVersion(call)) return
370371
if (messages.size > 1) {
371372
call.reject(
372373
HttpStatusCode.BadRequest,
@@ -393,11 +394,23 @@ public class StreamableHttpServerTransport(private val configuration: Configurat
393394
return
394395
}
395396

397+
// Extract protocol version for priming event decision.
398+
// For initialize requests, get from request params.
399+
// For other requests, get from header (already validated).
400+
val clientProtocolVersion = if (isInitializationRequest) {
401+
val initRequest = messages.first() as JSONRPCRequest
402+
(initRequest.params as? JsonObject)?.get("protocolVersion")
403+
?.let { McpJson.decodeFromJsonElement<String>(it) }
404+
?: DEFAULT_NEGOTIATED_PROTOCOL_VERSION
405+
} else {
406+
call.request.header(MCP_PROTOCOL_VERSION_HEADER) ?: DEFAULT_NEGOTIATED_PROTOCOL_VERSION
407+
}
408+
396409
val streamId = Uuid.random().toString()
397410
if (!configuration.enableJsonResponse) {
398411
call.appendSseHeaders()
399412
flushSse(session) // flush headers immediately
400-
maybeSendPrimingEvent(streamId, session, call.request.header(MCP_PROTOCOL_VERSION_HEADER))
413+
maybeSendPrimingEvent(streamId, session, clientProtocolVersion)
401414
}
402415

403416
streamMutex.withLock {
@@ -456,7 +469,9 @@ public class StreamableHttpServerTransport(private val configuration: Configurat
456469
// SSE headers (Content-Type, Cache-Control, Connection) are already set by the framework's SSE handler
457470
flushSse(sseSession)
458471
streamsMapping[STANDALONE_SSE_STREAM_ID] = SessionContext(sseSession, call)
459-
maybeSendPrimingEvent(STANDALONE_SSE_STREAM_ID, sseSession, call.request.header(MCP_PROTOCOL_VERSION_HEADER))
472+
val clientProtocolVersion =
473+
call.request.header(MCP_PROTOCOL_VERSION_HEADER) ?: DEFAULT_NEGOTIATED_PROTOCOL_VERSION
474+
maybeSendPrimingEvent(STANDALONE_SSE_STREAM_ID, sseSession, clientProtocolVersion)
460475
sseSession.coroutineContext.job.invokeOnCompletion {
461476
streamsMapping.remove(STANDALONE_SSE_STREAM_ID)
462477
}
@@ -568,9 +583,9 @@ public class StreamableHttpServerTransport(private val configuration: Configurat
568583
return false
569584
}
570585

571-
val sessionHeaderValues = call.request.headers.getAll(MCP_SESSION_ID_HEADER)
586+
val headerId = call.request.header(MCP_SESSION_ID_HEADER)
572587

573-
if (sessionHeaderValues.isNullOrEmpty()) {
588+
if (headerId == null) {
574589
call.reject(
575590
HttpStatusCode.BadRequest,
576591
RPCError.ErrorCode.CONNECTION_CLOSED,
@@ -579,17 +594,6 @@ public class StreamableHttpServerTransport(private val configuration: Configurat
579594
return false
580595
}
581596

582-
if (sessionHeaderValues.size > 1) {
583-
call.reject(
584-
HttpStatusCode.BadRequest,
585-
RPCError.ErrorCode.CONNECTION_CLOSED,
586-
"Bad Request: Mcp-Session-Id header must be a single value",
587-
)
588-
return false
589-
}
590-
591-
val headerId = sessionHeaderValues.single()
592-
593597
return when (headerId) {
594598
sessionId -> true
595599

@@ -605,8 +609,7 @@ public class StreamableHttpServerTransport(private val configuration: Configurat
605609
}
606610

607611
private suspend fun validateProtocolVersion(call: ApplicationCall): Boolean {
608-
val protocolVersions = call.request.headers.getAll(MCP_PROTOCOL_VERSION_HEADER)
609-
val version = protocolVersions?.lastOrNull() ?: DEFAULT_NEGOTIATED_PROTOCOL_VERSION
612+
val version = call.request.headers[MCP_PROTOCOL_VERSION_HEADER] ?: return true
610613

611614
return when (version) {
612615
!in SUPPORTED_PROTOCOL_VERSIONS -> {
@@ -715,14 +718,14 @@ public class StreamableHttpServerTransport(private val configuration: Configurat
715718
private suspend fun maybeSendPrimingEvent(
716719
streamId: String,
717720
session: ServerSSESession?,
718-
clientProtocolVersion: String? = null,
721+
clientProtocolVersion: String,
719722
) {
720723
val store = configuration.eventStore
721724
if (store == null || session == null) return
722725
// Priming events have empty data which older clients cannot handle.
723726
// Only send priming events to clients with protocol version >= 2025-11-25
724727
// which includes the fix for handling empty SSE data.
725-
if (clientProtocolVersion != null && clientProtocolVersion < MIN_PRIMING_EVENT_PROTOCOL_VERSION) return
728+
if (clientProtocolVersion < MIN_PRIMING_EVENT_PROTOCOL_VERSION) return
726729
try {
727730
val primingEventId = store.storeEvent(streamId, JSONRPCEmptyMessage)
728731
session.send(

kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransportTest.kt

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,31 @@ class StreamableHttpServerTransportTest {
176176
secondResponse.status shouldBe HttpStatusCode.BadRequest
177177
}
178178

179+
@Test
180+
fun `init request with unsupported protocol version returns an HTTP error`() = testApplication {
181+
configTestServer()
182+
183+
val client = createTestClient()
184+
185+
val transport = StreamableHttpServerTransport(enableJsonResponse = true)
186+
transport.onMessage { message ->
187+
if (message is JSONRPCRequest) {
188+
transport.send(JSONRPCResponse(message.id, EmptyResult()))
189+
}
190+
}
191+
192+
configureTransportEndpoint(transport)
193+
194+
val initResponse = client.post(path) {
195+
addStreamableHeaders()
196+
header("mcp-protocol-version", "1900-01-01")
197+
setBody(buildInitializeRequestPayload())
198+
}
199+
200+
initResponse.status shouldBe HttpStatusCode.BadRequest
201+
initResponse.headers[MCP_SESSION_ID_HEADER] shouldBe null
202+
}
203+
179204
@Test
180205
fun `request with unsupported protocol version returns an HTTP error`() = testApplication {
181206
configTestServer()
@@ -191,18 +216,15 @@ class StreamableHttpServerTransportTest {
191216

192217
configureTransportEndpoint(transport)
193218

194-
val initPayload = buildInitializeRequestPayload()
195219
val initResponse = client.post(path) {
196220
addStreamableHeaders()
197-
setBody(initPayload)
221+
setBody(buildInitializeRequestPayload())
198222
}
199223

200224
initResponse.status shouldBe HttpStatusCode.OK
201225
val sessionId = initResponse.headers[MCP_SESSION_ID_HEADER]
202226
assertNotNull(sessionId)
203227

204-
// TODO When https://github.com/modelcontextprotocol/kotlin-sdk/issues/547 is fixed,
205-
// check the incompatible mcp-protocol-version in the InitializeRequest and delete the part below
206228
val response = client.post(path) {
207229
addStreamableHeaders()
208230
header("mcp-session-id", sessionId)

0 commit comments

Comments
 (0)