Skip to content

Commit 84952a6

Browse files
jskjw157kpavlov
authored andcommitted
feat(server): support configurable max request payload size (#521)
1 parent ad07232 commit 84952a6

4 files changed

Lines changed: 73 additions & 10 deletions

File tree

kotlin-sdk-server/api/kotlin-sdk-server.api

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -220,13 +220,14 @@ public final class io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServe
220220
}
221221

222222
public final class io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport$Configuration {
223-
public synthetic fun <init> (ZZLjava/util/List;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/server/EventStore;Lkotlin/time/Duration;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
224-
public synthetic fun <init> (ZZLjava/util/List;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/server/EventStore;Lkotlin/time/Duration;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
223+
public synthetic fun <init> (ZZLjava/util/List;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/server/EventStore;Lkotlin/time/Duration;JILkotlin/jvm/internal/DefaultConstructorMarker;)V
224+
public synthetic fun <init> (ZZLjava/util/List;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/server/EventStore;Lkotlin/time/Duration;JLkotlin/jvm/internal/DefaultConstructorMarker;)V
225225
public final fun getAllowedHosts ()Ljava/util/List;
226226
public final fun getAllowedOrigins ()Ljava/util/List;
227227
public final fun getEnableDnsRebindingProtection ()Z
228228
public final fun getEnableJsonResponse ()Z
229229
public final fun getEventStore ()Lio/modelcontextprotocol/kotlin/sdk/server/EventStore;
230+
public final fun getMaxRequestBodySize ()J
230231
public final fun getRetryInterval-FghU774 ()Lkotlin/time/Duration;
231232
}
232233

kotlin-sdk-server/detekt-baseline-main.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
<CurrentIssues>
55
<ID>InjectDispatcher:FeatureNotificationService.kt:FeatureNotificationService$Default</ID>
66
<ID>LongParameterList:KtorServer.kt:private suspend fun RoutingContext.streamableTransport: StreamableHttpServerTransport?</ID>
7+
<ID>LongParameterList:StreamableHttpServerTransport.kt:StreamableHttpServerTransport.Configuration</ID>
78
<ID>MagicNumber:StdioServerTransport.kt:StdioServerTransport$8192</ID>
89
<ID>MaxLineLength:SSEServerTransport.kt:SseServerTransport$"SSEServerTransport already started! If using Server class, note that connect() calls start() automatically."</ID>
910
<ID>MaxLineLength:SSEServerTransport.kt:SseServerTransport$*</ID>

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

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ import kotlin.uuid.Uuid
4545
internal const val MCP_SESSION_ID_HEADER = "mcp-session-id"
4646
private const val MCP_PROTOCOL_VERSION_HEADER = "mcp-protocol-version"
4747
private const val MCP_RESUMPTION_TOKEN_HEADER = "Last-Event-ID"
48-
private const val MAXIMUM_MESSAGE_SIZE = 4 * 1024 * 1024 // 4 MB
48+
private const val DEFAULT_MAX_REQUEST_BODY_SIZE: Long = 4L * 1024 * 1024 // 4 MB
4949
private const val MIN_PRIMING_EVENT_PROTOCOL_VERSION = "2025-11-25"
5050

5151
/**
@@ -141,6 +141,9 @@ public class StreamableHttpServerTransport(private val configuration: Configurat
141141
*
142142
* @property retryInterval Retry interval for event handling or reconnection attempts.
143143
* Defaults to `null`.
144+
*
145+
* @property maxRequestBodySize Maximum allowed size (in bytes) for incoming request bodies.
146+
* Defaults to 4 MB (4,194,304 bytes).
144147
*/
145148
public class Configuration(
146149
public val enableJsonResponse: Boolean = false,
@@ -149,7 +152,14 @@ public class StreamableHttpServerTransport(private val configuration: Configurat
149152
public val allowedOrigins: List<String>? = null,
150153
public val eventStore: EventStore? = null,
151154
public val retryInterval: Duration? = null,
152-
)
155+
public val maxRequestBodySize: Long = DEFAULT_MAX_REQUEST_BODY_SIZE,
156+
) {
157+
init {
158+
require(maxRequestBodySize > 0) {
159+
"maxRequestBodySize must be greater than 0"
160+
}
161+
}
162+
}
153163

154164
public var sessionId: String? = null
155165
private set
@@ -661,24 +671,25 @@ public class StreamableHttpServerTransport(private val configuration: Configurat
661671
}
662672
}
663673

664-
@Suppress("ReturnCount", "MagicNumber")
674+
@Suppress("ReturnCount")
665675
private suspend fun parseBody(call: ApplicationCall): List<JSONRPCMessage>? {
666-
val contentLength = call.request.header(HttpHeaders.ContentLength)?.toIntOrNull() ?: 0
667-
if (contentLength > MAXIMUM_MESSAGE_SIZE) {
676+
val maxSize = configuration.maxRequestBodySize
677+
val contentLength = call.request.header(HttpHeaders.ContentLength)?.toLongOrNull() ?: 0L
678+
if (contentLength > maxSize) {
668679
call.reject(
669680
HttpStatusCode.PayloadTooLarge,
670681
RPCError.ErrorCode.INVALID_REQUEST,
671-
"Invalid Request: message size exceeds maximum of ${MAXIMUM_MESSAGE_SIZE / (1024 * 1024)} MB",
682+
"Invalid Request: message size exceeds maximum of $maxSize bytes",
672683
)
673684
return null
674685
}
675686

676687
val body = call.receiveText()
677-
if (body.length > MAXIMUM_MESSAGE_SIZE) {
688+
if (body.length.toLong() > maxSize) {
678689
call.reject(
679690
HttpStatusCode.PayloadTooLarge,
680691
RPCError.ErrorCode.INVALID_REQUEST,
681-
"Invalid Request: message size exceeds maximum of ${MAXIMUM_MESSAGE_SIZE / (1024 * 1024)} MB",
692+
"Invalid Request: message size exceeds maximum of $maxSize bytes",
682693
)
683694
return null
684695
}

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

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,12 @@ import kotlinx.serialization.builtins.ListSerializer
4343
import kotlinx.serialization.json.buildJsonObject
4444
import kotlinx.serialization.json.put
4545
import org.junit.jupiter.params.ParameterizedTest
46+
import org.junit.jupiter.params.provider.Arguments
4647
import org.junit.jupiter.params.provider.MethodSource
4748
import java.util.concurrent.atomic.AtomicBoolean
4849
import kotlin.test.Test
4950
import kotlin.test.assertEquals
51+
import kotlin.test.assertFailsWith
5052
import kotlin.test.assertFalse
5153
import kotlin.test.assertNotNull
5254
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation as ClientContentNegotiation
@@ -63,6 +65,15 @@ class StreamableHttpServerTransportTest {
6365
null,
6466
"lolol",
6567
)
68+
69+
private val sizeTestPayload = "x".repeat(64)
70+
71+
@JvmStatic
72+
fun maxBodySizeTestCases(): List<Arguments> = listOf(
73+
Arguments.of(sizeTestPayload.length.toLong() - 1, HttpStatusCode.PayloadTooLarge),
74+
Arguments.of(sizeTestPayload.length.toLong(), HttpStatusCode.BadRequest),
75+
Arguments.of(sizeTestPayload.length.toLong() + 1, HttpStatusCode.BadRequest),
76+
)
6677
}
6778

6879
private val path = "/transport"
@@ -383,6 +394,45 @@ class StreamableHttpServerTransportTest {
383394
response.status shouldBe HttpStatusCode.PayloadTooLarge
384395
}
385396

397+
@ParameterizedTest
398+
@MethodSource("maxBodySizeTestCases")
399+
fun `POST with custom max request body size validates payload size`(
400+
maxSize: Long,
401+
expectedStatus: HttpStatusCode,
402+
) = testApplication {
403+
configTestServer()
404+
405+
val client = createTestClient()
406+
407+
val transport = StreamableHttpServerTransport(
408+
StreamableHttpServerTransport.Configuration(
409+
enableJsonResponse = true,
410+
maxRequestBodySize = maxSize,
411+
),
412+
)
413+
transport.onMessage { message ->
414+
if (message is JSONRPCRequest) {
415+
transport.send(JSONRPCResponse(message.id, EmptyResult()))
416+
}
417+
}
418+
419+
configureTransportEndpoint(transport)
420+
421+
val response = client.post(path) {
422+
addStreamableHeaders()
423+
setBody(sizeTestPayload)
424+
}
425+
426+
response.status shouldBe expectedStatus
427+
}
428+
429+
@Test
430+
fun `Configuration with negative maxRequestBodySize throws IllegalArgumentException`() {
431+
assertFailsWith<IllegalArgumentException> {
432+
StreamableHttpServerTransport.Configuration(maxRequestBodySize = -1)
433+
}
434+
}
435+
386436
private fun ApplicationTestBuilder.configureTransportEndpoint(transport: StreamableHttpServerTransport) {
387437
application {
388438
routing {

0 commit comments

Comments
 (0)