Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .github/workflows/shared.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ jobs:
test:
runs-on: ${{ matrix.os }}
timeout-minutes: 10
continue-on-error: true
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]
Expand All @@ -45,4 +46,11 @@ jobs:

- name: Run pytest
run: uv run --frozen --no-sync pytest
continue-on-error: true

# This must run last as it modifies the environment!
- name: Run pytest with lowest versions
run: |
uv sync --all-extras --upgrade
uv run --no-sync pytest
env:
UV_RESOLUTION: lowest-direct
8 changes: 4 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ dependencies = [
"anyio>=4.5",
"httpx>=0.27",
"httpx-sse>=0.4",
"pydantic>=2.7.2,<3.0.0",
"pydantic>=2.8.0,<3.0.0",
"starlette>=0.27",
"python-multipart>=0.0.9",
"sse-starlette>=1.6.1",
Expand All @@ -36,14 +36,13 @@ dependencies = [

[project.optional-dependencies]
rich = ["rich>=13.9.4"]
cli = ["typer>=0.12.4", "python-dotenv>=1.0.0"]
cli = ["typer>=0.16.0", "python-dotenv>=1.0.0"]
ws = ["websockets>=15.0.1"]

[project.scripts]
mcp = "mcp.cli:app [cli]"

[tool.uv]
resolution = "lowest-direct"
default-groups = ["dev", "docs"]
required-version = ">=0.7.2"

Expand All @@ -58,6 +57,7 @@ dev = [
"pytest-examples>=0.0.14",
"pytest-pretty>=1.2.0",
"inline-snapshot>=0.23.0",
"dirty-equals>=0.9.0",
]
docs = [
"mkdocs>=1.6.1",
Expand Down Expand Up @@ -123,5 +123,5 @@ filterwarnings = [
# This should be fixed on Uvicorn's side.
"ignore::DeprecationWarning:websockets",
"ignore:websockets.server.WebSocketServerProtocol is deprecated:DeprecationWarning",
"ignore:Returning str or bytes.*:DeprecationWarning:mcp.server.lowlevel"
"ignore:Returning str or bytes.*:DeprecationWarning:mcp.server.lowlevel",
]
56 changes: 19 additions & 37 deletions tests/issues/test_188_concurrency.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,58 +3,40 @@
from pydantic import AnyUrl

from mcp.server.fastmcp import FastMCP
from mcp.shared.memory import (
create_connected_server_and_client_session as create_session,
)
from mcp.shared.memory import create_connected_server_and_client_session as create_session

_sleep_time_seconds = 0.01
_resource_name = "slow://slow_resource"


@pytest.mark.anyio
async def test_messages_are_executed_concurrently():
server = FastMCP("test")
call_timestamps = []
event = anyio.Event()
call_order = []

@server.tool("sleep")
async def sleep_tool():
call_timestamps.append(("tool_start_time", anyio.current_time()))
await anyio.sleep(_sleep_time_seconds)
call_timestamps.append(("tool_end_time", anyio.current_time()))
call_order.append("waiting_for_event")
await event.wait()
call_order.append("tool_end")
return "done"

@server.resource(_resource_name)
async def slow_resource():
call_timestamps.append(("resource_start_time", anyio.current_time()))
await anyio.sleep(_sleep_time_seconds)
call_timestamps.append(("resource_end_time", anyio.current_time()))
# Set event immediately
event.set()
call_order.append("resource_end")
return "slow"

async with create_session(server._mcp_server) as client_session:
# Start both requests concurrently
async with anyio.create_task_group() as tg:
for _ in range(10):
tg.start_soon(client_session.call_tool, "sleep")
tg.start_soon(client_session.read_resource, AnyUrl(_resource_name))

active_calls = 0
max_concurrent_calls = 0
for call_type, _ in sorted(call_timestamps, key=lambda x: x[1]):
if "start" in call_type:
active_calls += 1
max_concurrent_calls = max(max_concurrent_calls, active_calls)
else:
active_calls -= 1
print(f"Max concurrent calls: {max_concurrent_calls}")
assert max_concurrent_calls > 1, "No concurrent calls were executed"


def main():
anyio.run(test_messages_are_executed_concurrently)


if __name__ == "__main__":
import logging

logging.basicConfig(level=logging.DEBUG)

main()
tg.start_soon(client_session.call_tool, "sleep")
tg.start_soon(client_session.read_resource, AnyUrl(_resource_name))

# Verify that both ran concurrently
assert call_order == [
"waiting_for_event",
"resource_end",
"tool_end",
], f"Expected concurrent execution, but got: {call_order}"
8 changes: 3 additions & 5 deletions tests/server/fastmcp/test_func_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import annotated_types
import pytest
from dirty_equals import IsPartialDict
from pydantic import BaseModel, Field

from mcp.server.fastmcp.utilities.func_metadata import func_metadata
Expand Down Expand Up @@ -202,11 +203,8 @@ def func_dict_any() -> dict[str, Any]:
return {"a": 1, "b": "hello", "c": [1, 2, 3]}

meta = func_metadata(func_dict_any)
assert meta.output_schema == {
"additionalProperties": True,
"type": "object",
"title": "func_dict_anyDictOutput",
}

assert meta.output_schema == IsPartialDict(type="object", title="func_dict_anyDictOutput")

# Test dict[str, str]
def func_dict_str() -> dict[str, str]:
Expand Down
Loading
Loading