diff --git a/docs/changelog.mdx b/docs/changelog.mdx index 9ee438d53..063bc4646 100644 --- a/docs/changelog.mdx +++ b/docs/changelog.mdx @@ -635,7 +635,7 @@ FastMCP 3.2 is the Apps release: your tools can now return interactive UIs — c * Add tag to docs by [@jlowin](https://github.com/jlowin) in [#3382](https://github.com/PrefectHQ/fastmcp/pull/3382) * Add settings and environment variables reference by [@jlowin](https://github.com/jlowin) in [#3384](https://github.com/PrefectHQ/fastmcp/pull/3384) * Add contributing guidelines and update issue/PR templates by [@jlowin](https://github.com/jlowin) in [#3485](https://github.com/PrefectHQ/fastmcp/pull/3485) -* [Documentation] Move stateless_http transport kwarg to http_app as FastMCP constructo… by [@mhallo](https://github.com/mhallo) in [#3510](https://github.com/PrefectHQ/fastmcp/pull/3510) +* [Documentation] Move stateless_http transport kwarg to http_app as FastMCP constructor… by [@mhallo](https://github.com/mhallo) in [#3510](https://github.com/PrefectHQ/fastmcp/pull/3510) * Update security policy by [@jlowin](https://github.com/jlowin) in [#3521](https://github.com/PrefectHQ/fastmcp/pull/3521) * Add release instructions to CLAUDE.md by [@jlowin](https://github.com/jlowin) in [#3583](https://github.com/PrefectHQ/fastmcp/pull/3583) * fix(docs): correct misleading stateless_http header by [@jlowin](https://github.com/jlowin) in [#3622](https://github.com/PrefectHQ/fastmcp/pull/3622) @@ -1967,7 +1967,7 @@ Thank you to our new contributors and everyone who tested preview builds. Your f * Add configurable redirect URI validation for OAuth providers by [@jlowin](https://github.com/jlowin) in [#1582](https://github.com/PrefectHQ/fastmcp/pull/1582) * Remove invalid-argument-type ignore and fix type errors by [@jlowin](https://github.com/jlowin) in [#1588](https://github.com/PrefectHQ/fastmcp/pull/1588) * Remove generate-schema from public CLI by [@jlowin](https://github.com/jlowin) in [#1591](https://github.com/PrefectHQ/fastmcp/pull/1591) -* Skip flaky windows test / mulit-client garbage collection by [@jlowin](https://github.com/jlowin) in [#1592](https://github.com/PrefectHQ/fastmcp/pull/1592) +* Skip flaky windows test / multi-client garbage collection by [@jlowin](https://github.com/jlowin) in [#1592](https://github.com/PrefectHQ/fastmcp/pull/1592) * Add setting to disable logging configuration by [@isra17](https://github.com/isra17) in [#1575](https://github.com/PrefectHQ/fastmcp/pull/1575) * Improve debug logging for nested Servers / Clients by [@strawgate](https://github.com/strawgate) in [#1604](https://github.com/PrefectHQ/fastmcp/pull/1604) * Add GitHub pull request template by [@strawgate](https://github.com/strawgate) in [#1581](https://github.com/PrefectHQ/fastmcp/pull/1581) diff --git a/docs/development/v4-notes/change-register.mdx b/docs/development/v4-notes/change-register.mdx index 2a9aa3bd7..4798f7670 100644 --- a/docs/development/v4-notes/change-register.mdx +++ b/docs/development/v4-notes/change-register.mdx @@ -164,6 +164,22 @@ Resource-not-found responses from the core `resources/read` handler previously u *Verify:* `fastmcp_slim/fastmcp/exceptions.py` (`to_mcp_error`); `fastmcp_slim/fastmcp/server/mixins/mcp_operations.py`; `tests/test_exceptions.py`. +### `Cachable*` response-cache models renamed to `Cacheable*` — Breaking (rename) + +The response-caching middleware's Pydantic wrapper models — used to serialize cached tool, resource, and prompt results for `ResponseCachingMiddleware` — carried a spelling typo. `CachableToolResult`, `CachableResourceContent`, `CachableResourceResult`, `CachableMessage`, and `CachablePromptResult` are renamed to `CacheableToolResult`, `CacheableResourceContent`, `CacheableResourceResult`, `CacheableMessage`, and `CacheablePromptResult`. None of these classes are re-exported from `fastmcp` or any package `__init__.py`, so the realistic blast radius is limited to code that imported the old names directly from `fastmcp.server.middleware.caching`: + +```python +# Before (now raises ImportError): +# from fastmcp.server.middleware.caching import CachableToolResult + +# After +from fastmcp.server.middleware.caching import CacheableToolResult +``` + +There is deliberately no compatibility alias for the old spelling. + +*Verify:* `fastmcp_slim/fastmcp/server/middleware/caching.py`. + ## Client The `fastmcp.Client` public API is preserved exactly. The client stays a wrapper around `mcp.ClientSession` in legacy/handshake mode; the first-class `mcp.client.Client` is deliberately not adopted in this PR. diff --git a/docs/integrations/images/permit/role_assignement.png b/docs/integrations/images/permit/role_assignment.png similarity index 100% rename from docs/integrations/images/permit/role_assignement.png rename to docs/integrations/images/permit/role_assignment.png diff --git a/docs/integrations/permit.mdx b/docs/integrations/permit.mdx index 066f5b1ea..ddda7cd2c 100644 --- a/docs/integrations/permit.mdx +++ b/docs/integrations/permit.mdx @@ -31,7 +31,7 @@ The middleware automatically maps MCP methods to Permit.io resources and actions > **Note:** > Don't forget to assign the relevant role (e.g., Admin, User) to the user authenticating to your MCP server (such as the user in the JWT) in the Permit.io Directory. Without the correct role assignment, users will not have access to the resources and actions you've configured in your policies. > -> ![Permit.io Directory Role Assignment Example](./images/permit/role_assignement.png) +> ![Permit.io Directory Role Assignment Example](./images/permit/role_assignment.png) > > *Example: In Permit.io Directory, both 'client' and 'admin' users are assigned the 'Admin' role, granting them the permissions defined in your policy mapping.* diff --git a/docs/v2/changelog.mdx b/docs/v2/changelog.mdx index 6e9979277..e167f016b 100644 --- a/docs/v2/changelog.mdx +++ b/docs/v2/changelog.mdx @@ -550,7 +550,7 @@ Thank you to our new contributors and everyone who tested preview builds. Your f * Add configurable redirect URI validation for OAuth providers by [@jlowin](https://github.com/jlowin) in [#1582](https://github.com/PrefectHQ/fastmcp/pull/1582) * Remove invalid-argument-type ignore and fix type errors by [@jlowin](https://github.com/jlowin) in [#1588](https://github.com/PrefectHQ/fastmcp/pull/1588) * Remove generate-schema from public CLI by [@jlowin](https://github.com/jlowin) in [#1591](https://github.com/PrefectHQ/fastmcp/pull/1591) -* Skip flaky windows test / mulit-client garbage collection by [@jlowin](https://github.com/jlowin) in [#1592](https://github.com/PrefectHQ/fastmcp/pull/1592) +* Skip flaky windows test / multi-client garbage collection by [@jlowin](https://github.com/jlowin) in [#1592](https://github.com/PrefectHQ/fastmcp/pull/1592) * Add setting to disable logging configuration by [@isra17](https://github.com/isra17) in [#1575](https://github.com/PrefectHQ/fastmcp/pull/1575) * Improve debug logging for nested Servers / Clients by [@strawgate](https://github.com/strawgate) in [#1604](https://github.com/PrefectHQ/fastmcp/pull/1604) * Add GitHub pull request template by [@strawgate](https://github.com/strawgate) in [#1581](https://github.com/PrefectHQ/fastmcp/pull/1581) @@ -2339,4 +2339,4 @@ This release is highlighted by the ability to handle complex JSON objects as MCP The very first release of FastMCP! 🎉 **Full Changelog**: [Initial commits](https://github.com/PrefectHQ/fastmcp/commits/v0.1.0) - \ No newline at end of file + diff --git a/docs/v2/integrations/images/permit/role_assignement.png b/docs/v2/integrations/images/permit/role_assignment.png similarity index 100% rename from docs/v2/integrations/images/permit/role_assignement.png rename to docs/v2/integrations/images/permit/role_assignment.png diff --git a/docs/v2/integrations/permit.mdx b/docs/v2/integrations/permit.mdx index 066f5b1ea..ddda7cd2c 100644 --- a/docs/v2/integrations/permit.mdx +++ b/docs/v2/integrations/permit.mdx @@ -31,7 +31,7 @@ The middleware automatically maps MCP methods to Permit.io resources and actions > **Note:** > Don't forget to assign the relevant role (e.g., Admin, User) to the user authenticating to your MCP server (such as the user in the JWT) in the Permit.io Directory. Without the correct role assignment, users will not have access to the resources and actions you've configured in your policies. > -> ![Permit.io Directory Role Assignment Example](./images/permit/role_assignement.png) +> ![Permit.io Directory Role Assignment Example](./images/permit/role_assignment.png) > > *Example: In Permit.io Directory, both 'client' and 'admin' users are assigned the 'Admin' role, granting them the permissions defined in your policy mapping.* diff --git a/fastmcp_slim/fastmcp/server/middleware/caching.py b/fastmcp_slim/fastmcp/server/middleware/caching.py index 94c440e2c..d30f4b814 100644 --- a/fastmcp_slim/fastmcp/server/middleware/caching.py +++ b/fastmcp_slim/fastmcp/server/middleware/caching.py @@ -49,7 +49,7 @@ def _to_base_model(value: FastMCPBaseModel, model_type: type[BaseModelT]) -> Bas return model_type.model_validate(field_values) -class CachableResourceContent(FastMCPBaseModel): +class CacheableResourceContent(FastMCPBaseModel): """A wrapper for ResourceContent that can be cached.""" content: str | bytes @@ -57,10 +57,10 @@ class CachableResourceContent(FastMCPBaseModel): meta: dict[str, Any] | None = None -class CachableResourceResult(FastMCPBaseModel): +class CacheableResourceResult(FastMCPBaseModel): """A wrapper for ResourceResult that can be cached.""" - contents: list[CachableResourceContent] + contents: list[CacheableResourceContent] meta: dict[str, Any] | None = None def get_size(self) -> int: @@ -70,7 +70,7 @@ class CachableResourceResult(FastMCPBaseModel): def wrap(cls, value: ResourceResult) -> Self: return cls( contents=[ - CachableResourceContent( + CacheableResourceContent( content=item.content, mime_type=item.mime_type, meta=item.meta ) for item in value.contents @@ -90,7 +90,7 @@ class CachableResourceResult(FastMCPBaseModel): ) -class CachableToolResult(FastMCPBaseModel): +class CacheableToolResult(FastMCPBaseModel): content: list[mcp_types.ContentBlock] structured_content: dict[str, Any] | None meta: dict[str, Any] | None @@ -114,7 +114,7 @@ class CachableToolResult(FastMCPBaseModel): ) -class CachableMessage(FastMCPBaseModel): +class CacheableMessage(FastMCPBaseModel): """A wrapper for Message that can be cached.""" role: str @@ -126,10 +126,10 @@ class CachableMessage(FastMCPBaseModel): ) -class CachablePromptResult(FastMCPBaseModel): +class CacheablePromptResult(FastMCPBaseModel): """A wrapper for PromptResult that can be cached.""" - messages: list[CachableMessage] + messages: list[CacheableMessage] description: str | None = None meta: dict[str, Any] | None = None @@ -140,7 +140,7 @@ class CachablePromptResult(FastMCPBaseModel): def wrap(cls, value: PromptResult) -> Self: return cls( messages=[ - CachableMessage(role=m.role, content=m.content) for m in value.messages + CacheableMessage(role=m.role, content=m.content) for m in value.messages ], description=value.description, meta=value.meta, @@ -287,23 +287,25 @@ class ResponseCachingMiddleware(Middleware): default_collection="prompts/list", ) - self._read_resource_cache: PydanticAdapter[CachableResourceResult] = ( + self._read_resource_cache: PydanticAdapter[CacheableResourceResult] = ( PydanticAdapter( key_value=self._stats, - pydantic_model=CachableResourceResult, + pydantic_model=CacheableResourceResult, default_collection="resources/read", ) ) - self._get_prompt_cache: PydanticAdapter[CachablePromptResult] = PydanticAdapter( - key_value=self._stats, - pydantic_model=CachablePromptResult, - default_collection="prompts/get", + self._get_prompt_cache: PydanticAdapter[CacheablePromptResult] = ( + PydanticAdapter( + key_value=self._stats, + pydantic_model=CacheablePromptResult, + default_collection="prompts/get", + ) ) - self._call_tool_cache: PydanticAdapter[CachableToolResult] = PydanticAdapter( + self._call_tool_cache: PydanticAdapter[CacheableToolResult] = PydanticAdapter( key_value=self._stats, - pydantic_model=CachableToolResult, + pydantic_model=CacheableToolResult, default_collection="tools/call", ) @@ -326,15 +328,15 @@ class ResponseCachingMiddleware(Middleware): tools: Sequence[Tool] = await call_next(context) # Turn any subclass of Tool into a Tool - cachable_tools = [_to_base_model(tool, Tool) for tool in tools] + cacheable_tools = [_to_base_model(tool, Tool) for tool in tools] await self._list_tools_cache.put( key=cache_key, - value=cachable_tools, + value=cacheable_tools, ttl=self._list_tools_settings.get("ttl", FIVE_MINUTES_IN_SECONDS), ) - return cachable_tools + return cacheable_tools @override async def on_list_resources( @@ -355,17 +357,17 @@ class ResponseCachingMiddleware(Middleware): resources: Sequence[Resource] = await call_next(context) # Turn any subclass of Resource into a Resource - cachable_resources = [ + cacheable_resources = [ _to_base_model(resource, Resource) for resource in resources ] await self._list_resources_cache.put( key=cache_key, - value=cachable_resources, + value=cacheable_resources, ttl=self._list_resources_settings.get("ttl", FIVE_MINUTES_IN_SECONDS), ) - return cachable_resources + return cacheable_resources @override async def on_list_prompts( @@ -386,15 +388,15 @@ class ResponseCachingMiddleware(Middleware): prompts: Sequence[Prompt] = await call_next(context) # Turn any subclass of Prompt into a Prompt - cachable_prompts = [_to_base_model(prompt, Prompt) for prompt in prompts] + cacheable_prompts = [_to_base_model(prompt, Prompt) for prompt in prompts] await self._list_prompts_cache.put( key=cache_key, - value=cachable_prompts, + value=cacheable_prompts, ttl=self._list_prompts_settings.get("ttl", FIVE_MINUTES_IN_SECONDS), ) - return cachable_prompts + return cacheable_prompts @override async def on_call_tool( @@ -419,17 +421,17 @@ class ResponseCachingMiddleware(Middleware): return cached_value.unwrap() tool_result: ToolResult = await call_next(context) - cachable_tool_result: CachableToolResult = CachableToolResult.wrap( + cacheable_tool_result: CacheableToolResult = CacheableToolResult.wrap( value=tool_result ) await self._call_tool_cache.put( key=cache_key, - value=cachable_tool_result, + value=cacheable_tool_result, ttl=self._call_tool_settings.get("ttl", ONE_HOUR_IN_SECONDS), ) - return cachable_tool_result.unwrap() + return cacheable_tool_result.unwrap() @override async def on_read_resource( @@ -445,13 +447,13 @@ class ResponseCachingMiddleware(Middleware): cache_key: str = _make_read_resource_cache_key( msg=context.message, auth_key=_get_auth_partition_key() ) - cached_value: CachableResourceResult | None + cached_value: CacheableResourceResult | None if cached_value := await self._read_resource_cache.get(key=cache_key): return cached_value.unwrap() value: ResourceResult = await call_next(context) - cached_value = CachableResourceResult.wrap(value) + cached_value = CacheableResourceResult.wrap(value) await self._read_resource_cache.put( key=cache_key, @@ -480,7 +482,7 @@ class ResponseCachingMiddleware(Middleware): return cached_value.unwrap() value: PromptResult = await call_next(context) - cached_value = CachablePromptResult.wrap(value) + cached_value = CacheablePromptResult.wrap(value) await self._get_prompt_cache.put( key=cache_key, diff --git a/tests/server/middleware/test_caching.py b/tests/server/middleware/test_caching.py index 88909836e..c5e3f385a 100644 --- a/tests/server/middleware/test_caching.py +++ b/tests/server/middleware/test_caching.py @@ -32,7 +32,7 @@ from fastmcp.prompts.function_prompt import FunctionPrompt from fastmcp.resources.base import Resource from fastmcp.server.middleware.caching import ( ANONYMOUS_AUTH_KEY, - CachableToolResult, + CacheableToolResult, CallToolSettings, ResponseCachingMiddleware, ResponseCachingStatistics, @@ -571,7 +571,7 @@ class TestResponseCachingMiddlewareIntegration: ) -class TestCachableToolResult: +class TestCacheableToolResult: def test_wrap_and_unwrap(self): tool_result = ToolResult( "unstructured content", @@ -579,7 +579,7 @@ class TestCachableToolResult: meta={"meta": "data"}, ) - cached_tool_result = CachableToolResult.wrap(tool_result).unwrap() + cached_tool_result = CacheableToolResult.wrap(tool_result).unwrap() assert cached_tool_result.content == tool_result.content assert cached_tool_result.structured_content == tool_result.structured_content @@ -588,7 +588,7 @@ class TestCachableToolResult: def test_wrap_and_unwrap_preserves_is_error(self): tool_result = ToolResult("boom", is_error=True) - cached_tool_result = CachableToolResult.wrap(tool_result).unwrap() + cached_tool_result = CacheableToolResult.wrap(tool_result).unwrap() assert cached_tool_result.is_error is True diff --git a/tests/utilities/test_tests.py b/tests/utilities/test_tests.py index 6913d1899..d5fc24c21 100644 --- a/tests/utilities/test_tests.py +++ b/tests/utilities/test_tests.py @@ -48,7 +48,7 @@ class TestHeadlessOAuthCallbackHandler: The OAuth callback handler in HeadlessOAuth parses the redirect Location header. parse_qs without keep_blank_values=True silently drops keys whose - value is empty (e.g. `?state=`), which mis-models real OAuth callbacks + value is empty (e.g. `?state=`), which misrepresents real OAuth callbacks where an empty `state` is distinct from a missing one. """