From eac1cea6a4b1dc9167efdea7df7c472b4de281ad Mon Sep 17 00:00:00 2001
From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
Date: Wed, 8 Oct 2025 19:57:14 -0400
Subject: [PATCH 1/5] Improve docs on prefix rules (#2028)
---
docs/servers/composition.mdx | 2 ++
docs/servers/proxy.mdx | 25 +++++++++++++++++++++----
2 files changed, 23 insertions(+), 4 deletions(-)
diff --git a/docs/servers/composition.mdx b/docs/servers/composition.mdx
index 370f072de..63ac1f773 100644
--- a/docs/servers/composition.mdx
+++ b/docs/servers/composition.mdx
@@ -41,6 +41,8 @@ FastMCP supports [MCP proxying](/servers/proxy), which allows you to mirror a lo
You can also create proxies from configuration dictionaries that follow the MCPConfig schema, which is useful for quickly connecting to one or more remote servers. See the [Proxy Servers documentation](/servers/proxy#configuration-based-proxies) for details on configuration-based proxying. Note that MCPConfig follows an emerging standard and its format may evolve over time.
+Prefixing rules for tools, prompts, resources, and templates are identical across importing, mounting, and proxies.
+
## Importing (Static Composition)
The `import_server()` method copies all components (tools, resources, templates, prompts) from one `FastMCP` instance (the *subserver*) into another (the *main server*). An optional `prefix` can be provided to avoid naming conflicts. If no prefix is provided, components are imported without modification. When multiple servers are imported with the same prefix (or no prefix), the most recently imported server's components take precedence.
diff --git a/docs/servers/proxy.mdx b/docs/servers/proxy.mdx
index 82daccb5d..7cbb6d90c 100644
--- a/docs/servers/proxy.mdx
+++ b/docs/servers/proxy.mdx
@@ -245,11 +245,29 @@ config = {
# Create a unified proxy to multiple servers
composite_proxy = FastMCP.as_proxy(config, name="Composite Proxy")
-# Tools and resources are accessible with prefixes:
-# - weather_get_forecast, calendar_add_event
-# - weather://weather/icons/sunny, calendar://calendar/events/today
+# Tools, resources, prompts, and templates are accessible with prefixes:
+# - Tools: weather_get_forecast, calendar_add_event
+# - Prompts: weather_daily_summary, calendar_quick_add
+# - Resources: weather://weather/icons/sunny, calendar://calendar/events/today
+# - Templates: weather://weather/locations/{id}, calendar://calendar/events/{date}
```
+## Component Prefixing
+
+When proxying one or more servers, component names are prefixed the same way as with mounting and importing:
+
+- Tools: `{prefix}_{tool_name}`
+- Prompts: `{prefix}_{prompt_name}`
+- Resources: `protocol://{prefix}/path/to/resource` (default path format)
+- Resource templates: `protocol://{prefix}/...` and template names are also prefixed
+
+These rules apply uniformly whether you:
+- Mount a proxy on another server
+- Create a multi-server proxy from an `MCPConfig`
+- Use `FastMCP.as_proxy()` directly
+
+For resource URI prefix formats (path vs legacy protocol style) and configuration options, see Server Composition → Resource Prefix Formats.
+
## Mirrored Components
@@ -332,4 +350,3 @@ def custom_client_factory():
proxy = FastMCPProxy(client_factory=custom_client_factory)
```
-
From b1458c608208a520d16ade4234c624173a98db06 Mon Sep 17 00:00:00 2001
From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
Date: Wed, 8 Oct 2025 19:57:30 -0400
Subject: [PATCH 2/5] Default to FastMCP version when server version not
specified (#2022)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: William Easton
---
src/fastmcp/server/server.py | 2 +-
tests/client/test_client.py | 8 +++-----
tests/utilities/test_inspect.py | 4 ++--
3 files changed, 6 insertions(+), 8 deletions(-)
diff --git a/src/fastmcp/server/server.py b/src/fastmcp/server/server.py
index 887d6471b..91367360c 100644
--- a/src/fastmcp/server/server.py
+++ b/src/fastmcp/server/server.py
@@ -197,7 +197,7 @@ class FastMCP(Generic[LifespanResultT]):
self._mcp_server = LowLevelServer[LifespanResultT](
fastmcp=self,
name=name or self.generate_name(),
- version=version,
+ version=version or fastmcp.__version__,
instructions=instructions,
lifespan=_lifespan_wrapper(self, lifespan),
)
diff --git a/tests/client/test_client.py b/tests/client/test_client.py
index b521a8cdc..9a41c7600 100644
--- a/tests/client/test_client.py
+++ b/tests/client/test_client.py
@@ -9,6 +9,7 @@ from mcp import McpError
from mcp.client.auth import OAuthClientProvider
from pydantic import AnyUrl
+import fastmcp
from fastmcp.client import Client
from fastmcp.client.auth.bearer import BearerAuth
from fastmcp.client.transports import (
@@ -435,11 +436,8 @@ async def test_server_info_custom_version():
async with client:
result = client.initialize_result
assert result.serverInfo.name == "DefaultVersionServer"
- # Should fall back to MCP library version
- assert result.serverInfo.version is not None
- assert (
- result.serverInfo.version != "1.2.3"
- ) # Should be different from custom version
+ # Should fall back to FastMCP version
+ assert result.serverInfo.version == fastmcp.__version__
async def test_client_nested_context_manager(fastmcp_server):
diff --git a/tests/utilities/test_inspect.py b/tests/utilities/test_inspect.py
index e75439ea9..420335c82 100644
--- a/tests/utilities/test_inspect.py
+++ b/tests/utilities/test_inspect.py
@@ -90,7 +90,7 @@ class TestGetFastMCPInfo:
assert info.fastmcp_version == fastmcp.__version__
assert info.mcp_version == importlib.metadata.version("mcp")
assert info.server_generation == 2 # v2 server
- assert info.version is None
+ assert info.version == fastmcp.__version__
assert info.tools == []
assert info.prompts == []
assert info.resources == []
@@ -405,7 +405,7 @@ class TestFastMCP1xCompatibility:
assert info1x.server_generation == 1 # v1
assert info2x.server_generation == 2 # v2
assert info1x.version is None
- assert info2x.version is None
+ assert info2x.version == fastmcp.__version__
# No templates added in these tests
assert len(info1x.templates) == 0
From 9cb47e12ddc62ed1d7962426e12e5fa3e75caba9 Mon Sep 17 00:00:00 2001
From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
Date: Wed, 8 Oct 2025 20:10:12 -0400
Subject: [PATCH 3/5] Add version badge to on_initialize docs (#2029)
---
docs/servers/middleware.mdx | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/docs/servers/middleware.mdx b/docs/servers/middleware.mdx
index a57882016..66ebe638c 100644
--- a/docs/servers/middleware.mdx
+++ b/docs/servers/middleware.mdx
@@ -89,11 +89,12 @@ Note that the MCP SDK may perform additional operations like listing tools for c
This hierarchy allows you to target your middleware logic with the right level of specificity. Use `on_message` for broad concerns like logging, `on_request` for authentication, and `on_call_tool` for tool-specific logic like performance monitoring.
### Available Hooks
+
- `on_message`: Called for all MCP messages (requests and notifications)
- `on_request`: Called specifically for MCP requests (that expect responses)
- `on_notification`: Called specifically for MCP notifications (fire-and-forget)
-- `on_initialize`: Called when a client connects and initializes the session (returns `None`)
+
- `on_call_tool`: Called when tools are being executed
- `on_read_resource`: Called when resources are being read
- `on_get_prompt`: Called when prompts are being retrieved
@@ -101,7 +102,8 @@ This hierarchy allows you to target your middleware logic with the right level o
- `on_list_resources`: Called when listing available resources
- `on_list_resource_templates`: Called when listing resource templates
- `on_list_prompts`: Called when listing available prompts
-
+
+- `on_initialize`: Called when a client connects and initializes the session (returns `None`)
The `on_initialize` hook receives the client's initialization request but **returns `None`** rather than a result. The initialization response is handled internally by the MCP protocol and cannot be modified by middleware. This hook is useful for client detection, logging connections, or initializing session state, but not for modifying the initialization handshake itself.
From a8d2a667d61d741969a052219af955f1c56cb248 Mon Sep 17 00:00:00 2001
From: Adam Azzam <33043305+aaazzam@users.noreply.github.com>
Date: Wed, 8 Oct 2025 20:27:55 -0400
Subject: [PATCH 4/5] Add supabase auth (#1997)
---
src/fastmcp/server/auth/providers/supabase.py | 171 ++++++++++++++++++
tests/server/auth/providers/test_supabase.py | 165 +++++++++++++++++
2 files changed, 336 insertions(+)
create mode 100644 src/fastmcp/server/auth/providers/supabase.py
create mode 100644 tests/server/auth/providers/test_supabase.py
diff --git a/src/fastmcp/server/auth/providers/supabase.py b/src/fastmcp/server/auth/providers/supabase.py
new file mode 100644
index 000000000..5be7482dc
--- /dev/null
+++ b/src/fastmcp/server/auth/providers/supabase.py
@@ -0,0 +1,171 @@
+"""Supabase authentication provider for FastMCP.
+
+This module provides SupabaseProvider - a complete authentication solution that integrates
+with Supabase Auth's JWT verification, supporting Dynamic Client Registration (DCR)
+for seamless MCP client authentication.
+"""
+
+from __future__ import annotations
+
+import httpx
+from pydantic import AnyHttpUrl, field_validator
+from pydantic_settings import BaseSettings, SettingsConfigDict
+from starlette.responses import JSONResponse
+from starlette.routing import Route
+
+from fastmcp.server.auth import RemoteAuthProvider, TokenVerifier
+from fastmcp.server.auth.providers.jwt import JWTVerifier
+from fastmcp.utilities.auth import parse_scopes
+from fastmcp.utilities.logging import get_logger
+from fastmcp.utilities.types import NotSet, NotSetT
+
+logger = get_logger(__name__)
+
+
+class SupabaseProviderSettings(BaseSettings):
+ model_config = SettingsConfigDict(
+ env_prefix="FASTMCP_SERVER_AUTH_SUPABASE_",
+ env_file=".env",
+ extra="ignore",
+ )
+
+ project_url: AnyHttpUrl
+ base_url: AnyHttpUrl
+ required_scopes: list[str] | None = None
+
+ @field_validator("required_scopes", mode="before")
+ @classmethod
+ def _parse_scopes(cls, v):
+ return parse_scopes(v)
+
+
+class SupabaseProvider(RemoteAuthProvider):
+ """Supabase metadata provider for DCR (Dynamic Client Registration).
+
+ This provider implements Supabase Auth integration using metadata forwarding.
+ This approach allows Supabase to handle the OAuth flow directly while FastMCP acts
+ as a resource server, verifying JWTs issued by Supabase Auth.
+
+ IMPORTANT SETUP REQUIREMENTS:
+
+ 1. Supabase Project Setup:
+ - Create a Supabase project at https://supabase.com
+ - Note your project URL (e.g., "https://abc123.supabase.co")
+ - For projects created after May 1st, 2025, asymmetric RS256 keys are used by default
+ - For older projects, consider migrating to asymmetric keys for better security
+
+ 2. JWT Verification:
+ - FastMCP verifies JWTs using the JWKS endpoint at {project_url}/auth/v1/.well-known/jwks.json
+ - JWTs are issued by {project_url}/auth/v1
+ - Tokens are cached for up to 10 minutes by Supabase's edge servers
+
+ For detailed setup instructions, see:
+ https://supabase.com/docs/guides/auth/jwts
+
+ Example:
+ ```python
+ from fastmcp.server.auth.providers.supabase import SupabaseProvider
+
+ # Create Supabase metadata provider (JWT verifier created automatically)
+ supabase_auth = SupabaseProvider(
+ project_url="https://abc123.supabase.co",
+ base_url="https://your-fastmcp-server.com",
+ )
+
+ # Use with FastMCP
+ mcp = FastMCP("My App", auth=supabase_auth)
+ ```
+ """
+
+ def __init__(
+ self,
+ *,
+ project_url: AnyHttpUrl | str | NotSetT = NotSet,
+ base_url: AnyHttpUrl | str | NotSetT = NotSet,
+ required_scopes: list[str] | None | NotSetT = NotSet,
+ token_verifier: TokenVerifier | None = None,
+ ):
+ """Initialize Supabase metadata provider.
+
+ Args:
+ project_url: Your Supabase project URL (e.g., "https://abc123.supabase.co")
+ base_url: Public URL of this FastMCP server
+ required_scopes: Optional list of scopes to require for all requests
+ token_verifier: Optional token verifier. If None, creates JWT verifier for Supabase
+ """
+ settings = SupabaseProviderSettings.model_validate(
+ {
+ k: v
+ for k, v in {
+ "project_url": project_url,
+ "base_url": base_url,
+ "required_scopes": required_scopes,
+ }.items()
+ if v is not NotSet
+ }
+ )
+
+ self.project_url = str(settings.project_url).rstrip("/")
+ self.base_url = str(settings.base_url).rstrip("/")
+
+ # Create default JWT verifier if none provided
+ if token_verifier is None:
+ token_verifier = JWTVerifier(
+ jwks_uri=f"{self.project_url}/auth/v1/.well-known/jwks.json",
+ issuer=f"{self.project_url}/auth/v1",
+ algorithm="ES256", # Supabase uses ES256 for asymmetric keys
+ required_scopes=settings.required_scopes,
+ )
+
+ # Initialize RemoteAuthProvider with Supabase as the authorization server
+ super().__init__(
+ token_verifier=token_verifier,
+ authorization_servers=[AnyHttpUrl(f"{self.project_url}/auth/v1")],
+ base_url=self.base_url,
+ )
+
+ def get_routes(
+ self,
+ mcp_path: str | None = None,
+ ) -> list[Route]:
+ """Get OAuth routes including Supabase authorization server metadata forwarding.
+
+ This returns the standard protected resource routes plus an authorization server
+ metadata endpoint that forwards Supabase's OAuth metadata to clients.
+
+ Args:
+ mcp_path: The path where the MCP endpoint is mounted (e.g., "/mcp")
+ This is used to advertise the resource URL in metadata.
+ """
+ # Get the standard protected resource routes from RemoteAuthProvider
+ routes = super().get_routes(mcp_path)
+
+ async def oauth_authorization_server_metadata(request):
+ """Forward Supabase OAuth authorization server metadata with FastMCP customizations."""
+ try:
+ async with httpx.AsyncClient() as client:
+ response = await client.get(
+ f"{self.project_url}/auth/v1/.well-known/oauth-authorization-server"
+ )
+ response.raise_for_status()
+ metadata = response.json()
+ return JSONResponse(metadata)
+ except Exception as e:
+ return JSONResponse(
+ {
+ "error": "server_error",
+ "error_description": f"Failed to fetch Supabase metadata: {e}",
+ },
+ status_code=500,
+ )
+
+ # Add Supabase authorization server metadata forwarding
+ routes.append(
+ Route(
+ "/.well-known/oauth-authorization-server",
+ endpoint=oauth_authorization_server_metadata,
+ methods=["GET"],
+ )
+ )
+
+ return routes
diff --git a/tests/server/auth/providers/test_supabase.py b/tests/server/auth/providers/test_supabase.py
new file mode 100644
index 000000000..973f29679
--- /dev/null
+++ b/tests/server/auth/providers/test_supabase.py
@@ -0,0 +1,165 @@
+"""Tests for Supabase Auth provider."""
+
+import os
+from collections.abc import Generator
+from unittest.mock import patch
+
+import httpx
+import pytest
+
+from fastmcp import Client, FastMCP
+from fastmcp.client.transports import StreamableHttpTransport
+from fastmcp.server.auth.providers.supabase import SupabaseProvider
+from fastmcp.utilities.tests import HeadlessOAuth, run_server_in_process
+
+
+class TestSupabaseProvider:
+ """Test Supabase Auth provider functionality."""
+
+ def test_init_with_explicit_params(self):
+ """Test SupabaseProvider initialization with explicit parameters."""
+ provider = SupabaseProvider(
+ project_url="https://abc123.supabase.co",
+ base_url="https://myserver.com",
+ )
+
+ assert provider.project_url == "https://abc123.supabase.co"
+ assert str(provider.base_url) == "https://myserver.com/"
+
+ @pytest.mark.parametrize(
+ "scopes_env",
+ [
+ "openid,email",
+ '["openid", "email"]',
+ ],
+ )
+ def test_init_with_env_vars(self, scopes_env):
+ """Test SupabaseProvider initialization from environment variables."""
+ with patch.dict(
+ os.environ,
+ {
+ "FASTMCP_SERVER_AUTH_SUPABASE_PROJECT_URL": "https://env123.supabase.co",
+ "FASTMCP_SERVER_AUTH_SUPABASE_BASE_URL": "https://envserver.com",
+ },
+ ):
+ provider = SupabaseProvider()
+
+ assert provider.project_url == "https://env123.supabase.co"
+ assert str(provider.base_url) == "https://envserver.com/"
+
+ def test_environment_variable_loading(self):
+ """Test that environment variables are loaded correctly."""
+ provider = SupabaseProvider(
+ project_url="https://env123.supabase.co",
+ base_url="http://env-server.com",
+ )
+
+ assert provider.project_url == "https://env123.supabase.co"
+ assert str(provider.base_url) == "http://env-server.com/"
+
+ def test_project_url_normalization(self):
+ """Test that project_url handles trailing slashes correctly."""
+ # Without trailing slash
+ provider1 = SupabaseProvider(
+ project_url="https://abc123.supabase.co",
+ base_url="https://myserver.com",
+ )
+ assert provider1.project_url == "https://abc123.supabase.co"
+
+ # With trailing slash - should be stripped
+ provider2 = SupabaseProvider(
+ project_url="https://abc123.supabase.co/",
+ base_url="https://myserver.com",
+ )
+ assert provider2.project_url == "https://abc123.supabase.co"
+
+ def test_jwt_verifier_configured_correctly(self):
+ """Test that JWT verifier is configured correctly."""
+ provider = SupabaseProvider(
+ project_url="https://abc123.supabase.co",
+ base_url="https://myserver.com",
+ )
+
+ # Check that JWT verifier uses the correct endpoints
+ assert (
+ provider.token_verifier.jwks_uri # type: ignore[attr-defined]
+ == "https://abc123.supabase.co/auth/v1/.well-known/jwks.json"
+ )
+ assert (
+ provider.token_verifier.issuer == "https://abc123.supabase.co/auth/v1" # type: ignore[attr-defined]
+ )
+ assert provider.token_verifier.algorithm == "ES256" # type: ignore[attr-defined]
+
+ def test_jwt_verifier_with_required_scopes(self):
+ """Test that JWT verifier respects required_scopes."""
+ provider = SupabaseProvider(
+ project_url="https://abc123.supabase.co",
+ base_url="https://myserver.com",
+ required_scopes=["openid", "email"],
+ )
+
+ assert provider.token_verifier.required_scopes == ["openid", "email"] # type: ignore[attr-defined]
+
+ def test_authorization_servers_configured(self):
+ """Test that authorization servers list is configured correctly."""
+ provider = SupabaseProvider(
+ project_url="https://abc123.supabase.co",
+ base_url="https://myserver.com",
+ )
+
+ assert len(provider.authorization_servers) == 1
+ assert (
+ str(provider.authorization_servers[0])
+ == "https://abc123.supabase.co/auth/v1"
+ )
+
+
+def run_mcp_server(host: str, port: int) -> None:
+ mcp = FastMCP(
+ auth=SupabaseProvider(
+ project_url="https://test123.supabase.co",
+ base_url="http://localhost:4321",
+ )
+ )
+
+ @mcp.tool
+ def add(a: int, b: int) -> int:
+ return a + b
+
+ mcp.run(host=host, port=port, transport="http")
+
+
+@pytest.fixture
+def mcp_server_url() -> Generator[str]:
+ with run_server_in_process(run_mcp_server) as url:
+ yield f"{url}/mcp"
+
+
+@pytest.fixture()
+def client_with_headless_oauth(
+ mcp_server_url: str,
+) -> Generator[Client, None, None]:
+ """Client with headless OAuth that bypasses browser interaction."""
+ client = Client(
+ transport=StreamableHttpTransport(mcp_server_url),
+ auth=HeadlessOAuth(mcp_url=mcp_server_url),
+ )
+ yield client
+
+
+class TestSupabaseProviderIntegration:
+ async def test_unauthorized_access(self, mcp_server_url: str):
+ with pytest.raises(httpx.HTTPStatusError) as exc_info:
+ async with Client(mcp_server_url) as client:
+ tools = await client.list_tools() # noqa: F841
+
+ assert isinstance(exc_info.value, httpx.HTTPStatusError)
+ assert exc_info.value.response.status_code == 401
+ assert "tools" not in locals()
+
+ # async def test_authorized_access(self, client_with_headless_oauth: Client):
+ # async with client_with_headless_oauth:
+ # tools = await client_with_headless_oauth.list_tools()
+ # assert tools is not None
+ # assert len(tools) > 0
+ # assert "add" in tools
From 20c6749de973222841a69a6b974468a59159eeca Mon Sep 17 00:00:00 2001
From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
Date: Wed, 8 Oct 2025 21:06:41 -0400
Subject: [PATCH 5/5] Allow direct instantiation of Prompt and Resource classes
(#2031)
---
src/fastmcp/prompts/prompt.py | 12 +++++++-----
src/fastmcp/resources/resource.py | 12 +++++++-----
tests/resources/test_resources.py | 9 +++++----
3 files changed, 19 insertions(+), 14 deletions(-)
diff --git a/src/fastmcp/prompts/prompt.py b/src/fastmcp/prompts/prompt.py
index 91c5cf31f..2785d04df 100644
--- a/src/fastmcp/prompts/prompt.py
+++ b/src/fastmcp/prompts/prompt.py
@@ -4,7 +4,6 @@ from __future__ import annotations as _annotations
import inspect
import json
-from abc import ABC, abstractmethod
from collections.abc import Awaitable, Callable, Sequence
from typing import Any
@@ -62,7 +61,7 @@ class PromptArgument(FastMCPBaseModel):
)
-class Prompt(FastMCPComponent, ABC):
+class Prompt(FastMCPComponent):
"""A prompt template that can be rendered with parameters."""
arguments: list[PromptArgument] | None = Field(
@@ -139,13 +138,16 @@ class Prompt(FastMCPComponent, ABC):
meta=meta,
)
- @abstractmethod
async def render(
self,
arguments: dict[str, Any] | None = None,
) -> list[PromptMessage]:
- """Render the prompt with arguments."""
- raise NotImplementedError("Prompt.render() must be implemented by subclasses")
+ """Render the prompt with arguments.
+
+ This method is not implemented in the base Prompt class and must be
+ implemented by subclasses.
+ """
+ raise NotImplementedError("Subclasses must implement render()")
class FunctionPrompt(Prompt):
diff --git a/src/fastmcp/resources/resource.py b/src/fastmcp/resources/resource.py
index 3067fb2c7..d7f9d7177 100644
--- a/src/fastmcp/resources/resource.py
+++ b/src/fastmcp/resources/resource.py
@@ -2,7 +2,6 @@
from __future__ import annotations
-import abc
import inspect
from collections.abc import Callable
from typing import TYPE_CHECKING, Annotated, Any
@@ -31,7 +30,7 @@ if TYPE_CHECKING:
pass
-class Resource(FastMCPComponent, abc.ABC):
+class Resource(FastMCPComponent):
"""Base class for all resources."""
model_config = ConfigDict(validate_default=True)
@@ -111,10 +110,13 @@ class Resource(FastMCPComponent, abc.ABC):
raise ValueError("Either name or uri must be provided")
return self
- @abc.abstractmethod
async def read(self) -> str | bytes:
- """Read the resource content."""
- pass
+ """Read the resource content.
+
+ This method is not implemented in the base Resource class and must be
+ implemented by subclasses.
+ """
+ raise NotImplementedError("Subclasses must implement read()")
def to_mcp_resource(
self,
diff --git a/tests/resources/test_resources.py b/tests/resources/test_resources.py
index 1e165eea9..3e30be1d8 100644
--- a/tests/resources/test_resources.py
+++ b/tests/resources/test_resources.py
@@ -85,14 +85,15 @@ class TestResourceValidation:
)
assert resource.mime_type == "application/json"
- async def test_resource_read_abstract(self):
- """Test that Resource.read() is abstract."""
+ async def test_resource_read_not_implemented(self):
+ """Test that Resource.read() raises NotImplementedError."""
class ConcreteResource(Resource):
pass
- with pytest.raises(TypeError, match="abstract method"):
- ConcreteResource(uri=AnyUrl("test://test"), name="test") # type: ignore
+ resource = ConcreteResource(uri=AnyUrl("test://test"), name="test") # type: ignore
+ with pytest.raises(NotImplementedError, match="Subclasses must implement read"):
+ await resource.read()
def test_resource_meta_parameter(self):
"""Test that meta parameter is properly handled."""