Add meta to Client methods (#1923)
This commit is contained in:
parent
d77292fb06
commit
f0ab53e194
11 changed files with 221 additions and 156 deletions
|
|
@ -19,6 +19,7 @@ from mcp.types import (
|
|||
JSONRPCNotification,
|
||||
JSONRPCRequest,
|
||||
JSONRPCResponse,
|
||||
RequestParamsMeta,
|
||||
ServerCapabilities,
|
||||
TextContent,
|
||||
client_notification_adapter,
|
||||
|
|
@ -608,7 +609,7 @@ async def test_get_server_capabilities():
|
|||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.parametrize(argnames="meta", argvalues=[None, {"toolMeta": "value"}])
|
||||
async def test_client_tool_call_with_meta(meta: dict[str, Any] | None):
|
||||
async def test_client_tool_call_with_meta(meta: RequestParamsMeta | None):
|
||||
"""Test that client tool call requests can include metadata"""
|
||||
client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[SessionMessage](1)
|
||||
server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[SessionMessage](1)
|
||||
|
|
|
|||
|
|
@ -78,8 +78,8 @@ async def test_run_task_basic_flow() -> None:
|
|||
ctx.experimental.validate_task_mode(TASK_REQUIRED)
|
||||
|
||||
# Capture the meta from the request (if present)
|
||||
if ctx.meta is not None and ctx.meta.model_extra: # pragma: no branch
|
||||
received_meta[0] = ctx.meta.model_extra.get("custom_field")
|
||||
if ctx.meta is not None: # pragma: no branch
|
||||
received_meta[0] = ctx.meta.get("custom_field")
|
||||
|
||||
async def work(task: ServerTaskContext) -> CallToolResult:
|
||||
await task.update_status("Working...")
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import pytest
|
||||
from pydantic import AnyUrl
|
||||
|
||||
from mcp import Client
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
|
@ -88,14 +87,14 @@ async def test_resource_template_client_interaction():
|
|||
assert "resource://users/{user_id}/profile" in templates
|
||||
|
||||
# Read a resource with valid parameters
|
||||
result = await session.read_resource(AnyUrl("resource://users/123/posts/456"))
|
||||
result = await session.read_resource("resource://users/123/posts/456")
|
||||
contents = result.contents[0]
|
||||
assert isinstance(contents, TextResourceContents)
|
||||
assert contents.text == "Post 456 by user 123"
|
||||
assert contents.mime_type == "text/plain"
|
||||
|
||||
# Read another resource with valid parameters
|
||||
result = await session.read_resource(AnyUrl("resource://users/789/profile"))
|
||||
result = await session.read_resource("resource://users/789/profile")
|
||||
contents = result.contents[0]
|
||||
assert isinstance(contents, TextResourceContents)
|
||||
assert contents.text == "Profile for user 789"
|
||||
|
|
@ -103,7 +102,7 @@ async def test_resource_template_client_interaction():
|
|||
|
||||
# Verify invalid resource URIs raise appropriate errors
|
||||
with pytest.raises(Exception): # Specific exception type may vary
|
||||
await session.read_resource(AnyUrl("resource://users/123/posts")) # Missing post_id
|
||||
await session.read_resource("resource://users/123/posts") # Missing post_id
|
||||
|
||||
with pytest.raises(Exception): # Specific exception type may vary
|
||||
await session.read_resource(AnyUrl("resource://users/123/invalid")) # Invalid template
|
||||
await session.read_resource("resource://users/123/invalid") # Invalid template
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import base64
|
||||
|
||||
import pytest
|
||||
from pydantic import AnyUrl
|
||||
|
||||
from mcp import Client, types
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
|
@ -46,12 +45,12 @@ async def test_fastmcp_resource_mime_type():
|
|||
assert bytes_resource.mime_type == "image/png", "Bytes resource mime type not respected"
|
||||
|
||||
# Also verify the content can be read correctly
|
||||
string_result = await client.read_resource(AnyUrl("test://image"))
|
||||
string_result = await client.read_resource("test://image")
|
||||
assert len(string_result.contents) == 1
|
||||
assert getattr(string_result.contents[0], "text") == base64_string, "Base64 string mismatch"
|
||||
assert string_result.contents[0].mime_type == "image/png", "String content mime type not preserved"
|
||||
|
||||
bytes_result = await client.read_resource(AnyUrl("test://image_bytes"))
|
||||
bytes_result = await client.read_resource("test://image_bytes")
|
||||
assert len(bytes_result.contents) == 1
|
||||
assert base64.b64decode(getattr(bytes_result.contents[0], "blob")) == image_bytes, "Bytes mismatch"
|
||||
assert bytes_result.contents[0].mime_type == "image/png", "Bytes content mime type not preserved"
|
||||
|
|
@ -104,12 +103,12 @@ async def test_lowlevel_resource_mime_type():
|
|||
assert bytes_resource.mime_type == "image/png", "Bytes resource mime type not respected"
|
||||
|
||||
# Also verify the content can be read correctly
|
||||
string_result = await client.read_resource(AnyUrl("test://image"))
|
||||
string_result = await client.read_resource("test://image")
|
||||
assert len(string_result.contents) == 1
|
||||
assert getattr(string_result.contents[0], "text") == base64_string, "Base64 string mismatch"
|
||||
assert string_result.contents[0].mime_type == "image/png", "String content mime type not preserved"
|
||||
|
||||
bytes_result = await client.read_resource(AnyUrl("test://image_bytes"))
|
||||
bytes_result = await client.read_resource("test://image_bytes")
|
||||
assert len(bytes_result.contents) == 1
|
||||
assert base64.b64decode(getattr(bytes_result.contents[0], "blob")) == image_bytes, "Bytes mismatch"
|
||||
assert bytes_result.contents[0].mime_type == "image/png", "Bytes content mime type not preserved"
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ with parameters like 'text/html;profile=mcp-app' which are valid per RFC 2045.
|
|||
"""
|
||||
|
||||
import pytest
|
||||
from pydantic import AnyUrl
|
||||
|
||||
from mcp import Client
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
|
@ -63,6 +62,6 @@ async def test_mime_type_preserved_in_read_resource():
|
|||
|
||||
async with Client(mcp) as client:
|
||||
# Read the resource
|
||||
result = await client.read_resource(AnyUrl("ui://my-widget"))
|
||||
result = await client.read_resource("ui://my-widget")
|
||||
assert len(result.contents) == 1
|
||||
assert result.contents[0].mime_type == "text/html;profile=mcp-app"
|
||||
|
|
|
|||
|
|
@ -16,13 +16,10 @@ async def test_progress_token_zero_first_call():
|
|||
mock_session.send_progress_notification = AsyncMock()
|
||||
|
||||
# Create request context with progress token 0
|
||||
mock_meta = MagicMock()
|
||||
mock_meta.progress_token = 0 # This is the key test case - token is 0
|
||||
|
||||
request_context = RequestContext(
|
||||
request_id="test-request",
|
||||
session=mock_session,
|
||||
meta=mock_meta,
|
||||
meta={"progress_token": 0},
|
||||
lifespan_context=None,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import anyio
|
||||
import pytest
|
||||
from pydantic import AnyUrl
|
||||
|
||||
from mcp import Client
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
|
@ -76,7 +75,7 @@ async def test_messages_are_executed_concurrently_tools_and_resources():
|
|||
# Start the tool first (it will wait on event)
|
||||
tg.start_soon(client_session.call_tool, "sleep")
|
||||
# Then the resource (it will set the event)
|
||||
tg.start_soon(client_session.read_resource, AnyUrl("slow://slow_resource"))
|
||||
tg.start_soon(client_session.read_resource, "slow://slow_resource")
|
||||
|
||||
# Verify that both ran concurrently
|
||||
assert call_order == [
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ from collections.abc import Generator
|
|||
import pytest
|
||||
import uvicorn
|
||||
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
|
||||
from pydantic import AnyUrl
|
||||
from inline_snapshot import snapshot
|
||||
|
||||
from examples.snippets.servers import (
|
||||
basic_prompt,
|
||||
|
|
@ -300,14 +300,14 @@ async def test_basic_resources(server_transport: str, server_url: str) -> None:
|
|||
assert result.capabilities.resources is not None
|
||||
|
||||
# Test document resource
|
||||
doc_content = await session.read_resource(AnyUrl("file://documents/readme"))
|
||||
doc_content = await session.read_resource("file://documents/readme")
|
||||
assert isinstance(doc_content, ReadResourceResult)
|
||||
assert len(doc_content.contents) == 1
|
||||
assert isinstance(doc_content.contents[0], TextResourceContents)
|
||||
assert "Content of readme" in doc_content.contents[0].text
|
||||
|
||||
# Test settings resource
|
||||
settings_content = await session.read_resource(AnyUrl("config://settings"))
|
||||
settings_content = await session.read_resource("config://settings")
|
||||
assert isinstance(settings_content, ReadResourceResult)
|
||||
assert len(settings_content.contents) == 1
|
||||
assert isinstance(settings_content.contents[0], TextResourceContents)
|
||||
|
|
@ -412,10 +412,7 @@ async def test_tool_progress(server_transport: str, server_url: str) -> None:
|
|||
{"task_name": "Test Task", "steps": steps},
|
||||
progress_callback=progress_callback,
|
||||
)
|
||||
|
||||
assert len(tool_result.content) == 1
|
||||
assert isinstance(tool_result.content[0], TextContent)
|
||||
assert "Task 'Test Task' completed" in tool_result.content[0].text
|
||||
assert tool_result.content == snapshot([TextContent(text="Task 'Test Task' completed")])
|
||||
|
||||
# Verify progress updates
|
||||
assert len(progress_updates) == steps
|
||||
|
|
@ -641,7 +638,7 @@ async def test_fastmcp_quickstart(server_transport: str, server_url: str) -> Non
|
|||
assert tool_result.content[0].text == "30"
|
||||
|
||||
# Test greeting resource directly
|
||||
resource_result = await session.read_resource(AnyUrl("greeting://Alice"))
|
||||
resource_result = await session.read_resource("greeting://Alice")
|
||||
assert len(resource_result.contents) == 1
|
||||
assert isinstance(resource_result.contents[0], TextResourceContents)
|
||||
assert resource_result.contents[0].text == "Hello, Alice!"
|
||||
|
|
|
|||
|
|
@ -275,11 +275,10 @@ async def test_progress_context_manager():
|
|||
progress_token = "client_token_456"
|
||||
|
||||
# Create request context
|
||||
meta = types.RequestParams.Meta(progress_token=progress_token)
|
||||
request_context = RequestContext(
|
||||
request_id="test-request",
|
||||
session=client_session,
|
||||
meta=meta,
|
||||
meta={"progress_token": progress_token},
|
||||
lifespan_context=None,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ from urllib.parse import urlparse
|
|||
import anyio
|
||||
import pytest
|
||||
import uvicorn
|
||||
from pydantic import AnyUrl
|
||||
from starlette.applications import Starlette
|
||||
from starlette.routing import WebSocketRoute
|
||||
from starlette.websockets import WebSocket
|
||||
|
|
@ -164,7 +163,7 @@ async def test_ws_client_happy_request_and_response(
|
|||
initialized_ws_client_session: ClientSession,
|
||||
) -> None:
|
||||
"""Test a successful request and response via WebSocket"""
|
||||
result = await initialized_ws_client_session.read_resource(AnyUrl("foobar://example"))
|
||||
result = await initialized_ws_client_session.read_resource("foobar://example")
|
||||
assert isinstance(result, ReadResourceResult)
|
||||
assert isinstance(result.contents, list)
|
||||
assert len(result.contents) > 0
|
||||
|
|
@ -178,7 +177,7 @@ async def test_ws_client_exception_handling(
|
|||
) -> None:
|
||||
"""Test exception handling in WebSocket communication"""
|
||||
with pytest.raises(McpError) as exc_info:
|
||||
await initialized_ws_client_session.read_resource(AnyUrl("unknown://example"))
|
||||
await initialized_ws_client_session.read_resource("unknown://example")
|
||||
assert exc_info.value.error.code == 404
|
||||
|
||||
|
||||
|
|
@ -190,11 +189,11 @@ async def test_ws_client_timeout(
|
|||
# Set a very short timeout to trigger a timeout exception
|
||||
with pytest.raises(TimeoutError):
|
||||
with anyio.fail_after(0.1): # 100ms timeout
|
||||
await initialized_ws_client_session.read_resource(AnyUrl("slow://example"))
|
||||
await initialized_ws_client_session.read_resource("slow://example")
|
||||
|
||||
# Now test that we can still use the session after a timeout
|
||||
with anyio.fail_after(5): # Longer timeout to allow completion
|
||||
result = await initialized_ws_client_session.read_resource(AnyUrl("foobar://example"))
|
||||
result = await initialized_ws_client_session.read_resource("foobar://example")
|
||||
assert isinstance(result, ReadResourceResult)
|
||||
assert isinstance(result.contents, list)
|
||||
assert len(result.contents) > 0
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import sys
|
|||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from pydantic import AnyUrl
|
||||
from pytest_examples import CodeExample, EvalExample, find_examples
|
||||
|
||||
from mcp import Client
|
||||
|
|
@ -82,7 +81,7 @@ async def test_desktop(monkeypatch: pytest.MonkeyPatch):
|
|||
assert content.text == "3"
|
||||
|
||||
# Test the desktop resource
|
||||
result = await client.read_resource(AnyUrl("dir://desktop"))
|
||||
result = await client.read_resource("dir://desktop")
|
||||
assert len(result.contents) == 1
|
||||
content = result.contents[0]
|
||||
assert isinstance(content, TextResourceContents)
|
||||
|
|
|
|||
Loading…
Reference in a new issue