8000 Add resource Link (#974) · modelcontextprotocol/python-sdk@d0443a1 · GitHub
[go: up one dir, main page]

Skip to content

Commit d0443a1

Browse files
authored
Add resource Link (#974)
1 parent 86bb54c commit d0443a1

File tree

10 files changed

+66
-22
lines changed

10 files changed

+66
-22
lines changed

examples/servers/simple-streamablehttp-stateless/mcp_simple_streamablehttp_stateless/server.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def main(
4141
app = Server("mcp-streamable-http-stateless-demo")
4242

4343
@app.call_tool()
44-
async def call_tool(name: str, arguments: dict) -> list[types.Content]:
44+
async def call_tool(name: str, arguments: dict) -> list[types.ContentBlock]:
4545
ctx = app.request_context
4646
interval = arguments.get("interval", 1.0)
4747
count = arguments.get("count", 5)

examples/servers/simple-streamablehttp/mcp_simple_streamablehttp/server.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ def main(
4545
app = Server("mcp-streamable-http-demo")
4646

4747
@app.call_tool()
48-
async def call_tool(name: str, arguments: dict) -> list[types.Content]:
48+
async def call_tool(name: str, arguments: dict) -> list[types.ContentBlock]:
4949
ctx = app.request_context
5050
interval = arguments.get("interval", 1.0)
5151
count = arguments.get("count", 5)

examples/servers/simple-tool/mcp_simple_tool/server.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
async def fetch_website(
99
url: str,
10-
) -> list[types.Content]:
10+
) -> list[types.ContentBlock]:
1111
headers = {
1212
"User-Agent": "MCP Test Server (github.com/modelcontextprotocol/python-sdk)"
1313
}
@@ -29,7 +29,7 @@ def main(port: int, transport: str) -> int:
2929
app = Server("mcp-website-fetcher")
3030

3131
@app.call_tool()
32-
async def fetch_tool(name: str, arguments: dict) -> list[types.Content]:
32+
async def fetch_tool(name: str, arguments: dict) -> list[types.ContentBlock]:
3333
if name != "fetch":
3434
raise ValueError(f"Unknown tool: {name}")
3535
if "url" not in arguments:

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,16 @@
77
import pydantic_core
88
from pydantic import BaseModel, Field, TypeAdapter, validate_call
99

10-
from mcp.types import Content, TextContent
10+
from mcp.types import ContentBlock, TextContent
1111

1212

1313
class Message(BaseModel):
1414
"""Base class for all prompt messages."""
1515

1616
role: Literal["user", "assistant"]
17-
content: Content
17+
content: ContentBlock
1818

19-
def __init__(self, content: str | Content, **kwargs: Any):
19+
def __init__(self, content: str | ContentBlock, **kwargs: Any):
2020
if isinstance(content, str):
2121
content = TextContent(type="text", text=content)
2222
super().__init__(content=content, **kwargs)
@@ -27,7 +27,7 @@ class UserMessage(Message):
2727

2828
role: Literal["user", "assistant"] = "user"
2929

30-
def __init__(self, content: str | Content, **kwargs: Any):
30+
def __init__(self, content: str | ContentBlock, **kwargs: Any):
3131
super().__init__(content=content, **kwargs)
3232

3333

@@ -36,7 +36,7 @@ class AssistantMessage(Message):
3636

3737
role: Literal["user", "assistant"] = "assistant"
3838

39-
def __init__(self, content: str | Content, **kwargs: Any):
39+
def __init__(self, content: str | ContentBlock, **kwargs: Any):
4040
super().__init__(content=content, **kwargs)
4141

4242

src/mcp/server/fastmcp/server.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
from mcp.shared.context import LifespanContextT, RequestContext, RequestT
5555
from mcp.types import (
5656
AnyFunction,
57-
Content,
57+
ContentBlock,
5858
GetPromptResult,
5959
TextContent,
6060
ToolAnnotations,
@@ -260,7 +260,7 @@ def get_context(self) -> Context[ServerSession, object, Request]:
260260
request_context = None
261261
return Context(request_context=request_context, fastmcp=self)
262262

263-
async def call_tool(self, name: str, arguments: dict[str, Any]) -> Sequence[Content]:
263+
async def call_tool(self, name: str, arguments: dict[str, Any]) -> Sequence[ContentBlock]:
264264
"""Call a tool by name with arguments."""
265265
context = self.get_context()
266266
result = await self._tool_manager.call_tool(name, arguments, context=context)
@@ -878,12 +878,12 @@ async def get_prompt(self, name: str, arguments: dict[str, Any] | None = None) -
878878

879879
def _convert_to_content(
880880
result: Any,
881-
) -> Sequence[Content]:
881+
) -> Sequence[ContentBlock]:
882882
"""Convert a result to a sequence of content objects."""
883883
if result is None:
884884
return []
885885

886-
if isinstance(result, Content):
886+
if isinstance(result, ContentBlock):
887887
return [result]
888888

889889
if isinstance(result, Image):

src/mcp/server/lowlevel/server.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -384,7 +384,7 @@ def call_tool(self):
384384
def decorator(
385385
func: Callable[
386386
...,
387-
Awaitable[Iterable[types.Content]],
387+
Awaitable[Iterable[types.ContentBlock]],
388388
],
389389
):
390390
logger.debug("Registering handler for CallToolRequest")

src/mcp/types.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -733,14 +733,28 @@ class EmbeddedResource(BaseModel):
733733
model_config = ConfigDict(extra="allow")
734734

735735

736-
Content = TextContent | ImageContent | AudioContent | EmbeddedResource
736+
class ResourceLink(Resource):
737+
"""
738+
A resource that the server is capable of reading, included in a prompt or tool call result.
739+
740+
Note: resource links returned by tools are not guaranteed to appear in the results of `resources/list` requests.
741+
"""
742+
743+
type: Literal["resource_link"]
744+
745+
746+
ContentBlock = TextContent | ImageContent | AudioContent | ResourceLink | EmbeddedResource
747+
"""A content block that can be used in prompts and tool results."""
748+
749+
Content: TypeAlias = ContentBlock
750+
# """DEPRECATED: Content is deprecated, you should use ContentBlock directly."""
737751

738752

739753
class PromptMessage(BaseModel):
740754
"""Describes a message returned as part of a prompt."""
741755

742756
role: Role
743-
content: Content
757+
content: ContentBlock
744758
model_config = ConfigDict(extra="allow")
745759

746760

@@ -859,7 +873,7 @@ class CallToolRequest(Request[CallToolRequestParams, Literal["tools/call"]]):
859873
class CallToolResult(Result):
860874
"""The server's response to a tool call."""
861875

862-
content: list[Content]
876+
content: list[ContentBlock]
863877
isError: bool = False
864878

865879

tests/issues/test_88_random_error.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from mcp.client.session import ClientSession
1212
from mcp.server.lowlevel import Server
1313
from mcp.shared.exceptions import McpError
14-
from mcp.types import Content, TextContent
14+
from mcp.types import ContentBlock, TextContent
1515

1616

1717
@pytest.mark.anyio
@@ -31,7 +31,7 @@ async def test_notification_validation_error(tmp_path: Path):
3131
slow_request_complete = anyio.Event()
3232

3333
@server.call_tool()
34-
async def slow_tool(name: str, arg) -> Sequence[Content]:
34+
async def slow_tool(name: str, arg) -> Sequence[ContentBlock]:
3535
nonlocal request_count
3636
request_count += 1
3737

tests/server/fastmcp/test_integration.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
ProgressNotification,
3939
PromptReference,
4040
ReadResourceResult,
41+
ResourceLink,
4142
ResourceListChangedNotification,
4243
ResourceTemplateReference,
4344
SamplingMessage,
@@ -152,6 +153,25 @@ async def tool_with_progress(message: str, ctx: Context, steps: int = 3) -> str:
152153
def echo(message: str) -> str:
153154
return f"Echo: {message}"
154155

156+
# Tool that returns ResourceLinks
157+
@mcp.tool(description="Lists files and returns resource links", title="List Files Tool")
158+
def list_files() -> list[ResourceLink]:
159+
"""Returns a list of resource links for files matching the pattern."""
160+
161+
# Mock some file resources for testing
162+
file_resources = [
163+
{
164+
"type": "resource_link",
165+
"uri": "file:///project/README.md",
166+
"name": "README.md",
167+
"mimeType": "text/markdown",
168+
}
169+
]
170+
171+
result: list[ResourceLink] = [ResourceLink.model_validate(file_json) for file_json in file_resources]
172+
173+
return result
174+
155175
# Tool with sampling capability
156176
@mcp.tool(description="A tool that uses sampling to generate content", title="Sampling Tool")
157177
async def sampling_tool(prompt: str, ctx: Context) -> str:
@@ -762,7 +782,17 @@ async def call_all_mcp_features(session: ClientSession, collector: NotificationC
762782
assert isinstance(tool_result.content[0], TextContent)
763783
assert tool_result.content[0].text == "Echo: hello"
764784

765-
# 2. Tool with context (logging and progress)
785+
# 2. Test tool that returns ResourceLinks
786+
list_files_result = await session.call_tool("list_files")
787+
assert len(list_files_result.content) == 1
788+
789+
# Rest should be ResourceLinks
790+
content = list_files_result.content[0]
791+
assert isinstance(content, ResourceLink)
792+
assert str(content.uri).startswith("file:///")
793+
assert content.name is not None
794+
assert content.mimeType is not None
795+
766796
# Test progress callback functionality
767797
progress_updates = []
768798

tests/server/fastmcp/test_server.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from mcp.types import (
1919
AudioContent,
2020
BlobResourceContents,
21-
Content,
21+
ContentBlock,
2222
EmbeddedResource,
2323
ImageContent,
2424
TextContent,
@@ -194,7 +194,7 @@ def image_tool_fn(path: str) -> Image:
194194
return Image(path)
195195

196196

197-
def mixed_content_tool_fn() -> list[Content]:
197+
def mixed_content_tool_fn() -> list[ContentBlock]:
198198
return [
199199
TextContent(type="text", text="Hello"),
200200
ImageContent(type="image", data="abc", mimeType="image/png"),

0 commit comments

Comments
 (0)
0