Skip to content

Commit 48890f1

Browse files
committed
Merge branch 'main' into basil/resource_streams
2 parents 7a2e646 + 96e5327 commit 48890f1

File tree

14 files changed

+50
-49
lines changed

14 files changed

+50
-49
lines changed

examples/clients/simple-chatbot/mcp_simple_chatbot/main.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,10 @@ async def list_tools(self) -> list[Any]:
122122

123123
for item in tools_response:
124124
if isinstance(item, tuple) and item[0] == "tools":
125-
for tool in item[1]:
126-
tools.append(Tool(tool.name, tool.description, tool.inputSchema))
125+
tools.extend(
126+
Tool(tool.name, tool.description, tool.inputSchema)
127+
for tool in item[1]
128+
)
127129

128130
return tools
129131

@@ -282,10 +284,9 @@ def __init__(self, servers: list[Server], llm_client: LLMClient) -> None:
282284

283285
async def cleanup_servers(self) -> None:
284286
"""Clean up all servers properly."""
285-
cleanup_tasks = []
286-
for server in self.servers:
287-
cleanup_tasks.append(asyncio.create_task(server.cleanup()))
288-
287+
cleanup_tasks = [
288+
asyncio.create_task(server.cleanup()) for server in self.servers
289+
]
289290
if cleanup_tasks:
290291
try:
291292
await asyncio.gather(*cleanup_tasks, return_exceptions=True)

pyproject.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ dependencies = [
2929
"starlette>=0.27",
3030
"sse-starlette>=1.6.1",
3131
"pydantic-settings>=2.5.2",
32-
"uvicorn>=0.23.1",
32+
"uvicorn>=0.23.1; sys_platform != 'emscripten'",
3333
]
3434

3535
[project.optional-dependencies]
@@ -89,8 +89,8 @@ venv = ".venv"
8989
strict = ["src/mcp/**/*.py"]
9090

9191
[tool.ruff.lint]
92-
select = ["E", "F", "I", "UP"]
93-
ignore = []
92+
select = ["C4", "E", "F", "I", "PERF", "UP"]
93+
ignore = ["PERF203"]
9494

9595
[tool.ruff]
9696
line-length = 88

src/mcp/client/session.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,10 @@ async def unsubscribe_resource(self, uri: AnyUrl) -> types.EmptyResult:
254254
)
255255

256256
async def call_tool(
257-
self, name: str, arguments: dict[str, Any] | None = None
257+
self,
258+
name: str,
259+
arguments: dict[str, Any] | None = None,
260+
read_timeout_seconds: timedelta | None = None,
258261
) -> types.CallToolResult:
259262
"""Send a tools/call request."""
260263
return await self.send_request(
@@ -265,6 +268,7 @@ async def call_tool(
265268
)
266269
),
267270
types.CallToolResult,
271+
request_read_timeout_seconds=read_timeout_seconds,
268272
)
269273

270274
async def list_prompts(self) -> types.ListPromptsResult:

src/mcp/server/fastmcp/prompts/base.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
"""Base classes for FastMCP prompts."""
22

33
import inspect
4-
import json
54
from collections.abc import Awaitable, Callable, Sequence
65
from typing import Any, Literal
76

@@ -155,7 +154,9 @@ async def render(self, arguments: dict[str, Any] | None = None) -> list[Message]
155154
content = TextContent(type="text", text=msg)
156155
messages.append(UserMessage(content=content))
157156
else:
158-
content = json.dumps(pydantic_core.to_jsonable_python(msg))
157+
content = pydantic_core.to_json(
158+
msg, fallback=str, indent=2
159+
).decode()
159160
messages.append(Message(role="user", content=content))
160161
except Exception:
161162
raise ValueError(

src/mcp/server/fastmcp/resources/types.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import anyio
1010
import anyio.to_thread
1111
import httpx
12-
import pydantic.json
12+
import pydantic
1313
import pydantic_core
1414
from pydantic import Field, ValidationInfo
1515

@@ -59,15 +59,12 @@ async def read(self) -> str | bytes:
5959
)
6060
if isinstance(result, Resource):
6161
return await result.read()
62-
if isinstance(result, bytes):
62+
elif isinstance(result, bytes):
6363
return result
64-
if isinstance(result, str):
64+
elif isinstance(result, str):
6565
return result
66-
try:
67-
return json.dumps(pydantic_core.to_jsonable_python(result))
68-
except (TypeError, pydantic_core.PydanticSerializationError):
69-
# If JSON serialization fails, try str()
70-
return str(result)
66+
else:
67+
return pydantic_core.to_json(result, fallback=str, indent=2).decode()
7168
except Exception as e:
7269
raise ValueError(f"Error reading resource {self.uri}: {e}")
7370

src/mcp/server/fastmcp/server.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
from __future__ import annotations as _annotations
44

