diff --git a/docs/python-sdk/fastmcp-utilities-cache.mdx b/docs/python-sdk/fastmcp-utilities-cache.mdx deleted file mode 100644 index d06374aeb..000000000 --- a/docs/python-sdk/fastmcp-utilities-cache.mdx +++ /dev/null @@ -1,30 +0,0 @@ ---- -title: cache -sidebarTitle: cache ---- - -# `fastmcp.utilities.cache` - -## Classes - -### `TimedCache` - -**Methods:** - -#### `set` - -```python -set(self, key: Any, value: Any) -> None -``` - -#### `get` - -```python -get(self, key: Any) -> Any -``` - -#### `clear` - -```python -clear(self) -> None -``` diff --git a/src/fastmcp/server/server.py b/src/fastmcp/server/server.py index c18ef2564..ff461e351 100644 --- a/src/fastmcp/server/server.py +++ b/src/fastmcp/server/server.py @@ -2,7 +2,6 @@ from __future__ import annotations -import datetime import inspect import re import warnings @@ -63,7 +62,6 @@ from fastmcp.settings import Settings from fastmcp.tools import ToolManager from fastmcp.tools.tool import FunctionTool, Tool, ToolResult from fastmcp.tools.tool_transform import ToolTransformConfig -from fastmcp.utilities.cache import TimedCache from fastmcp.utilities.cli import log_server_banner from fastmcp.utilities.components import FastMCPComponent from fastmcp.utilities.logging import get_logger @@ -174,9 +172,6 @@ class FastMCP(Generic[LifespanResultT]): resource_prefix_format or fastmcp.settings.resource_prefix_format ) - self._cache = TimedCache( - expiration=datetime.timedelta(seconds=cache_expiration_seconds or 0) - ) self._additional_http_routes: list[BaseRoute] = [] self._tool_manager = ToolManager( duplicate_behavior=on_duplicate_tools, @@ -816,7 +811,6 @@ class FastMCP(Generic[LifespanResultT]): The tool instance that was added to the server. """ self._tool_manager.add_tool(tool) - self._cache.clear() # Send notification if we're in a request context try: @@ -839,7 +833,6 @@ class FastMCP(Generic[LifespanResultT]): NotFoundError: If the tool is not found """ self._tool_manager.remove_tool(name) - self._cache.clear() # Send notification if we're in a request context try: @@ -1033,7 +1026,6 @@ class FastMCP(Generic[LifespanResultT]): The resource instance that was added to the server. """ self._resource_manager.add_resource(resource) - self._cache.clear() # Send notification if we're in a request context try: @@ -1105,7 +1097,6 @@ class FastMCP(Generic[LifespanResultT]): mime_type=mime_type, tags=tags, ) - self._cache.clear() def resource( self, @@ -1254,7 +1245,6 @@ class FastMCP(Generic[LifespanResultT]): The prompt instance that was added to the server. """ self._prompt_manager.add_prompt(prompt) - self._cache.clear() # Send notification if we're in a request context try: @@ -1803,8 +1793,6 @@ class FastMCP(Generic[LifespanResultT]): self._resource_manager.mount(mounted_server) self._prompt_manager.mount(mounted_server) - self._cache.clear() - async def import_server( self, server: FastMCP[LifespanResultT], @@ -1927,8 +1915,6 @@ class FastMCP(Generic[LifespanResultT]): else: logger.debug(f"Imported server {server.name}") - self._cache.clear() - @classmethod def from_openapi( cls, diff --git a/src/fastmcp/utilities/cache.py b/src/fastmcp/utilities/cache.py deleted file mode 100644 index 9124ad4fa..000000000 --- a/src/fastmcp/utilities/cache.py +++ /dev/null @@ -1,26 +0,0 @@ -import datetime -from typing import Any - -UTC = datetime.timezone.utc - - -class TimedCache: - NOT_FOUND = object() - - def __init__(self, expiration: datetime.timedelta): - self.expiration = expiration - self.cache: dict[Any, tuple[Any, datetime.datetime]] = {} - - def set(self, key: Any, value: Any) -> None: - expires = datetime.datetime.now(UTC) + self.expiration - self.cache[key] = (value, expires) - - def get(self, key: Any) -> Any: - value = self.cache.get(key) - if value is not None and value[1] > datetime.datetime.now(UTC): - return value[0] - else: - return self.NOT_FOUND - - def clear(self) -> None: - self.cache.clear() diff --git a/tests/server/test_mount.py b/tests/server/test_mount.py index 56369ebb8..b6dd6a6e2 100644 --- a/tests/server/test_mount.py +++ b/tests/server/test_mount.py @@ -629,7 +629,6 @@ class TestDynamicChanges: # The tool should no longer be accessible # Refresh the cache by clearing it - main_app._cache.cache.clear() tools = await main_app.get_tools() assert "sub_temp_tool" not in tools diff --git a/tests/utilities/test_cache.py b/tests/utilities/test_cache.py deleted file mode 100644 index 0d4974cd6..000000000 --- a/tests/utilities/test_cache.py +++ /dev/null @@ -1,233 +0,0 @@ -"""Tests for the cache.py module.""" - -import datetime -import time -from unittest.mock import patch - -from fastmcp.utilities.cache import TimedCache - - -class TestTimedCache: - """Tests for the TimedCache class.""" - - def test_init(self): - """Test that a TimedCache can be initialized with an expiration.""" - expiration = datetime.timedelta(seconds=10) - cache = TimedCache(expiration) - assert cache.expiration == expiration - assert isinstance(cache.cache, dict) - assert len(cache.cache) == 0 - - def test_set(self): - """Test that values can be set in the cache.""" - cache = TimedCache(datetime.timedelta(seconds=10)) - key, value = "test_key", "test_value" - - with patch("datetime.datetime") as mock_datetime: - now = datetime.datetime(2023, 1, 1, tzinfo=datetime.timezone.utc) - mock_datetime.now.return_value = now - - cache.set(key, value) - - # Check that the value is stored with the correct expiration - assert key in cache.cache - stored_value, expiration = cache.cache[key] - assert stored_value == value - assert expiration == now + datetime.timedelta(seconds=10) - - def test_get_found(self): - """Test retrieving a value that exists and has not expired.""" - cache = TimedCache(datetime.timedelta(seconds=10)) - key, value = "test_key", "test_value" - - # Set a future expiration time - future = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta( - seconds=30 - ) - cache.cache[key] = (value, future) - - # The value should be returned - assert cache.get(key) == value - - def test_get_expired(self): - """Test retrieving a value that exists but has expired.""" - cache = TimedCache(datetime.timedelta(seconds=10)) - key, value = "test_key", "test_value" - - # Set a past expiration time - past = datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta( - seconds=1 - ) - cache.cache[key] = (value, past) - - # Should return NOT_FOUND - assert cache.get(key) is TimedCache.NOT_FOUND - - def test_get_not_found(self): - """Test retrieving a value that doesn't exist in the cache.""" - cache = TimedCache(datetime.timedelta(seconds=10)) - - # Key doesn't exist - assert cache.get("nonexistent_key") is TimedCache.NOT_FOUND - - def test_clear(self): - """Test that the cache can be cleared.""" - cache = TimedCache(datetime.timedelta(seconds=10)) - - # Add some items - cache.set("key1", "value1") - cache.set("key2", "value2") - assert len(cache.cache) == 2 - - # Clear the cache - cache.clear() - assert len(cache.cache) == 0 - - def test_real_expiration(self): - """Test that values actually expire after the specified time.""" - # Use a very short expiration for the test - cache = TimedCache(datetime.timedelta(milliseconds=50)) - key, value = "test_key", "test_value" - - cache.set(key, value) - # Value should be available immediately - assert cache.get(key) == value - - # Wait for expiration - time.sleep(0.06) # 60 milliseconds, slightly longer than expiration - - # Value should now be expired - assert cache.get(key) is TimedCache.NOT_FOUND - - def test_overwrite_value(self): - """Test that setting a key that already exists overwrites the old value.""" - cache = TimedCache(datetime.timedelta(seconds=10)) - key = "test_key" - - # Set initial value - cache.set(key, "initial_value") - assert cache.get(key) == "initial_value" - - # Overwrite with new value - cache.set(key, "new_value") - assert cache.get(key) == "new_value" - - def test_extends_expiration_on_overwrite(self): - """Test that overwriting a key extends its expiration time.""" - cache = TimedCache(datetime.timedelta(seconds=10)) - key = "test_key" - - with patch("datetime.datetime") as mock_datetime: - # Set initial value at t=0 - initial_time = datetime.datetime(2023, 1, 1, tzinfo=datetime.timezone.utc) - mock_datetime.now.return_value = initial_time - cache.set(key, "initial_value") - - initial_expiration = cache.cache[key][1] - assert initial_expiration == initial_time + datetime.timedelta(seconds=10) - - # Overwrite at t=5 - later_time = initial_time + datetime.timedelta(seconds=5) - mock_datetime.now.return_value = later_time - cache.set(key, "new_value") - - # Expiration should be extended - new_expiration = cache.cache[key][1] - assert new_expiration == later_time + datetime.timedelta(seconds=10) - - def test_different_key_types(self): - """Test that different types of keys can be used.""" - cache = TimedCache(datetime.timedelta(seconds=10)) - - # Test various key types - keys_and_values = [ - (42, "int_value"), - (3.14, "float_value"), - ((1, 2), "tuple_value"), - (frozenset({1, 2, 3}), "frozenset_value"), - ] - - for key, value in keys_and_values: - cache.set(key, value) - assert cache.get(key) == value - - def test_none_value(self): - """Test that None can be stored as a value.""" - cache = TimedCache(datetime.timedelta(seconds=10)) - key = "none_key" - - cache.set(key, None) - # The stored value is None, but get() should return None, not NOT_FOUND - assert cache.get(key) is None - - def test_edge_case_zero_expiration(self): - """Test with a zero expiration time.""" - cache = TimedCache(datetime.timedelta(seconds=0)) - key, value = "test_key", "test_value" - - cache.set(key, value) - # The value might already be expired by the time we call get() - # We can't make strong assertions here due to timing variability - retrieved = cache.get(key) - assert retrieved in (value, TimedCache.NOT_FOUND) - - def test_negative_expiration(self): - """Test with a negative expiration time.""" - cache = TimedCache(datetime.timedelta(seconds=-1)) - key, value = "test_key", "test_value" - - cache.set(key, value) - # Value should be immediately expired - assert cache.get(key) is TimedCache.NOT_FOUND - - def test_cache_consistency(self): - """Test cache consistency with multiple operations.""" - cache = TimedCache(datetime.timedelta(seconds=10)) - - # Add multiple items - cache.set("key1", "value1") - cache.set("key2", "value2") - cache.set("key3", "value3") - - # Check all items - assert cache.get("key1") == "value1" - assert cache.get("key2") == "value2" - assert cache.get("key3") == "value3" - - # Overwrite one item - cache.set("key2", "updated_value") - - # Check again - assert cache.get("key1") == "value1" - assert cache.get("key2") == "updated_value" - assert cache.get("key3") == "value3" - - # Clear and verify all items are gone - cache.clear() - assert cache.get("key1") is TimedCache.NOT_FOUND - assert cache.get("key2") is TimedCache.NOT_FOUND - assert cache.get("key3") is TimedCache.NOT_FOUND - - def test_large_expiration(self): - """Test with a very large expiration time.""" - # One year expiration - cache = TimedCache(datetime.timedelta(days=365)) - key, value = "test_key", "test_value" - - cache.set(key, value) - assert cache.get(key) == value - - def test_many_items(self): - """Test cache with many items.""" - cache = TimedCache(datetime.timedelta(seconds=10)) - - # Add 1000 items - for i in range(1000): - cache.set(f"key{i}", f"value{i}") - - # Check size - assert len(cache.cache) == 1000 - - # Check some random items - for i in [0, 123, 456, 789, 999]: - assert cache.get(f"key{i}") == f"value{i}"