fix(server): preserve id on duplicate initialize errors (#868)
## Summary Fixes #866. ## Changes - Preserve the parsed JSON-RPC request id when Streamable HTTP rejects a duplicate `initialize` request. - Keep pre-parse and transport-level rejection paths unchanged when the request id is not available. - Extend the duplicate-initialize regression test to assert the error response id as well as the HTTP status. ## Verification ```bash ./gradlew :kotlin-sdk-server:jvmTest --tests 'io.modelcontextprotocol.kotlin.sdk.server.StreamableHttpServerTransportTest.second initialization request returns JSON-RPC error with request id' ./gradlew :kotlin-sdk-server:jvmTest --tests 'io.modelcontextprotocol.kotlin.sdk.server.StreamableHttpServerTransportTest' ./gradlew :kotlin-sdk-server:jvmTest ./gradlew :kotlin-sdk-server:ktlintCheck :kotlin-sdk-server:detekt ``` Co-authored-by: Pavel Gorgulov <devcrocod@gmail.com>
This commit is contained in:
parent
9308473745
commit
b6cb547d58
2 changed files with 19 additions and 8 deletions
|
|
@ -459,9 +459,10 @@ 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) {
|
||||
|
|
@ -469,6 +470,7 @@ public class StreamableHttpServerTransport(private val configuration: Configurat
|
|||
HttpStatusCode.BadRequest,
|
||||
RPCError.ErrorCode.INVALID_REQUEST,
|
||||
"Invalid Request: Server already initialized",
|
||||
initializationRequest.id,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
@ -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)))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue