fastmcp/tests/server/test_context.py
Jeremiah Lowin ee5f465a82
2.14 deprecation removals (#2329)
* Initialize 2.14 deprecation removal branch

* Remove deprecated FASTMCP_SERVER_ environment variable prefix (#2330)

* Remove deprecated Context.get_http_request method (#2332)

* Remove fastmcp.Image top-level import (deprecated 2.8.1) (#2334)

* Remove test warnings (#2331)

* Create new branch and fix issue

* Remove deprecated client parameter from FastMCPProxy (#2333)

* Remove deprecated run_streamable_http_async method (#2338)

* Remove deprecated sse_app method (#2337)

* Remove deprecated run_sse_async method (#2335)

* Remove deprecated run_sse_async method

* Update CLI and tests to use run_http_async(transport="sse")

- Change CLI to call run_http_async with transport="sse" instead of run_sse_async
- Update test to mock run_http_async with create=True for v1 servers

* Revert CLI changes - v1 servers do have run_sse_async

- Keep CLI calling run_sse_async() for v1 compatibility
- Update test to mock run_sse_async (which exists on v1)

* Remove unnecessary type ignore for run_sse_async

Method exists on v1 FastMCP class, no type error

* Remove unused imports after test deletion

* Remove deprecated streamable_http_app method (#2336)

* Remove deprecated dependencies parameter from FastMCP constructor (#2340)

* Remove output_schema=False support (deprecated 2.11.4) (#2339)

* Remove deprecated client parameter from FastMCPProxy (#2333)

* Delete deprecated test_output_schema_false.py

Tests functionality that has been removed

* Remove deprecated BearerAuthProvider module (#2341)

* Remove resource_prefix_format="protocol" support (deprecated 2.4.0) (#2342)

* Remove resource_prefix_format="protocol" support (fixes #2195)

Removes deprecated protocol format (prefix+resource://path) and keeps only
path format (resource://prefix/path). Since only one format remains:

- Removed resource_prefix_format from settings, FastMCP.__init__, and helpers
- Simplified add_resource_prefix, remove_resource_prefix, has_resource_prefix
- Removed MountedServer.resource_prefix_format field
- Deleted tests for protocol format

All resource prefixes now use path format exclusively.

* Clean up resource_prefix_format references

- Remove from test files
- Update documentation to remove protocol format section
- Move custom HTTP routes note to mounting section
- Remove resource_prefix_format from settings docs

* Use inline version note instead of badge for prefix format

* Remove obsolete test functions and update docs

- Delete test functions that no longer assert anything
- Remove proxy.mdx reference to deleted prefix format section

* Format error messages per ruff

* Remove from_client classmethod (deprecated 2.8.0) (#2343)

* Remove deprecated from_client classmethod (fixes #2192)

* Remove unused Client import

* Remove add_resource_fn method (deprecated 2.7.0) (#2345)

* Update SDK

* Add missing imports for exclude_args deprecation warning
2025-12-01 14:11:00 -05:00

182 lines
6.5 KiB
Python

from unittest.mock import MagicMock
import pytest
from mcp.types import ModelPreferences
from fastmcp.server.context import (
Context,
_parse_model_preferences,
)
from fastmcp.server.server import FastMCP
@pytest.fixture
def context():
return Context(fastmcp=FastMCP())
class TestParseModelPreferences:
def test_parse_model_preferences_string(self, context):
mp = _parse_model_preferences("claude-3-sonnet")
assert isinstance(mp, ModelPreferences)
assert mp.hints is not None
assert mp.hints[0].name == "claude-3-sonnet"
def test_parse_model_preferences_list(self, context):
mp = _parse_model_preferences(["claude-3-sonnet", "claude"])
assert isinstance(mp, ModelPreferences)
assert mp.hints is not None
assert [h.name for h in mp.hints] == ["claude-3-sonnet", "claude"]
def test_parse_model_preferences_object(self, context):
obj = ModelPreferences(hints=[])
assert _parse_model_preferences(obj) is obj
def test_parse_model_preferences_invalid_type(self, context):
with pytest.raises(ValueError):
_parse_model_preferences(model_preferences=123) # pyright: ignore[reportArgumentType] # type: ignore[invalid-argument-type]
class TestSessionId:
def test_session_id_with_http_headers(self, context):
"""Test that session_id returns the value from mcp-session-id header."""
from mcp.server.lowlevel.server import request_ctx
from mcp.shared.context import RequestContext
mock_headers = {"mcp-session-id": "test-session-123"}
token = request_ctx.set(
RequestContext( # type: ignore[arg-type]
request_id=0,
meta=None,
session=MagicMock(wraps={}),
lifespan_context=MagicMock(),
request=MagicMock(headers=mock_headers),
)
)
assert context.session_id == "test-session-123"
request_ctx.reset(token)
def test_session_id_without_http_headers(self, context):
"""Test that session_id returns a UUID string when no HTTP headers are available."""
import uuid
from mcp.server.lowlevel.server import request_ctx
from mcp.shared.context import RequestContext
token = request_ctx.set(
RequestContext( # type: ignore[arg-type]
request_id=0,
meta=None,
session=MagicMock(wraps={}),
lifespan_context=MagicMock(),
)
)
assert uuid.UUID(context.session_id)
request_ctx.reset(token)
class TestContextState:
"""Test suite for Context state functionality."""
async def test_context_state(self):
"""Test that state modifications in child contexts don't affect parent."""
mock_fastmcp = MagicMock()
async with Context(fastmcp=mock_fastmcp) as context:
assert context.get_state("test1") is None
assert context.get_state("test2") is None
context.set_state("test1", "value")
context.set_state("test2", 2)
assert context.get_state("test1") == "value"
assert context.get_state("test2") == 2
context.set_state("test1", "new_value")
assert context.get_state("test1") == "new_value"
async def test_context_state_inheritance(self):
"""Test that child contexts inherit parent state."""
mock_fastmcp = MagicMock()
async with Context(fastmcp=mock_fastmcp) as context1:
context1.set_state("key1", "key1-context1")
context1.set_state("key2", "key2-context1")
async with Context(fastmcp=mock_fastmcp) as context2:
# Override one key
context2.set_state("key1", "key1-context2")
assert context2.get_state("key1") == "key1-context2"
assert context1.get_state("key1") == "key1-context1"
assert context2.get_state("key2") == "key2-context1"
async with Context(fastmcp=mock_fastmcp) as context3:
# Verify state was inherited
assert context3.get_state("key1") == "key1-context2"
assert context3.get_state("key2") == "key2-context1"
# Add a new key and verify parents were not affected
context3.set_state("key-context3-only", 1)
assert context1.get_state("key-context3-only") is None
assert context2.get_state("key-context3-only") is None
assert context3.get_state("key-context3-only") == 1
assert context1.get_state("key1") == "key1-context1"
assert context1.get_state("key-context3-only") is None
class TestContextMeta:
"""Test suite for Context meta functionality."""
def test_request_context_meta_access(self, context):
"""Test that meta can be accessed from request context."""
from mcp.server.lowlevel.server import request_ctx
from mcp.shared.context import RequestContext
# Create a mock meta object with attributes
class MockMeta:
def __init__(self):
self.user_id = "user-123"
self.trace_id = "trace-456"
self.custom_field = "custom-value"
mock_meta = MockMeta()
token = request_ctx.set(
RequestContext( # type: ignore[arg-type]
request_id=0,
meta=mock_meta, # type: ignore[arg-type]
session=MagicMock(wraps={}),
lifespan_context=MagicMock(),
)
)
# Access meta through context
retrieved_meta = context.request_context.meta
assert retrieved_meta is not None
assert retrieved_meta.user_id == "user-123"
assert retrieved_meta.trace_id == "trace-456"
assert retrieved_meta.custom_field == "custom-value"
request_ctx.reset(token)
def test_request_context_meta_none(self, context):
"""Test that context handles None meta gracefully."""
from mcp.server.lowlevel.server import request_ctx
from mcp.shared.context import RequestContext
token = request_ctx.set(
RequestContext( # type: ignore[arg-type]
request_id=0,
meta=None,
session=MagicMock(wraps={}),
lifespan_context=MagicMock(),
)
)
# Access meta through context
retrieved_meta = context.request_context.meta
assert retrieved_meta is None
request_ctx.reset(token)