* Fix typos

* Document Cachable* -> Cacheable* rename as v4 breaking change

Adds the response-cache model rename to the change register, per
maintainer decision to skip compatibility aliases in favor of clear
documentation.

* Skip invalid Before import in doc test; use inline codespell ignore

The Cachable* -> Cacheable* breaking-change entry showed the old,
now-invalid import for contrast, which the doc-example test picked up
as a real import and flagged as a regression. Comment out the
deliberately-broken "Before" line (matching the McpError entry just
above it) so only the working "After" import is exercised.

Also swap the blanket codespell ignore-words-list entry for a
narrower inline `codespell:ignore` directive on the one line that
needs it, so codespell keeps flagging "cachable" everywhere else.

---------

Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
This commit is contained in:
Viktor Szépe 2026-07-19 03:18:39 +02:00 committed by GitHub
commit 81fada4922
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 61 additions and 43 deletions

View file

@ -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)

View file

@ -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) <!-- codespell:ignore -->
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.

View file

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 72 KiB

Before After
Before After

View file

@ -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.*

View file

@ -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)
</Update>
</Update>

View file

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 72 KiB

Before After
Before After

View file

@ -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.*

View file

@ -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,

View file

@ -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

View file

@ -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.
"""