Skip to content

Commit ddf9bc9

Browse files
committed
feat(server): support configurable max request payload size (#521)
Add maxRequestBodySize parameter to StreamableHttpServerTransport.Configuration, defaulting to 4 MB. This allows consumers to set a lower request body size limit without manual payload counting. - Add maxRequestBodySize: Long to Configuration with require(> 0) validation - Replace hardcoded MAXIMUM_MESSAGE_SIZE with configurable value in parseBody() - Use Long for content-length comparison to avoid Int overflow - Error message displays exact byte limit for clarity at any configured size - Note: body.length check after receiveText() uses character count (pre-existing behavior); byte-accurate fallback is tracked separately
1 parent fd3a858 commit ddf9bc9

4 files changed

Lines changed: 86 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
@@ -222,13 +222,14 @@ public final class io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServe
222222
}
223223

224224
public final class io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport$Configuration {
225-
public synthetic fun <init> (ZZLjava/util/List;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/server/EventStore;Lkotlin/time/Duration;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
226-
public synthetic fun <init> (ZZLjava/util/List;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/server/EventStore;Lkotlin/time/Duration;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
225+
public synthetic fun <init> (ZZLjava/util/List;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/server/EventStore;Lkotlin/time/Duration;JILkotlin/jvm/internal/DefaultConstructorMarker;)V
226+
public synthetic fun <init> (ZZLjava/util/List;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/server/EventStore;Lkotlin/time/Duration;JLkotlin/jvm/internal/DefaultConstructorMarker;)V
227227
public final fun getAllowedHosts ()Ljava/util/List;
228228
public final fun getAllowedOrigins ()Ljava/util/List;
229229
public final fun getEnableDnsRebindingProtection ()Z
230230
public final fun getEnableJsonResponse ()Z
231231
public final fun getEventStore ()Lio/modelcontextprotocol/kotlin/sdk/server/EventStore;
232+
public final fun getMaxRequestBodySize ()J
232233
public final fun getRetryInterval-FghU774 ()Lkotlin/time/Duration;
233234
}
234235

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
@@ -659,24 +669,25 @@ public class StreamableHttpServerTransport(private val configuration: Configurat
659669
}
660670
}
661671

662-
@Suppress("ReturnCount", "MagicNumber")
672+
@Suppress("ReturnCount")
663673
private suspend fun parseBody(call: ApplicationCall): List<JSONRPCMessage>? {
664-
val contentLength = call.request.header(HttpHeaders.ContentLength)?.toIntOrNull() ?: 0
665-
if (contentLength > MAXIMUM_MESSAGE_SIZE) {
674+
val maxSize = configuration.maxRequestBodySize
675+
val contentLength = call.request.header(HttpHeaders.ContentLength)?.toLongOrNull() ?: 0L
676+
if (contentLength > maxSize) {
666677
call.reject(
667678
HttpStatusCode.PayloadTooLarge,
668679
RPCError.ErrorCode.INVALID_REQUEST,
669-
"Invalid Request: message size exceeds maximum of ${MAXIMUM_MESSAGE_SIZE / (1024 * 1024)} MB",
680+
"Invalid Request: message size exceeds maximum of $maxSize bytes",
670681
)
671682
return null
672683
}
673684

674685
val body = call.receiveText()
675-
if (body.length > MAXIMUM_MESSAGE_SIZE) {
686+
if (body.length.toLong() > maxSize) {
676687
call.reject(
677688
HttpStatusCode.PayloadTooLarge,
678689
RPCError.ErrorCode.INVALID_REQUEST,
679-
"Invalid Request: message size exceeds maximum of ${MAXIMUM_MESSAGE_SIZE / (1024 * 1024)} MB",
690+
"Invalid Request: message size exceeds maximum of $maxSize bytes",
680691
)
681692
return null
682693
}

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

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package io.modelcontextprotocol.kotlin.sdk.server
33
import io.kotest.matchers.collections.shouldContainAll
44
import io.kotest.matchers.equals.shouldBeEqual
55
import io.kotest.matchers.shouldBe
6+
import io.kotest.matchers.shouldNotBe
67
import io.ktor.client.HttpClient
78
import io.ktor.client.call.body
89
import io.ktor.client.plugins.logging.LogLevel
@@ -383,6 +384,68 @@ class StreamableHttpServerTransportTest {
383384
response.status shouldBe HttpStatusCode.PayloadTooLarge
384385
}
385386

387+
@Test
388+
fun `POST with custom max request body size rejects oversized payload`() = testApplication {
389+
configTestServer()
390+
391+
val client = createTestClient()
392+
393+
val customMaxSize = 1024L // 1 KB
394+
val transport = StreamableHttpServerTransport(
395+
StreamableHttpServerTransport.Configuration(
396+
enableJsonResponse = true,
397+
maxRequestBodySize = customMaxSize,
398+
),
399+
)
400+
transport.onMessage { message ->
401+
if (message is JSONRPCRequest) {
402+
transport.send(JSONRPCResponse(message.id, EmptyResult()))
403+
}
404+
}
405+
406+
configureTransportEndpoint(transport)
407+
408+
val oversizedPayload = "x".repeat(customMaxSize.toInt() + 1)
409+
410+
val response = client.post(path) {
411+
addStreamableHeaders()
412+
setBody(oversizedPayload)
413+
}
414+
415+
response.status shouldBe HttpStatusCode.PayloadTooLarge
416+
}
417+
418+
@Test
419+
fun `POST at exactly custom max request body size is not rejected as payload too large`() = testApplication {
420+
configTestServer()
421+
422+
val client = createTestClient()
423+
424+
val payloadBody = "x".repeat(256)
425+
val transport = StreamableHttpServerTransport(
426+
StreamableHttpServerTransport.Configuration(
427+
enableJsonResponse = true,
428+
maxRequestBodySize = payloadBody.length.toLong(),
429+
),
430+
)
431+
transport.onMessage { message ->
432+
if (message is JSONRPCRequest) {
433+
transport.send(JSONRPCResponse(message.id, EmptyResult()))
434+
}
435+
}
436+
437+
configureTransportEndpoint(transport)
438+
439+
val response = client.post(path) {
440+
addStreamableHeaders()
441+
setBody(payloadBody)
442+
}
443+
444+
// Size check should pass — the body is exactly at the limit
445+
response.status shouldNotBe HttpStatusCode.PayloadTooLarge
446+
response.status shouldBe HttpStatusCode.BadRequest
447+
}
448+
386449
private fun ApplicationTestBuilder.configureTransportEndpoint(transport: StreamableHttpServerTransport) {
387450
application {
388451
routing {

0 commit comments

Comments
 (0)