55
import inspect
6-
import json
76
import re
87
from collections.abc import AsyncIterator, Callable, Iterable, Sequence
98
from contextlib import (
@@ -15,7 +14,6 @@
1514

1615
import anyio
1716
import pydantic_core
18-
import uvicorn
1917
from pydantic import BaseModel, Field
2018
from pydantic.networks import AnyUrl
2119
from pydantic_settings import BaseSettings, SettingsConfigDict
@@ -466,6 +464,8 @@ async def run_stdio_async(self) -> None:
466464

467465
async def run_sse_async(self) -> None:
468466
"""Run the server using SSE transport."""
467+
import uvicorn
468+
469469
starlette_app = self.sse_app()
470470

471471
config = uvicorn.Config(
@@ -550,10 +550,7 @@ def _convert_to_content(
550550
return list(chain.from_iterable(_convert_to_content(item) for item in result)) # type: ignore[reportUnknownVariableType]
551551

552552
if not isinstance(result, str):
553-
try:
554-
result = json.dumps(pydantic_core.to_jsonable_python(result))
555-
except Exception:
556-
result = str(result)
553+
result = pydantic_core.to_json(result, fallback=str, indent=2).decode()
557554

558555
return [TextContent(type="text", text=result)]
559556

src/mcp/server/fastmcp/utilities/func_metadata.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ def pre_parse_json(self, data: dict[str, Any]) -> dict[str, Any]:
8080
dicts (JSON objects) as JSON strings, which can be pre-parsed here.
8181
"""
8282
new_data = data.copy() # Shallow copy
83-
for field_name, _field_info in self.arg_model.model_fields.items():
83+
for field_name in self.arg_model.model_fields.keys():
8484
if field_name not in data.keys():
8585
continue
8686
if isinstance(data[field_name], str):

src/mcp/shared/session.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ def __init__(
185185
self._request_id = 0
186186
self._receive_request_type = receive_request_type
187187
self._receive_notification_type = receive_notification_type
188-
self._read_timeout_seconds = read_timeout_seconds
188+
self._session_read_timeout_seconds = read_timeout_seconds
189189
self._in_flight = {}
190190
self._exit_stack = AsyncExitStack()
191191

@@ -212,10 +212,12 @@ async def send_request(
212212
self,
213213
request: SendRequestT,
214214
result_type: type[ReceiveResultT],
215+
request_read_timeout_seconds: timedelta | None = None,
215216
) -> ReceiveResultT:
216217
"""
217218
Sends a request and wait for a response. Raises an McpError if the
218-
response contains an error.
219+
response contains an error. If a request read timeout is provided, it
220+
will take precedence over the session read timeout.
219221
220222
Do not use this method to emit notifications! Use send_notification()
221223
instead.
@@ -240,12 +242,15 @@ async def send_request(
240242

241243
await self._write_stream.send(JSONRPCMessage(jsonrpc_request))
242244

245+
# request read timeout takes precedence over session read timeout
246+
timeout = None
247+
if request_read_timeout_seconds is not None:
248+
timeout = request_read_timeout_seconds.total_seconds()
249+
elif self._session_read_timeout_seconds is not None:
250+
timeout = self._session_read_timeout_seconds.total_seconds()
251+
243252
try:
244-
with anyio.fail_after(
245-
None
246-
if self._read_timeout_seconds is None
247-
else self._read_timeout_seconds.total_seconds()
248-
):
253+
with anyio.fail_after(timeout):
249254
response_or_error = await response_stream_reader.receive()
250255
except TimeoutError:
251256
raise McpError(
@@ -254,7 +259,7 @@ async def send_request(
254259
message=(
255260
f"Timed out while waiting for response to "
256261
f"{request.__class__.__name__}. Waited "
257-
f"{self._read_timeout_seconds} seconds."
262+
f"{timeout} seconds."
258263
),
259264
)
260265
)

tests/issues/test_342_base64_encoding.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ async def test_server_base64_encoding_issue():
4242

4343
# Create binary data that will definitely result in + and / characters
4444
# when encoded with standard base64
45-
binary_data = bytes([x for x in range(255)] * 4)
45+
binary_data = bytes(list(range(255)) * 4)
4646

4747
# Register a resource handler that returns our test data
4848
@server.read_resource()

tests/server/fastmcp/prompts/test_base.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ async def fn(name: str, age: int = 30) -> str:
3838
return f"Hello, {name}! You're {age} years old."
3939

4040
prompt = Prompt.from_function(fn)
41-
assert await prompt.render(arguments=dict(name="World")) == [
41+
assert await prompt.render(arguments={"name": "World"}) == [
4242
UserMessage(
4343
content=TextContent(
4444
type="text", text="Hello, World! You're 30 years old."
@@ -53,7 +53,7 @@ async def fn(name: str, age: int = 30) -> str:
5353

5454
prompt = Prompt.from_function(fn)
5555
with pytest.raises(ValueError):
56-
await prompt.render(arguments=dict(age=40))
56+
await prompt.render(arguments={"age": 40})
5757

5858
@pytest.mark.anyio
5959
async def test_fn_returns_message(self):

0 commit comments

Comments
 (0)