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 messages = parseBody(call) ?: return
|
||||||
val isInitializationRequest = messages.any {
|
val initializationRequest = messages.filterIsInstance<JSONRPCRequest>().firstOrNull {
|
||||||
it is JSONRPCRequest && it.method == Method.Defined.Initialize.value
|
it.method == Method.Defined.Initialize.value
|
||||||
}
|
}
|
||||||
|
val isInitializationRequest = initializationRequest != null
|
||||||
|
|
||||||
if (isInitializationRequest) {
|
if (isInitializationRequest) {
|
||||||
if (initialized.load() && sessionId != null) {
|
if (initialized.load() && sessionId != null) {
|
||||||
|
|
@ -469,6 +470,7 @@ public class StreamableHttpServerTransport(private val configuration: Configurat
|
||||||
HttpStatusCode.BadRequest,
|
HttpStatusCode.BadRequest,
|
||||||
RPCError.ErrorCode.INVALID_REQUEST,
|
RPCError.ErrorCode.INVALID_REQUEST,
|
||||||
"Invalid Request: Server already initialized",
|
"Invalid Request: Server already initialized",
|
||||||
|
initializationRequest.id,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -506,8 +508,7 @@ public class StreamableHttpServerTransport(private val configuration: Configurat
|
||||||
// For initialize requests, get from request params.
|
// For initialize requests, get from request params.
|
||||||
// For other requests, get from header (already validated).
|
// For other requests, get from header (already validated).
|
||||||
val clientProtocolVersion = if (isInitializationRequest) {
|
val clientProtocolVersion = if (isInitializationRequest) {
|
||||||
val initRequest = messages.first() as JSONRPCRequest
|
(initializationRequest.params as? JsonObject)?.get("protocolVersion")
|
||||||
(initRequest.params as? JsonObject)?.get("protocolVersion")
|
|
||||||
?.let { McpJson.decodeFromJsonElement<String>(it) }
|
?.let { McpJson.decodeFromJsonElement<String>(it) }
|
||||||
?: DEFAULT_NEGOTIATED_PROTOCOL_VERSION
|
?: DEFAULT_NEGOTIATED_PROTOCOL_VERSION
|
||||||
} else {
|
} 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.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.InitializeRequest
|
||||||
import io.modelcontextprotocol.kotlin.sdk.types.InitializeRequestParams
|
import io.modelcontextprotocol.kotlin.sdk.types.InitializeRequestParams
|
||||||
import io.modelcontextprotocol.kotlin.sdk.types.InitializedNotification
|
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.JSONRPCMessage
|
||||||
import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCRequest
|
import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCRequest
|
||||||
import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCResponse
|
import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCResponse
|
||||||
|
|
@ -163,7 +164,7 @@ class StreamableHttpServerTransportTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `second initialization request returns an HTTP error`() = testApplication {
|
fun `second initialization request returns JSON-RPC error with request id`() = testApplication {
|
||||||
configTestServer()
|
configTestServer()
|
||||||
|
|
||||||
val client = createTestClient()
|
val client = createTestClient()
|
||||||
|
|
@ -186,13 +187,17 @@ class StreamableHttpServerTransportTest {
|
||||||
|
|
||||||
firstResponse.status shouldBe HttpStatusCode.OK
|
firstResponse.status shouldBe HttpStatusCode.OK
|
||||||
|
|
||||||
|
val secondRequest = buildInitializeRequestPayload().copy(id = RequestId("second-init"))
|
||||||
val secondResponse = client.post(path) {
|
val secondResponse = client.post(path) {
|
||||||
addStreamableHeaders()
|
addStreamableHeaders()
|
||||||
header("mcp-session-id", firstResponse.headers[MCP_SESSION_ID_HEADER])
|
header("mcp-session-id", firstResponse.headers[MCP_SESSION_ID_HEADER])
|
||||||
setBody(payload)
|
setBody(secondRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
secondResponse.status shouldBe HttpStatusCode.BadRequest
|
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
|
@Test
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue