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
Expand Up @@ -459,16 +459,18 @@ public class StreamableHttpServerTransport(private val configuration: Configurat
}

val messages = parseBody(call) ?: return
val isInitializationRequest = messages.any {
it is JSONRPCRequest && it.method == Method.Defined.Initialize.value
val initializationRequest = messages.filterIsInstance<JSONRPCRequest>().firstOrNull {
it.method == Method.Defined.Initialize.value
}
val isInitializationRequest = initializationRequest != null

if (isInitializationRequest) {
if (initialized.load() && sessionId != null) {
call.reject(
HttpStatusCode.BadRequest,
RPCError.ErrorCode.INVALID_REQUEST,
"Invalid Request: Server already initialized",
initializationRequest.id,
)
return
}
Expand Down Expand Up @@ -506,8 +508,7 @@ public class StreamableHttpServerTransport(private val configuration: Configurat
// 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")
(initializationRequest.params as? JsonObject)?.get("protocolVersion")
?.let { McpJson.decodeFromJsonElement<String>(it) }
?: DEFAULT_NEGOTIATED_PROTOCOL_VERSION
} else {
Expand Down Expand Up @@ -913,7 +914,12 @@ public class StreamableHttpServerTransport(private val configuration: Configurat
}
}

internal suspend fun ApplicationCall.reject(status: HttpStatusCode, code: Int, message: String) {
internal suspend fun ApplicationCall.reject(
status: HttpStatusCode,
code: Int,
message: String,
id: RequestId? = null,
) {
this.response.status(status)
this.respond(JSONRPCError(id = null, error = RPCError(code = code, message = message)))
this.respond(JSONRPCError(id = id, error = RPCError(code = code, message = message)))
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import io.modelcontextprotocol.kotlin.sdk.types.Implementation
import io.modelcontextprotocol.kotlin.sdk.types.InitializeRequest
import io.modelcontextprotocol.kotlin.sdk.types.InitializeRequestParams
import io.modelcontextprotocol.kotlin.sdk.types.InitializedNotification
import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCError
import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage
import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCRequest
import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCResponse
Expand Down Expand Up @@ -163,7 +164,7 @@ class StreamableHttpServerTransportTest {
}

@Test
fun `second initialization request returns an HTTP error`() = testApplication {
fun `second initialization request returns JSON-RPC error with request id`() = testApplication {
configTestServer()

val client = createTestClient()
Expand All @@ -186,13 +187,17 @@ class StreamableHttpServerTransportTest {

firstResponse.status shouldBe HttpStatusCode.OK

val secondRequest = buildInitializeRequestPayload().copy(id = RequestId("second-init"))
val secondResponse = client.post(path) {
addStreamableHeaders()
header("mcp-session-id", firstResponse.headers[MCP_SESSION_ID_HEADER])
setBody(payload)
setBody(secondRequest)
}

secondResponse.status shouldBe HttpStatusCode.BadRequest
val error = secondResponse.body<JSONRPCError>()
error.id shouldBe secondRequest.id
error.error.message shouldBe "Invalid Request: Server already initialized"
}

@Test
Expand Down
Loading