Skip to content

Commit 79c89d5

Browse files
author
Tapan Chugh
committed
feat: implement resource-based inheritance for tools and prompts
Add ToolResource and PromptResource base classes that extend the Resource base class. Tool now inherits from ToolResource and Prompt inherits from PromptResource, establishing a proper inheritance hierarchy. Tools and prompts automatically generate URIs with their respective schemes (tool://name and prompt://name) when created. This allows them to be accessed through the ResourceManager using their URIs while maintaining full backward compatibility with existing code. The ResourceManager's list_resources() method filters out tool and prompt URIs by default to preserve the existing API behavior. Tools and prompts can still be accessed directly by their URIs when needed. Comprehensive tests verify the inheritance model works correctly and that all existing functionality remains intact.
1 parent 7453e70 commit 79c89d5

File tree

7 files changed

+307
-12
lines changed

7 files changed

+307
-12
lines changed

src/mcp/server/fastmcp/prompts/base.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
"""Base classes for FastMCP prompts."""
22

33
import inspect
4+
import json
45
from collections.abc import Awaitable, Callable, Sequence
56
from typing import Any, Literal
67

78
import pydantic_core
89
from pydantic import BaseModel, Field, TypeAdapter, validate_call
910

11+
from mcp.server.fastmcp.resources import PromptResource
1012
from mcp.types import ContentBlock, TextContent
1113

1214

@@ -54,12 +56,9 @@ class PromptArgument(BaseModel):
5456
required: bool = Field(default=False, description="Whether the argument is required")
5557

5658

57-
class Prompt(BaseModel):
59+
class Prompt(PromptResource):
5860
"""A prompt template that can be rendered with parameters."""
5961

60-
name: str = Field(description="Name of the prompt")
61-
title: str | None = Field(None, description="Human-readable title of the prompt")
62-
description: str | None = Field(None, description="Description of what the prompt does")
6362
arguments: list[PromptArgument] | None = Field(None, description="Arguments that can be passed to the prompt")
6463
fn: Callable[..., PromptResult | Awaitable[PromptResult]] = Field(exclude=True)
6564

@@ -111,6 +110,28 @@ def from_function(
111110
fn=fn,
112111
)
113112

113+
async def read(self) -> str | bytes:
114+
"""Read the prompt template/documentation as JSON."""
115+
prompt_info: dict[str, Any] = {
116+
"name": self.name,
117+
"title": self.title,
118+
"description": self.description,
119+
"uri": str(self.uri),
120+
}
121+
122+
# Include arguments if available
123+
if self.arguments:
124+
prompt_info["arguments"] = [
125+
{
126+
"name": arg.name,
127+
"description": arg.description,
128+
"required": arg.required,
129+
}
130+
for arg in self.arguments
131+
]
132+
133+
return json.dumps(prompt_info, indent=2)
134+
114135
async def render(self, arguments: dict[str, Any] | None = None) -> list[Message]:
115136
"""Render the prompt with arguments."""
116137
# Validate required arguments

src/mcp/server/fastmcp/resources/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from .base import Resource
2+
from .prompt_resource import PromptResource
23
from .resource_manager import ResourceManager
34
from .templates import ResourceTemplate
5+
from .tool_resource import ToolResource
46
from .types import (
57
BinaryResource,
68
DirectoryResource,
@@ -20,4 +22,6 @@
2022
"DirectoryResource",
2123
"ResourceTemplate",
2224
"ResourceManager",
25+
"ToolResource",
26+
"PromptResource",
2327
]
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"""Prompt resource base class."""
2+
3+
import json
4+
from typing import Any
5+
6+
from pydantic import AnyUrl, Field
7+
8+
from mcp.server.fastmcp.resources.base import Resource
9+
10+
11+
class PromptResource(Resource):
12+
"""Base class for prompts that are also resources."""
13+
14+
# Override mime_type default for prompts
15+
mime_type: str = Field(
16+
default="application/json",
17+
description="MIME type of the resource content",
18+
pattern=r"^[a-zA-Z0-9]+/[a-zA-Z0-9\-+.]+$",
19+
)
20+
21+
def __init__(self, **data: Any):
22+
# Auto-generate URI if not provided
23+
if "uri" not in data and "name" in data:
24+
data["uri"] = AnyUrl(f"prompt://{data['name']}")
25+
super().__init__(**data)
26+
27+
async def read(self) -> str | bytes:
28+
"""Read the prompt template/documentation as JSON."""
29+
# This will be overridden by the Prompt class
30+
return json.dumps(
31+
{
32+
"name": self.name,
33+
"title": self.title,
34+
"description": self.description,
35+
"uri": str(self.uri),
36+
}
37+
)

src/mcp/server/fastmcp/resources/resource_manager.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,30 @@ async def get_resource(self, uri: AnyUrl | str) -> Resource | None:
8787
raise ValueError(f"Unknown resource: {uri}")
8888

8989
def list_resources(self, prefix: str | None = None) -> list[Resource]:
90-
"""List all registered resources, optionally filtered by URI prefix."""
91-
resources = list(self._resources.values())
90+
"""List all registered resources, optionally filtered by URI prefix.
91+
92+
Note: Tool and prompt resources (with tool:// and prompt:// URIs) are excluded
93+
by default to maintain API compatibility.
94+
"""
95+
all_resources = list(self._resources.values())
96+
97+
# Filter out tool and prompt resources to maintain API compatibility
98+
resources = [
99+
r for r in all_resources if not (str(r.uri).startswith("tool://") or str(r.uri).startswith("prompt://"))
100+
]
101+
102+
# Apply prefix filter if provided
92103
if prefix:
93104
resources = [r for r in resources if str(r.uri).startswith(prefix)]
94-
logger.debug("Listing resources", extra={"count": len(resources), "prefix": prefix})
105+
106+
logger.debug(
107+
"Listing resources",
108+
extra={
109+
"total_count": len(all_resources),
110+
"filtered_count": len(resources),
111+
"prefix": prefix
112+
}
113+
)
95114
return resources
96115

97116
def list_templates(self, prefix: str | None = None) -> list[ResourceTemplate]:
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"""Tool resource base class."""
2+
3+
import json
4+
from typing import Any
5+
6+
from pydantic import AnyUrl, Field
7+
8+
from mcp.server.fastmcp.resources.base import Resource
9+
10+
11+
class ToolResource(Resource):
12+
"""Base class for tools that are also resources."""
13+
14+
# Override mime_type default for tools
15+
mime_type: str = Field(
16+
default="application/json",
17+
description="MIME type of the resource content",
18+
pattern=r"^[a-zA-Z0-9]+/[a-zA-Z0-9\-+.]+$",
19+
)
20+
21+
def __init__(self, **data: Any):
22+
# Auto-generate URI if not provided
23+
if "uri" not in data and "name" in data:
24+
data["uri"] = AnyUrl(f"tool://{data['name']}")
25+
super().__init__(**data)
26+
27+
async def read(self) -> str | bytes:
28+
"""Read the tool schema/documentation as JSON."""
29+
# This will be overridden by the Tool class
30+
return json.dumps(
31+
{
32+
"name": self.name,
33+
"title": self.title,
34+
"description": self.description,
35+
"uri": str(self.uri),
36+
}
37+
)

src/mcp/server/fastmcp/tools/base.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22

33
import functools
44
import inspect
5+
import json
56
from collections.abc import Callable
67
from functools import cached_property
78
from typing import TYPE_CHECKING, Any, get_origin
89

9-
from pydantic import BaseModel, Field
10+
from pydantic import Field
1011

1112
from mcp.server.fastmcp.exceptions import ToolError
13+
from mcp.server.fastmcp.resources import ToolResource
1214
from mcp.server.fastmcp.utilities.func_metadata import FuncMetadata, func_metadata
1315
from mcp.types import ToolAnnotations
1416

@@ -18,13 +20,10 @@
1820
from mcp.shared.context import LifespanContextT, RequestT
1921

2022

21-
class Tool(BaseModel):
23+
class Tool(ToolResource):
2224
"""Internal tool registration info."""
2325

2426
fn: Callable[..., Any] = Field(exclude=True)
25-
name: str = Field(description="Name of the tool")
26-
title: str | None = Field(None, description="Human-readable title of the tool")
27-
description: str = Field(description="Description of what the tool does")
2827
parameters: dict[str, Any] = Field(description="JSON schema for tool parameters")
2928
fn_metadata: FuncMetadata = Field(
3029
description="Metadata about the function including a pydantic model for tool arguments"
@@ -87,6 +86,26 @@ def from_function(
8786
annotations=annotations,
8887
)
8988

89+
async def read(self) -> str | bytes:
90+
"""Read the tool schema/documentation as JSON."""
91+
tool_info = {
92+
"name": self.name,
93+
"title": self.title,
94+
"description": self.description,
95+
"uri": str(self.uri),
96+
"parameters": self.parameters,
97+
}
98+
99+
# Include output schema if available
100+
if self.output_schema:
101+
tool_info["output_schema"] = self.output_schema
102+
103+
# Include annotations if available
104+
if self.annotations:
105+
tool_info["annotations"] = self.annotations.model_dump(exclude_none=True)
106+
107+
return json.dumps(tool_info, indent=2)
108+
90109
async def run(
91110
self,
92111
arguments: dict[str, Any],

0 commit comments

Comments
 (0)