Skip to content

Commit 429f8df

Browse files
committed
feat: Make maximum message size configurable
1 parent 4d45bb8 commit 429f8df

File tree

5 files changed

+59
-2
lines changed

5 files changed

+59
-2
lines changed

src/mcp/server/fastmcp/server.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ class Settings(BaseSettings, Generic[LifespanResultT]):
9696
# StreamableHTTP settings
9797
json_response: bool = False
9898
stateless_http: bool = False # If True, uses true stateless mode (new transport per request)
99+
maximum_message_size: int | None = None # Specified in bytes
99100

100101
# resource settings
101102
warn_on_duplicate_resources: bool = True
@@ -815,6 +816,7 @@ def streamable_http_app(self) -> Starlette:
815816
json_response=self.settings.json_response,
816817
stateless=self.settings.stateless_http, # Use the stateless setting
817818
security_settings=self.settings.transport_security,
819+
maximum_message_size=self.settings.maximum_message_size,
818820
)
819821

820822
# Create the ASGI handler

src/mcp/server/streamable_http.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,13 +135,15 @@ class StreamableHTTPServerTransport:
135135
_write_stream: MemoryObjectSendStream[SessionMessage] | None = None
136136
_write_stream_reader: MemoryObjectReceiveStream[SessionMessage] | None = None
137137
_security: TransportSecurityMiddleware
138+
_maximum_message_size: int = MAXIMUM_MESSAGE_SIZE
138139

139140
def __init__(
140141
self,
141142
mcp_session_id: str | None,
142143
is_json_response_enabled: bool = False,
143144
event_store: EventStore | None = None,
144145
security_settings: TransportSecuritySettings | None = None,
146+
maximum_message_size: int | None = None,
145147
) -> None:
146148
"""
147149
Initialize a new StreamableHTTP server transport.
@@ -155,6 +157,8 @@ def __init__(
155157
resumability will be enabled, allowing clients to
156158
reconnect and resume messages.
157159
security_settings: Optional security settings for DNS rebinding protection.
160+
maximum_message_size: Optional configurable maximum message size specified
161+
in bytes
158162
159163
Raises:
160164
ValueError: If the session ID contains invalid characters.
@@ -166,6 +170,7 @@ def __init__(
166170
self.is_json_response_enabled = is_json_response_enabled
167171
self._event_store = event_store
168172
self._security = TransportSecurityMiddleware(security_settings)
173+
self._maximum_message_size = maximum_message_size if maximum_message_size else MAXIMUM_MESSAGE_SIZE
169174
self._request_streams: dict[
170175
RequestId,
171176
tuple[
@@ -329,7 +334,7 @@ async def _handle_post_request(self, scope: Scope, request: Request, receive: Re
329334

330335
# Parse the body - only read it once
331336
body = await request.body()
332-
if len(body) > MAXIMUM_MESSAGE_SIZE:
337+
if len(body) > self._maximum_message_size:
333338
response = self._create_error_response(
334339
"Payload Too Large: Message exceeds maximum size",
335340
HTTPStatus.REQUEST_ENTITY_TOO_LARGE,

src/mcp/server/streamable_http_manager.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ class StreamableHTTPSessionManager:
5252
json_response: Whether to use JSON responses instead of SSE streams
5353
stateless: If True, creates a completely fresh transport for each request
5454
with no session tracking or state persistence between requests.
55-
55+
maximum_message_size: Optional configurable maximum message size specified
56+
in bytes
5657
"""
5758

5859
def __init__(
@@ -62,12 +63,14 @@ def __init__(
6263
json_response: bool = False,
6364
stateless: bool = False,
6465
security_settings: TransportSecuritySettings | None = None,
66+
maximum_message_size: int | None = None,
6567
):
6668
self.app = app
6769
self.event_store = event_store
6870
self.json_response = json_response
6971
self.stateless = stateless
7072
self.security_settings = security_settings
73+
self.maximum_message_size = maximum_message_size
7174

7275
# Session tracking (only used if not stateless)
7376
self._session_creation_lock = anyio.Lock()
@@ -166,6 +169,7 @@ async def _handle_stateless_request(
166169
is_json_response_enabled=self.json_response,
167170
event_store=None, # No event store in stateless mode
168171
security_settings=self.security_settings,
172+
maximum_message_size=self.maximum_message_size,
169173
)
170174

171175
# Start server in a new task
@@ -222,6 +226,7 @@ async def _handle_stateful_request(
222226
is_json_response_enabled=self.json_response,
223227
event_store=self.event_store, # May be None (no resumability)
224228
security_settings=self.security_settings,
229+
maximum_message_size=self.maximum_message_size,
225230
)
226231

227232
assert http_transport.mcp_session_id is not None
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
"""Test that the maximum http input size is configurable via FastMCP settings"""
2+
3+
import pytest
4+
5+
from mcp.server.fastmcp import FastMCP
6+
7+
8+
@pytest.mark.anyio
9+
async def test_configure_input_size():
10+
"""Create a FastMCP server with StreamableHTTP transport."""
11+
configured_input_size = 1024
12+
mcp = FastMCP("Test Server", maximum_message_size=configured_input_size)
13+
14+
# Add a simple tool
15+
@mcp.tool(description="A simple echo tool")
16+
def echo(message: str) -> str:
17+
return f"Echo: {message}"
18+
19+
# Create the StreamableHTTP app
20+
_ = mcp.streamable_http_app()
21+
22+
# Check that the maximum input size is set correctly
23+
assert mcp.session_manager.maximum_message_size == configured_input_size

tests/shared/test_streamable_http.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ def create_app(is_json_response_enabled=False, event_store: EventStore | None =
236236
event_store=event_store,
237237
json_response=is_json_response_enabled,
238238
security_settings=security_settings,
239+
maximum_message_size=1024,
239240
)
240241

241242
# Create an ASGI application that uses the session manager
@@ -491,6 +492,27 @@ def test_method_not_allowed(basic_server, basic_server_url):
491492
assert "Method Not Allowed" in response.text
492493

493494

495+
def test_maximum_message_size_validation(basic_server, basic_server_url):
496+
"""Test maximum allowed input size validation."""
497+
498+
# Test with input greater than 1024 bytes
499+
large_input = "A" * 1300 # string of size 1300 bytes
500+
response = requests.post(
501+
f"{basic_server_url}/mcp",
502+
headers={
503+
"Accept": "application/json, text/event-stream",
504+
"Content-Type": "application/json",
505+
},
506+
json={
507+
"jsonrpc": "2.0",
508+
"method": "call_tool",
509+
"body": {"name": "test_tool_with_standalone_notification", "args": {"text": large_input}},
510+
},
511+
)
512+
assert response.status_code == 413
513+
assert "Payload Too Large" in response.text
514+
515+
494516
def test_session_validation(basic_server, basic_server_url):
495517
"""Test session ID validation."""
496518
# session_id not used directly in this test

0 commit comments

Comments
 (0)