Add meta to Client methods (#1923)
This commit is contained in:
parent
d77292fb06
commit
f0ab53e194
27 changed files with 221 additions and 156 deletions
|
|
@ -2119,8 +2119,6 @@ uv run client
|
||||||
import asyncio
|
import asyncio
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from pydantic import AnyUrl
|
|
||||||
|
|
||||||
from mcp import ClientSession, StdioServerParameters, types
|
from mcp import ClientSession, StdioServerParameters, types
|
||||||
from mcp.client.stdio import stdio_client
|
from mcp.client.stdio import stdio_client
|
||||||
from mcp.shared.context import RequestContext
|
from mcp.shared.context import RequestContext
|
||||||
|
|
@ -2173,7 +2171,7 @@ async def run():
|
||||||
print(f"Available tools: {[t.name for t in tools.tools]}")
|
print(f"Available tools: {[t.name for t in tools.tools]}")
|
||||||
|
|
||||||
# Read a resource (greeting resource from fastmcp_quickstart)
|
# Read a resource (greeting resource from fastmcp_quickstart)
|
||||||
resource_content = await session.read_resource(AnyUrl("greeting://World"))
|
resource_content = await session.read_resource("greeting://World")
|
||||||
content_block = resource_content.contents[0]
|
content_block = resource_content.contents[0]
|
||||||
if isinstance(content_block, types.TextContent):
|
if isinstance(content_block, types.TextContent):
|
||||||
print(f"Resource content: {content_block.text}")
|
print(f"Resource content: {content_block.text}")
|
||||||
|
|
|
||||||
|
|
@ -233,6 +233,35 @@ notification = server_notification_adapter.validate_python(data)
|
||||||
|
|
||||||
All adapters are exported from `mcp.types`.
|
All adapters are exported from `mcp.types`.
|
||||||
|
|
||||||
|
### `RequestParams.Meta` replaced with `RequestParamsMeta` TypedDict
|
||||||
|
|
||||||
|
The nested `RequestParams.Meta` Pydantic model class has been replaced with a top-level `RequestParamsMeta` TypedDict. This affects the `ctx.meta` field in request handlers and any code that imports or references this type.
|
||||||
|
|
||||||
|
**Key changes:**
|
||||||
|
|
||||||
|
- `RequestParams.Meta` (Pydantic model) → `RequestParamsMeta` (TypedDict)
|
||||||
|
- Attribute access (`meta.progress_token`) → Dictionary access (`meta.get("progress_token")`)
|
||||||
|
- `progress_token` field changed from `ProgressToken | None = None` to `NotRequired[ProgressToken]`
|
||||||
|
`
|
||||||
|
|
||||||
|
**In request context handlers:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Before (v1)
|
||||||
|
@server.call_tool()
|
||||||
|
async def handle_tool(name: str, arguments: dict) -> list[TextContent]:
|
||||||
|
ctx = server.request_context
|
||||||
|
if ctx.meta and ctx.meta.progress_token:
|
||||||
|
await ctx.session.send_progress_notification(ctx.meta.progress_token, 0.5, 100)
|
||||||
|
|
||||||
|
# After (v2)
|
||||||
|
@server.call_tool()
|
||||||
|
async def handle_tool(name: str, arguments: dict) -> list[TextContent]:
|
||||||
|
ctx = server.request_context
|
||||||
|
if ctx.meta and "progress_token" in ctx.meta:
|
||||||
|
await ctx.session.send_progress_notification(ctx.meta["progress_token"], 0.5, 100)
|
||||||
|
```
|
||||||
|
|
||||||
### Resource URI type changed from `AnyUrl` to `str`
|
### Resource URI type changed from `AnyUrl` to `str`
|
||||||
|
|
||||||
The `uri` field on resource-related types now uses `str` instead of Pydantic's `AnyUrl`. This aligns with the [MCP specification schema](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/draft/schema.ts) which defines URIs as plain strings (`uri: string`) without strict URL validation. This change allows relative paths like `users/me` that were previously rejected.
|
The `uri` field on resource-related types now uses `str` instead of Pydantic's `AnyUrl`. This aligns with the [MCP specification schema](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/draft/schema.ts) which defines URIs as plain strings (`uri: string`) without strict URL validation. This change allows relative paths like `users/me` that were previously rejected.
|
||||||
|
|
@ -274,7 +303,19 @@ Affected types:
|
||||||
- `UnsubscribeRequestParams.uri`
|
- `UnsubscribeRequestParams.uri`
|
||||||
- `ResourceUpdatedNotificationParams.uri`
|
- `ResourceUpdatedNotificationParams.uri`
|
||||||
|
|
||||||
The `ClientSession.read_resource()`, `subscribe_resource()`, and `unsubscribe_resource()` methods now accept both `str` and `AnyUrl` for backwards compatibility.
|
The `Client` and `ClientSession` methods `read_resource()`, `subscribe_resource()`, and `unsubscribe_resource()` now only accept `str` for the `uri` parameter. If you were passing `AnyUrl` objects, convert them to strings:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Before (v1)
|
||||||
|
from pydantic import AnyUrl
|
||||||
|
|
||||||
|
await client.read_resource(AnyUrl("test://resource"))
|
||||||
|
|
||||||
|
# After (v2)
|
||||||
|
await client.read_resource("test://resource")
|
||||||
|
# Or if you have an AnyUrl from elsewhere:
|
||||||
|
await client.read_resource(str(my_any_url))
|
||||||
|
```
|
||||||
|
|
||||||
## Deprecations
|
## Deprecations
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -161,7 +161,9 @@ async def test_tool_with_progress(ctx: Context[ServerSession, None]) -> str:
|
||||||
await ctx.report_progress(progress=100, total=100, message="Completed step 100 of 100")
|
await ctx.report_progress(progress=100, total=100, message="Completed step 100 of 100")
|
||||||
|
|
||||||
# Return progress token as string
|
# Return progress token as string
|
||||||
progress_token = ctx.request_context.meta.progress_token if ctx.request_context and ctx.request_context.meta else 0
|
progress_token = (
|
||||||
|
ctx.request_context.meta.get("progress_token") if ctx.request_context and ctx.request_context.meta else 0
|
||||||
|
)
|
||||||
return str(progress_token)
|
return str(progress_token)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,6 @@ uv run client
|
||||||
import asyncio
|
import asyncio
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from pydantic import AnyUrl
|
|
||||||
|
|
||||||
from mcp import ClientSession, StdioServerParameters, types
|
from mcp import ClientSession, StdioServerParameters, types
|
||||||
from mcp.client.stdio import stdio_client
|
from mcp.client.stdio import stdio_client
|
||||||
from mcp.shared.context import RequestContext
|
from mcp.shared.context import RequestContext
|
||||||
|
|
@ -59,7 +57,7 @@ async def run():
|
||||||
print(f"Available tools: {[t.name for t in tools.tools]}")
|
print(f"Available tools: {[t.name for t in tools.tools]}")
|
||||||
|
|
||||||
# Read a resource (greeting resource from fastmcp_quickstart)
|
# Read a resource (greeting resource from fastmcp_quickstart)
|
||||||
resource_content = await session.read_resource(AnyUrl("greeting://World"))
|
resource_content = await session.read_resource("greeting://World")
|
||||||
content_block = resource_content.contents[0]
|
content_block = resource_content.contents[0]
|
||||||
if isinstance(content_block, types.TextContent):
|
if isinstance(content_block, types.TextContent):
|
||||||
print(f"Resource content: {content_block.text}")
|
print(f"Resource content: {content_block.text}")
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ dependencies = [
|
||||||
"jsonschema>=4.20.0",
|
"jsonschema>=4.20.0",
|
||||||
"pywin32>=311; sys_platform == 'win32'",
|
"pywin32>=311; sys_platform == 'win32'",
|
||||||
"pyjwt[crypto]>=2.10.1",
|
"pyjwt[crypto]>=2.10.1",
|
||||||
"typing-extensions>=4.9.0",
|
"typing-extensions>=4.13.0",
|
||||||
"typing-inspection>=0.4.1",
|
"typing-inspection>=0.4.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,21 +5,29 @@ from __future__ import annotations
|
||||||
from contextlib import AsyncExitStack
|
from contextlib import AsyncExitStack
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from pydantic import AnyUrl
|
|
||||||
|
|
||||||
import mcp.types as types
|
|
||||||
from mcp.client._memory import InMemoryTransport
|
from mcp.client._memory import InMemoryTransport
|
||||||
from mcp.client.session import (
|
from mcp.client.session import ClientSession, ElicitationFnT, ListRootsFnT, LoggingFnT, MessageHandlerFnT, SamplingFnT
|
||||||
ClientSession,
|
|
||||||
ElicitationFnT,
|
|
||||||
ListRootsFnT,
|
|
||||||
LoggingFnT,
|
|
||||||
MessageHandlerFnT,
|
|
||||||
SamplingFnT,
|
|
||||||
)
|
|
||||||
from mcp.server import Server
|
from mcp.server import Server
|
||||||
from mcp.server.fastmcp import FastMCP
|
from mcp.server.fastmcp import FastMCP
|
||||||
from mcp.shared.session import ProgressFnT
|
from mcp.shared.session import ProgressFnT
|
||||||
|
from mcp.types import (
|
||||||
|
CallToolResult,
|
||||||
|
CompleteResult,
|
||||||
|
EmptyResult,
|
||||||
|
GetPromptResult,
|
||||||
|
Implementation,
|
||||||
|
ListPromptsResult,
|
||||||
|
ListResourcesResult,
|
||||||
|
ListResourceTemplatesResult,
|
||||||
|
ListToolsResult,
|
||||||
|
LoggingLevel,
|
||||||
|
PaginatedRequestParams,
|
||||||
|
PromptReference,
|
||||||
|
ReadResourceResult,
|
||||||
|
RequestParamsMeta,
|
||||||
|
ResourceTemplateReference,
|
||||||
|
ServerCapabilities,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Client:
|
class Client:
|
||||||
|
|
@ -39,8 +47,11 @@ class Client:
|
||||||
def add(a: int, b: int) -> int:
|
def add(a: int, b: int) -> int:
|
||||||
return a + b
|
return a + b
|
||||||
|
|
||||||
async with Client(server) as client:
|
async def main():
|
||||||
result = await client.call_tool("add", {"a": 1, "b": 2})
|
async with Client(server) as client:
|
||||||
|
result = await client.call_tool("add", {"a": 1, "b": 2})
|
||||||
|
|
||||||
|
asyncio.run(main())
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
@ -62,7 +73,7 @@ class Client:
|
||||||
list_roots_callback: ListRootsFnT | None = None,
|
list_roots_callback: ListRootsFnT | None = None,
|
||||||
logging_callback: LoggingFnT | None = None,
|
logging_callback: LoggingFnT | None = None,
|
||||||
message_handler: MessageHandlerFnT | None = None,
|
message_handler: MessageHandlerFnT | None = None,
|
||||||
client_info: types.Implementation | None = None,
|
client_info: Implementation | None = None,
|
||||||
elicitation_callback: ElicitationFnT | None = None,
|
elicitation_callback: ElicitationFnT | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the client with a server.
|
"""Initialize the client with a server.
|
||||||
|
|
@ -143,13 +154,13 @@ class Client:
|
||||||
return self._session
|
return self._session
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def server_capabilities(self) -> types.ServerCapabilities | None:
|
def server_capabilities(self) -> ServerCapabilities | None:
|
||||||
"""The server capabilities received during initialization, or None if not yet initialized."""
|
"""The server capabilities received during initialization, or None if not yet initialized."""
|
||||||
return self.session.get_server_capabilities()
|
return self.session.get_server_capabilities()
|
||||||
|
|
||||||
async def send_ping(self) -> types.EmptyResult:
|
async def send_ping(self, *, meta: RequestParamsMeta | None = None) -> EmptyResult:
|
||||||
"""Send a ping request to the server."""
|
"""Send a ping request to the server."""
|
||||||
return await self.session.send_ping()
|
return await self.session.send_ping(meta=meta)
|
||||||
|
|
||||||
async def send_progress_notification(
|
async def send_progress_notification(
|
||||||
self,
|
self,
|
||||||
|
|
@ -166,36 +177,47 @@ class Client:
|
||||||
message=message,
|
message=message,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def set_logging_level(self, level: types.LoggingLevel) -> types.EmptyResult:
|
async def set_logging_level(self, level: LoggingLevel, *, meta: RequestParamsMeta | None = None) -> EmptyResult:
|
||||||
"""Set the logging level on the server."""
|
"""Set the logging level on the server."""
|
||||||
return await self.session.set_logging_level(level)
|
return await self.session.set_logging_level(level=level, meta=meta)
|
||||||
|
|
||||||
async def list_resources(self, *, cursor: str | None = None) -> types.ListResourcesResult:
|
async def list_resources(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
cursor: str | None = None,
|
||||||
|
meta: RequestParamsMeta | None = None,
|
||||||
|
) -> ListResourcesResult:
|
||||||
"""List available resources from the server."""
|
"""List available resources from the server."""
|
||||||
return await self.session.list_resources(params=types.PaginatedRequestParams(cursor=cursor))
|
return await self.session.list_resources(params=PaginatedRequestParams(cursor=cursor, _meta=meta))
|
||||||
|
|
||||||
async def list_resource_templates(self, *, cursor: str | None = None) -> types.ListResourceTemplatesResult:
|
async def list_resource_templates(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
cursor: str | None = None,
|
||||||
|
meta: RequestParamsMeta | None = None,
|
||||||
|
) -> ListResourceTemplatesResult:
|
||||||
"""List available resource templates from the server."""
|
"""List available resource templates from the server."""
|
||||||
return await self.session.list_resource_templates(params=types.PaginatedRequestParams(cursor=cursor))
|
return await self.session.list_resource_templates(params=PaginatedRequestParams(cursor=cursor, _meta=meta))
|
||||||
|
|
||||||
async def read_resource(self, uri: str | AnyUrl) -> types.ReadResourceResult:
|
async def read_resource(self, uri: str, *, meta: RequestParamsMeta | None = None) -> ReadResourceResult:
|
||||||
"""Read a resource from the server.
|
"""Read a resource from the server.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
uri: The URI of the resource to read.
|
uri: The URI of the resource to read.
|
||||||
|
meta: Additional metadata for the request
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The resource content.
|
The resource content.
|
||||||
"""
|
"""
|
||||||
return await self.session.read_resource(uri)
|
return await self.session.read_resource(uri, meta=meta)
|
||||||
|
|
||||||
async def subscribe_resource(self, uri: str | AnyUrl) -> types.EmptyResult:
|
async def subscribe_resource(self, uri: str, *, meta: RequestParamsMeta | None = None) -> EmptyResult:
|
||||||
"""Subscribe to resource updates."""
|
"""Subscribe to resource updates."""
|
||||||
return await self.session.subscribe_resource(uri)
|
return await self.session.subscribe_resource(uri, meta=meta)
|
||||||
|
|
||||||
async def unsubscribe_resource(self, uri: str | AnyUrl) -> types.EmptyResult:
|
async def unsubscribe_resource(self, uri: str, *, meta: RequestParamsMeta | None = None) -> EmptyResult:
|
||||||
"""Unsubscribe from resource updates."""
|
"""Unsubscribe from resource updates."""
|
||||||
return await self.session.unsubscribe_resource(uri)
|
return await self.session.unsubscribe_resource(uri, meta=meta)
|
||||||
|
|
||||||
async def call_tool(
|
async def call_tool(
|
||||||
self,
|
self,
|
||||||
|
|
@ -204,8 +226,8 @@ class Client:
|
||||||
read_timeout_seconds: float | None = None,
|
read_timeout_seconds: float | None = None,
|
||||||
progress_callback: ProgressFnT | None = None,
|
progress_callback: ProgressFnT | None = None,
|
||||||
*,
|
*,
|
||||||
meta: dict[str, Any] | None = None,
|
meta: RequestParamsMeta | None = None,
|
||||||
) -> types.CallToolResult:
|
) -> CallToolResult:
|
||||||
"""Call a tool on the server.
|
"""Call a tool on the server.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|
@ -226,28 +248,36 @@ class Client:
|
||||||
meta=meta,
|
meta=meta,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def list_prompts(self, *, cursor: str | None = None) -> types.ListPromptsResult:
|
async def list_prompts(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
cursor: str | None = None,
|
||||||
|
meta: RequestParamsMeta | None = None,
|
||||||
|
) -> ListPromptsResult:
|
||||||
"""List available prompts from the server."""
|
"""List available prompts from the server."""
|
||||||
return await self.session.list_prompts(params=types.PaginatedRequestParams(cursor=cursor))
|
return await self.session.list_prompts(params=PaginatedRequestParams(cursor=cursor, _meta=meta))
|
||||||
|
|
||||||
async def get_prompt(self, name: str, arguments: dict[str, str] | None = None) -> types.GetPromptResult:
|
async def get_prompt(
|
||||||
|
self, name: str, arguments: dict[str, str] | None = None, *, meta: RequestParamsMeta | None = None
|
||||||
|
) -> GetPromptResult:
|
||||||
"""Get a prompt from the server.
|
"""Get a prompt from the server.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
name: The name of the prompt
|
name: The name of the prompt
|
||||||
arguments: Arguments to pass to the prompt
|
arguments: Arguments to pass to the prompt
|
||||||
|
meta: Additional metadata for the request
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The prompt content.
|
The prompt content.
|
||||||
"""
|
"""
|
||||||
return await self.session.get_prompt(name=name, arguments=arguments)
|
return await self.session.get_prompt(name=name, arguments=arguments, meta=meta)
|
||||||
|
|
||||||
async def complete(
|
async def complete(
|
||||||
self,
|
self,
|
||||||
ref: types.ResourceTemplateReference | types.PromptReference,
|
ref: ResourceTemplateReference | PromptReference,
|
||||||
argument: dict[str, str],
|
argument: dict[str, str],
|
||||||
context_arguments: dict[str, str] | None = None,
|
context_arguments: dict[str, str] | None = None,
|
||||||
) -> types.CompleteResult:
|
) -> CompleteResult:
|
||||||
"""Get completions for a prompt or resource template argument.
|
"""Get completions for a prompt or resource template argument.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|
@ -260,9 +290,9 @@ class Client:
|
||||||
"""
|
"""
|
||||||
return await self.session.complete(ref=ref, argument=argument, context_arguments=context_arguments)
|
return await self.session.complete(ref=ref, argument=argument, context_arguments=context_arguments)
|
||||||
|
|
||||||
async def list_tools(self, *, cursor: str | None = None) -> types.ListToolsResult:
|
async def list_tools(self, *, cursor: str | None = None, meta: RequestParamsMeta | None = None) -> ListToolsResult:
|
||||||
"""List available tools from the server."""
|
"""List available tools from the server."""
|
||||||
return await self.session.list_tools(params=types.PaginatedRequestParams(cursor=cursor))
|
return await self.session.list_tools(params=PaginatedRequestParams(cursor=cursor, _meta=meta))
|
||||||
|
|
||||||
async def send_roots_list_changed(self) -> None:
|
async def send_roots_list_changed(self) -> None:
|
||||||
"""Send a notification that the roots list has changed."""
|
"""Send a notification that the roots list has changed."""
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ from typing import TYPE_CHECKING, Any, TypeVar
|
||||||
|
|
||||||
import mcp.types as types
|
import mcp.types as types
|
||||||
from mcp.shared.experimental.tasks.polling import poll_until_terminal
|
from mcp.shared.experimental.tasks.polling import poll_until_terminal
|
||||||
|
from mcp.types._types import RequestParamsMeta
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from mcp.client.session import ClientSession
|
from mcp.client.session import ClientSession
|
||||||
|
|
@ -53,7 +54,7 @@ class ExperimentalClientFeatures:
|
||||||
arguments: dict[str, Any] | None = None,
|
arguments: dict[str, Any] | None = None,
|
||||||
*,
|
*,
|
||||||
ttl: int = 60000,
|
ttl: int = 60000,
|
||||||
meta: dict[str, Any] | None = None,
|
meta: RequestParamsMeta | None = None,
|
||||||
) -> types.CreateTaskResult:
|
) -> types.CreateTaskResult:
|
||||||
"""Call a tool as a task, returning a CreateTaskResult for polling.
|
"""Call a tool as a task, returning a CreateTaskResult for polling.
|
||||||
|
|
||||||
|
|
@ -87,17 +88,13 @@ class ExperimentalClientFeatures:
|
||||||
# Get result
|
# Get result
|
||||||
final = await session.experimental.get_task_result(task_id, CallToolResult)
|
final = await session.experimental.get_task_result(task_id, CallToolResult)
|
||||||
"""
|
"""
|
||||||
_meta: types.RequestParams.Meta | None = None
|
|
||||||
if meta is not None:
|
|
||||||
_meta = types.RequestParams.Meta(**meta)
|
|
||||||
|
|
||||||
return await self._session.send_request(
|
return await self._session.send_request(
|
||||||
types.CallToolRequest(
|
types.CallToolRequest(
|
||||||
params=types.CallToolRequestParams(
|
params=types.CallToolRequestParams(
|
||||||
name=name,
|
name=name,
|
||||||
arguments=arguments,
|
arguments=arguments,
|
||||||
task=types.TaskMetadata(ttl=ttl),
|
task=types.TaskMetadata(ttl=ttl),
|
||||||
_meta=_meta,
|
_meta=meta,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
types.CreateTaskResult,
|
types.CreateTaskResult,
|
||||||
|
|
@ -113,9 +110,7 @@ class ExperimentalClientFeatures:
|
||||||
GetTaskResult containing the task status and metadata
|
GetTaskResult containing the task status and metadata
|
||||||
"""
|
"""
|
||||||
return await self._session.send_request(
|
return await self._session.send_request(
|
||||||
types.GetTaskRequest(
|
types.GetTaskRequest(params=types.GetTaskRequestParams(task_id=task_id)),
|
||||||
params=types.GetTaskRequestParams(task_id=task_id),
|
|
||||||
),
|
|
||||||
types.GetTaskResult,
|
types.GetTaskResult,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ from typing import Any, Protocol
|
||||||
|
|
||||||
import anyio.lowlevel
|
import anyio.lowlevel
|
||||||
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
|
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
|
||||||
from pydantic import AnyUrl, TypeAdapter
|
from pydantic import TypeAdapter
|
||||||
|
|
||||||
import mcp.types as types
|
import mcp.types as types
|
||||||
from mcp.client.experimental import ExperimentalClientFeatures
|
from mcp.client.experimental import ExperimentalClientFeatures
|
||||||
|
|
@ -12,6 +12,7 @@ from mcp.shared.context import RequestContext
|
||||||
from mcp.shared.message import SessionMessage
|
from mcp.shared.message import SessionMessage
|
||||||
from mcp.shared.session import BaseSession, ProgressFnT, RequestResponder
|
from mcp.shared.session import BaseSession, ProgressFnT, RequestResponder
|
||||||
from mcp.shared.version import SUPPORTED_PROTOCOL_VERSIONS
|
from mcp.shared.version import SUPPORTED_PROTOCOL_VERSIONS
|
||||||
|
from mcp.types._types import RequestParamsMeta
|
||||||
|
|
||||||
DEFAULT_CLIENT_INFO = types.Implementation(name="mcp", version="0.1.0")
|
DEFAULT_CLIENT_INFO = types.Implementation(name="mcp", version="0.1.0")
|
||||||
|
|
||||||
|
|
@ -216,9 +217,9 @@ class ClientSession(
|
||||||
self._experimental_features = ExperimentalClientFeatures(self)
|
self._experimental_features = ExperimentalClientFeatures(self)
|
||||||
return self._experimental_features
|
return self._experimental_features
|
||||||
|
|
||||||
async def send_ping(self) -> types.EmptyResult:
|
async def send_ping(self, *, meta: RequestParamsMeta | None = None) -> types.EmptyResult:
|
||||||
"""Send a ping request."""
|
"""Send a ping request."""
|
||||||
return await self.send_request(types.PingRequest(), types.EmptyResult)
|
return await self.send_request(types.PingRequest(params=types.RequestParams(_meta=meta)), types.EmptyResult)
|
||||||
|
|
||||||
async def send_progress_notification(
|
async def send_progress_notification(
|
||||||
self,
|
self,
|
||||||
|
|
@ -226,6 +227,8 @@ class ClientSession(
|
||||||
progress: float,
|
progress: float,
|
||||||
total: float | None = None,
|
total: float | None = None,
|
||||||
message: str | None = None,
|
message: str | None = None,
|
||||||
|
*,
|
||||||
|
meta: RequestParamsMeta | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Send a progress notification."""
|
"""Send a progress notification."""
|
||||||
await self.send_notification(
|
await self.send_notification(
|
||||||
|
|
@ -235,14 +238,20 @@ class ClientSession(
|
||||||
progress=progress,
|
progress=progress,
|
||||||
total=total,
|
total=total,
|
||||||
message=message,
|
message=message,
|
||||||
|
_meta=meta,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
async def set_logging_level(self, level: types.LoggingLevel) -> types.EmptyResult:
|
async def set_logging_level(
|
||||||
|
self,
|
||||||
|
level: types.LoggingLevel,
|
||||||
|
*,
|
||||||
|
meta: RequestParamsMeta | None = None,
|
||||||
|
) -> types.EmptyResult:
|
||||||
"""Send a logging/setLevel request."""
|
"""Send a logging/setLevel request."""
|
||||||
return await self.send_request( # pragma: no cover
|
return await self.send_request( # pragma: no cover
|
||||||
types.SetLevelRequest(params=types.SetLevelRequestParams(level=level)),
|
types.SetLevelRequest(params=types.SetLevelRequestParams(level=level, _meta=meta)),
|
||||||
types.EmptyResult,
|
types.EmptyResult,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -267,24 +276,24 @@ class ClientSession(
|
||||||
types.ListResourceTemplatesResult,
|
types.ListResourceTemplatesResult,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def read_resource(self, uri: str | AnyUrl) -> types.ReadResourceResult:
|
async def read_resource(self, uri: str, *, meta: RequestParamsMeta | None = None) -> types.ReadResourceResult:
|
||||||
"""Send a resources/read request."""
|
"""Send a resources/read request."""
|
||||||
return await self.send_request(
|
return await self.send_request(
|
||||||
types.ReadResourceRequest(params=types.ReadResourceRequestParams(uri=str(uri))),
|
types.ReadResourceRequest(params=types.ReadResourceRequestParams(uri=uri, _meta=meta)),
|
||||||
types.ReadResourceResult,
|
types.ReadResourceResult,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def subscribe_resource(self, uri: str | AnyUrl) -> types.EmptyResult:
|
async def subscribe_resource(self, uri: str, *, meta: RequestParamsMeta | None = None) -> types.EmptyResult:
|
||||||
"""Send a resources/subscribe request."""
|
"""Send a resources/subscribe request."""
|
||||||
return await self.send_request( # pragma: no cover
|
return await self.send_request( # pragma: no cover
|
||||||
types.SubscribeRequest(params=types.SubscribeRequestParams(uri=str(uri))),
|
types.SubscribeRequest(params=types.SubscribeRequestParams(uri=uri, _meta=meta)),
|
||||||
types.EmptyResult,
|
types.EmptyResult,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def unsubscribe_resource(self, uri: str | AnyUrl) -> types.EmptyResult:
|
async def unsubscribe_resource(self, uri: str, *, meta: RequestParamsMeta | None = None) -> types.EmptyResult:
|
||||||
"""Send a resources/unsubscribe request."""
|
"""Send a resources/unsubscribe request."""
|
||||||
return await self.send_request( # pragma: no cover
|
return await self.send_request( # pragma: no cover
|
||||||
types.UnsubscribeRequest(params=types.UnsubscribeRequestParams(uri=str(uri))),
|
types.UnsubscribeRequest(params=types.UnsubscribeRequestParams(uri=uri, _meta=meta)),
|
||||||
types.EmptyResult,
|
types.EmptyResult,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -295,17 +304,13 @@ class ClientSession(
|
||||||
read_timeout_seconds: float | None = None,
|
read_timeout_seconds: float | None = None,
|
||||||
progress_callback: ProgressFnT | None = None,
|
progress_callback: ProgressFnT | None = None,
|
||||||
*,
|
*,
|
||||||
meta: dict[str, Any] | None = None,
|
meta: RequestParamsMeta | None = None,
|
||||||
) -> types.CallToolResult:
|
) -> types.CallToolResult:
|
||||||
"""Send a tools/call request with optional progress callback support."""
|
"""Send a tools/call request with optional progress callback support."""
|
||||||
|
|
||||||
_meta: types.RequestParams.Meta | None = None
|
|
||||||
if meta is not None:
|
|
||||||
_meta = types.RequestParams.Meta(**meta)
|
|
||||||
|
|
||||||
result = await self.send_request(
|
result = await self.send_request(
|
||||||
types.CallToolRequest(
|
types.CallToolRequest(
|
||||||
params=types.CallToolRequestParams(name=name, arguments=arguments, _meta=_meta),
|
params=types.CallToolRequestParams(name=name, arguments=arguments, _meta=meta),
|
||||||
),
|
),
|
||||||
types.CallToolResult,
|
types.CallToolResult,
|
||||||
request_read_timeout_seconds=read_timeout_seconds,
|
request_read_timeout_seconds=read_timeout_seconds,
|
||||||
|
|
@ -351,11 +356,17 @@ class ClientSession(
|
||||||
"""
|
"""
|
||||||
return await self.send_request(types.ListPromptsRequest(params=params), types.ListPromptsResult)
|
return await self.send_request(types.ListPromptsRequest(params=params), types.ListPromptsResult)
|
||||||
|
|
||||||
async def get_prompt(self, name: str, arguments: dict[str, str] | None = None) -> types.GetPromptResult:
|
async def get_prompt(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
arguments: dict[str, str] | None = None,
|
||||||
|
*,
|
||||||
|
meta: RequestParamsMeta | None = None,
|
||||||
|
) -> types.GetPromptResult:
|
||||||
"""Send a prompts/get request."""
|
"""Send a prompts/get request."""
|
||||||
return await self.send_request(
|
return await self.send_request(
|
||||||
types.GetPromptRequest(
|
types.GetPromptRequest(
|
||||||
params=types.GetPromptRequestParams(name=name, arguments=arguments),
|
params=types.GetPromptRequestParams(name=name, arguments=arguments, _meta=meta),
|
||||||
),
|
),
|
||||||
types.GetPromptResult,
|
types.GetPromptResult,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -196,7 +196,7 @@ class ClientSessionGroup:
|
||||||
read_timeout_seconds: float | None = None,
|
read_timeout_seconds: float | None = None,
|
||||||
progress_callback: ProgressFnT | None = None,
|
progress_callback: ProgressFnT | None = None,
|
||||||
*,
|
*,
|
||||||
meta: dict[str, Any] | None = None,
|
meta: types.RequestParamsMeta | None = None,
|
||||||
) -> types.CallToolResult:
|
) -> types.CallToolResult:
|
||||||
"""Executes a tool given its name and arguments."""
|
"""Executes a tool given its name and arguments."""
|
||||||
session = self._tool_to_session[name]
|
session = self._tool_to_session[name]
|
||||||
|
|
|
||||||
|
|
@ -1049,7 +1049,7 @@ class Context(BaseModel, Generic[ServerSessionT, LifespanContextT, RequestT]):
|
||||||
total: Optional total value e.g. 100
|
total: Optional total value e.g. 100
|
||||||
message: Optional message e.g. Starting render...
|
message: Optional message e.g. Starting render...
|
||||||
"""
|
"""
|
||||||
progress_token = self.request_context.meta.progress_token if self.request_context.meta else None
|
progress_token = self.request_context.meta.get("progress_token") if self.request_context.meta else None
|
||||||
|
|
||||||
if progress_token is None: # pragma: no cover
|
if progress_token is None: # pragma: no cover
|
||||||
return
|
return
|
||||||
|
|
@ -1174,9 +1174,7 @@ class Context(BaseModel, Generic[ServerSessionT, LifespanContextT, RequestT]):
|
||||||
@property
|
@property
|
||||||
def client_id(self) -> str | None:
|
def client_id(self) -> str | None:
|
||||||
"""Get the client ID if available."""
|
"""Get the client ID if available."""
|
||||||
return (
|
return self.request_context.meta.get("client_id") if self.request_context.meta else None # pragma: no cover
|
||||||
getattr(self.request_context.meta, "client_id", None) if self.request_context.meta else None
|
|
||||||
) # pragma: no cover
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def request_id(self) -> str:
|
def request_id(self) -> str:
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ from typing_extensions import TypeVar
|
||||||
|
|
||||||
from mcp.shared.message import CloseSSEStreamCallback
|
from mcp.shared.message import CloseSSEStreamCallback
|
||||||
from mcp.shared.session import BaseSession
|
from mcp.shared.session import BaseSession
|
||||||
from mcp.types import RequestId, RequestParams
|
from mcp.types import RequestId, RequestParamsMeta
|
||||||
|
|
||||||
SessionT = TypeVar("SessionT", bound=BaseSession[Any, Any, Any, Any, Any])
|
SessionT = TypeVar("SessionT", bound=BaseSession[Any, Any, Any, Any, Any])
|
||||||
LifespanContextT = TypeVar("LifespanContextT")
|
LifespanContextT = TypeVar("LifespanContextT")
|
||||||
|
|
@ -17,7 +17,7 @@ RequestT = TypeVar("RequestT", default=Any)
|
||||||
@dataclass
|
@dataclass
|
||||||
class RequestContext(Generic[SessionT, LifespanContextT, RequestT]):
|
class RequestContext(Generic[SessionT, LifespanContextT, RequestT]):
|
||||||
request_id: RequestId
|
request_id: RequestId
|
||||||
meta: RequestParams.Meta | None
|
meta: RequestParamsMeta | None
|
||||||
session: SessionT
|
session: SessionT
|
||||||
lifespan_context: LifespanContextT
|
lifespan_context: LifespanContextT
|
||||||
# NOTE: This is typed as Any to avoid circular imports. The actual type is
|
# NOTE: This is typed as Any to avoid circular imports. The actual type is
|
||||||
|
|
|
||||||
|
|
@ -48,10 +48,11 @@ def progress(
|
||||||
ProgressContext[SendRequestT, SendNotificationT, SendResultT, ReceiveRequestT, ReceiveNotificationT],
|
ProgressContext[SendRequestT, SendNotificationT, SendResultT, ReceiveRequestT, ReceiveNotificationT],
|
||||||
None,
|
None,
|
||||||
]:
|
]:
|
||||||
if ctx.meta is None or ctx.meta.progress_token is None: # pragma: no cover
|
progress_token = ctx.meta.get("progress_token") if ctx.meta else None
|
||||||
|
if progress_token is None: # pragma: no cover
|
||||||
raise ValueError("No progress token provided")
|
raise ValueError("No progress token provided")
|
||||||
|
|
||||||
progress_ctx = ProgressContext(ctx.session, ctx.meta.progress_token, total)
|
progress_ctx = ProgressContext(ctx.session, progress_token, total)
|
||||||
try:
|
try:
|
||||||
yield progress_ctx
|
yield progress_ctx
|
||||||
finally:
|
finally:
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,8 @@ from mcp.types import (
|
||||||
JSONRPCRequest,
|
JSONRPCRequest,
|
||||||
JSONRPCResponse,
|
JSONRPCResponse,
|
||||||
ProgressNotification,
|
ProgressNotification,
|
||||||
RequestParams,
|
ProgressToken,
|
||||||
|
RequestParamsMeta,
|
||||||
ServerNotification,
|
ServerNotification,
|
||||||
ServerRequest,
|
ServerRequest,
|
||||||
ServerResult,
|
ServerResult,
|
||||||
|
|
@ -71,7 +72,7 @@ class RequestResponder(Generic[ReceiveRequestT, SendResultT]):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
request_id: RequestId,
|
request_id: RequestId,
|
||||||
request_meta: RequestParams.Meta | None,
|
request_meta: RequestParamsMeta | None,
|
||||||
request: ReceiveRequestT,
|
request: ReceiveRequestT,
|
||||||
session: BaseSession[SendRequestT, SendNotificationT, SendResultT, ReceiveRequestT, ReceiveNotificationT],
|
session: BaseSession[SendRequestT, SendNotificationT, SendResultT, ReceiveRequestT, ReceiveNotificationT],
|
||||||
on_complete: Callable[[RequestResponder[ReceiveRequestT, SendResultT]], Any],
|
on_complete: Callable[[RequestResponder[ReceiveRequestT, SendResultT]], Any],
|
||||||
|
|
@ -501,7 +502,7 @@ class BaseSession(
|
||||||
|
|
||||||
async def send_progress_notification(
|
async def send_progress_notification(
|
||||||
self,
|
self,
|
||||||
progress_token: str | int,
|
progress_token: ProgressToken,
|
||||||
progress: float,
|
progress: float,
|
||||||
total: float | None = None,
|
total: float | None = None,
|
||||||
message: str | None = None,
|
message: str | None = None,
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,6 @@ from mcp.types._types import (
|
||||||
LoggingLevel,
|
LoggingLevel,
|
||||||
LoggingMessageNotification,
|
LoggingMessageNotification,
|
||||||
LoggingMessageNotificationParams,
|
LoggingMessageNotificationParams,
|
||||||
MCPModel,
|
|
||||||
MethodT,
|
MethodT,
|
||||||
ModelHint,
|
ModelHint,
|
||||||
ModelPreferences,
|
ModelPreferences,
|
||||||
|
|
@ -116,6 +115,7 @@ from mcp.types._types import (
|
||||||
RelatedTaskMetadata,
|
RelatedTaskMetadata,
|
||||||
Request,
|
Request,
|
||||||
RequestParams,
|
RequestParams,
|
||||||
|
RequestParamsMeta,
|
||||||
RequestParamsT,
|
RequestParamsT,
|
||||||
Resource,
|
Resource,
|
||||||
ResourceContents,
|
ResourceContents,
|
||||||
|
|
@ -235,12 +235,12 @@ __all__ = [
|
||||||
"TaskExecutionMode",
|
"TaskExecutionMode",
|
||||||
"TaskStatus",
|
"TaskStatus",
|
||||||
# Base classes
|
# Base classes
|
||||||
"MCPModel",
|
|
||||||
"BaseMetadata",
|
"BaseMetadata",
|
||||||
"Request",
|
"Request",
|
||||||
"Notification",
|
"Notification",
|
||||||
"Result",
|
"Result",
|
||||||
"RequestParams",
|
"RequestParams",
|
||||||
|
"RequestParamsMeta",
|
||||||
"NotificationParams",
|
"NotificationParams",
|
||||||
"PaginatedRequest",
|
"PaginatedRequest",
|
||||||
"PaginatedRequestParams",
|
"PaginatedRequestParams",
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ from typing import Annotated, Any, Final, Generic, Literal, TypeAlias, TypeVar
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict, Field, FileUrl, TypeAdapter
|
from pydantic import BaseModel, ConfigDict, Field, FileUrl, TypeAdapter
|
||||||
from pydantic.alias_generators import to_camel
|
from pydantic.alias_generators import to_camel
|
||||||
|
from typing_extensions import NotRequired, TypedDict
|
||||||
|
|
||||||
from mcp.types.jsonrpc import RequestId
|
from mcp.types.jsonrpc import RequestId
|
||||||
|
|
||||||
|
|
@ -35,6 +36,19 @@ class MCPModel(BaseModel):
|
||||||
model_config = ConfigDict(extra="allow", alias_generator=to_camel, populate_by_name=True)
|
model_config = ConfigDict(extra="allow", alias_generator=to_camel, populate_by_name=True)
|
||||||
|
|
||||||
|
|
||||||
|
Meta: TypeAlias = dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
|
class RequestParamsMeta(TypedDict, extra_items=Any):
|
||||||
|
progress_token: NotRequired[ProgressToken]
|
||||||
|
"""
|
||||||
|
If specified, the caller requests out-of-band progress notifications for
|
||||||
|
this request (as represented by notifications/progress). The value of this
|
||||||
|
parameter is an opaque token that will be attached to any subsequent
|
||||||
|
notifications. The receiver is not obligated to provide these notifications.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class TaskMetadata(MCPModel):
|
class TaskMetadata(MCPModel):
|
||||||
"""Metadata for augmenting a request with task execution.
|
"""Metadata for augmenting a request with task execution.
|
||||||
Include this in the `task` field of the request parameters.
|
Include this in the `task` field of the request parameters.
|
||||||
|
|
@ -45,15 +59,6 @@ class TaskMetadata(MCPModel):
|
||||||
|
|
||||||
|
|
||||||
class RequestParams(MCPModel):
|
class RequestParams(MCPModel):
|
||||||
class Meta(MCPModel):
|
|
||||||
progress_token: ProgressToken | None = None
|
|
||||||
"""
|
|
||||||
If specified, the caller requests out-of-band progress notifications for
|
|
||||||
this request (as represented by notifications/progress). The value of this
|
|
||||||
parameter is an opaque token that will be attached to any subsequent
|
|
||||||
notifications. The receiver is not obligated to provide these notifications.
|
|
||||||
"""
|
|
||||||
|
|
||||||
task: TaskMetadata | None = None
|
task: TaskMetadata | None = None
|
||||||
"""
|
"""
|
||||||
If specified, the caller is requesting task-augmented execution for this request.
|
If specified, the caller is requesting task-augmented execution for this request.
|
||||||
|
|
@ -64,7 +69,7 @@ class RequestParams(MCPModel):
|
||||||
for task augmentation of specific request types in their capabilities.
|
for task augmentation of specific request types in their capabilities.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
meta: Meta | None = Field(alias="_meta", default=None)
|
meta: RequestParamsMeta | None = Field(alias="_meta", default=None)
|
||||||
|
|
||||||
|
|
||||||
class PaginatedRequestParams(RequestParams):
|
class PaginatedRequestParams(RequestParams):
|
||||||
|
|
@ -76,9 +81,6 @@ class PaginatedRequestParams(RequestParams):
|
||||||
|
|
||||||
|
|
||||||
class NotificationParams(MCPModel):
|
class NotificationParams(MCPModel):
|
||||||
class Meta(MCPModel):
|
|
||||||
pass
|
|
||||||
|
|
||||||
meta: Meta | None = Field(alias="_meta", default=None)
|
meta: Meta | None = Field(alias="_meta", default=None)
|
||||||
"""
|
"""
|
||||||
See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
|
See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
|
||||||
|
|
@ -115,7 +117,7 @@ class Notification(MCPModel, Generic[NotificationParamsT, MethodT]):
|
||||||
class Result(MCPModel):
|
class Result(MCPModel):
|
||||||
"""Base class for JSON-RPC results."""
|
"""Base class for JSON-RPC results."""
|
||||||
|
|
||||||
meta: dict[str, Any] | None = Field(alias="_meta", default=None)
|
meta: Meta | None = Field(alias="_meta", default=None)
|
||||||
"""
|
"""
|
||||||
See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
|
See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
|
||||||
for notes on _meta usage.
|
for notes on _meta usage.
|
||||||
|
|
@ -624,7 +626,7 @@ class Resource(BaseMetadata):
|
||||||
icons: list[Icon] | None = None
|
icons: list[Icon] | None = None
|
||||||
"""An optional list of icons for this resource."""
|
"""An optional list of icons for this resource."""
|
||||||
annotations: Annotations | None = None
|
annotations: Annotations | None = None
|
||||||
meta: dict[str, Any] | None = Field(alias="_meta", default=None)
|
meta: Meta | None = Field(alias="_meta", default=None)
|
||||||
"""
|
"""
|
||||||
See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
|
See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
|
||||||
for notes on _meta usage.
|
for notes on _meta usage.
|
||||||
|
|
@ -649,7 +651,7 @@ class ResourceTemplate(BaseMetadata):
|
||||||
icons: list[Icon] | None = None
|
icons: list[Icon] | None = None
|
||||||
"""An optional list of icons for this resource template."""
|
"""An optional list of icons for this resource template."""
|
||||||
annotations: Annotations | None = None
|
annotations: Annotations | None = None
|
||||||
meta: dict[str, Any] | None = Field(alias="_meta", default=None)
|
meta: Meta | None = Field(alias="_meta", default=None)
|
||||||
"""
|
"""
|
||||||
See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
|
See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
|
||||||
for notes on _meta usage.
|
for notes on _meta usage.
|
||||||
|
|
@ -698,7 +700,7 @@ class ResourceContents(MCPModel):
|
||||||
"""The URI of this resource."""
|
"""The URI of this resource."""
|
||||||
mime_type: str | None = None
|
mime_type: str | None = None
|
||||||
"""The MIME type of this resource, if known."""
|
"""The MIME type of this resource, if known."""
|
||||||
meta: dict[str, Any] | None = Field(alias="_meta", default=None)
|
meta: Meta | None = Field(alias="_meta", default=None)
|
||||||
"""
|
"""
|
||||||
See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
|
See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
|
||||||
for notes on _meta usage.
|
for notes on _meta usage.
|
||||||
|
|
@ -821,7 +823,7 @@ class Prompt(BaseMetadata):
|
||||||
"""A list of arguments to use for templating the prompt."""
|
"""A list of arguments to use for templating the prompt."""
|
||||||
icons: list[Icon] | None = None
|
icons: list[Icon] | None = None
|
||||||
"""An optional list of icons for this prompt."""
|
"""An optional list of icons for this prompt."""
|
||||||
meta: dict[str, Any] | None = Field(alias="_meta", default=None)
|
meta: Meta | None = Field(alias="_meta", default=None)
|
||||||
"""
|
"""
|
||||||
See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
|
See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
|
||||||
for notes on _meta usage.
|
for notes on _meta usage.
|
||||||
|
|
@ -857,7 +859,7 @@ class TextContent(MCPModel):
|
||||||
text: str
|
text: str
|
||||||
"""The text content of the message."""
|
"""The text content of the message."""
|
||||||
annotations: Annotations | None = None
|
annotations: Annotations | None = None
|
||||||
meta: dict[str, Any] | None = Field(alias="_meta", default=None)
|
meta: Meta | None = Field(alias="_meta", default=None)
|
||||||
"""
|
"""
|
||||||
See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
|
See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
|
||||||
for notes on _meta usage.
|
for notes on _meta usage.
|
||||||
|
|
@ -876,7 +878,7 @@ class ImageContent(MCPModel):
|
||||||
image types.
|
image types.
|
||||||
"""
|
"""
|
||||||
annotations: Annotations | None = None
|
annotations: Annotations | None = None
|
||||||
meta: dict[str, Any] | None = Field(alias="_meta", default=None)
|
meta: Meta | None = Field(alias="_meta", default=None)
|
||||||
"""
|
"""
|
||||||
See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
|
See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
|
||||||
for notes on _meta usage.
|
for notes on _meta usage.
|
||||||
|
|
@ -895,7 +897,7 @@ class AudioContent(MCPModel):
|
||||||
audio types.
|
audio types.
|
||||||
"""
|
"""
|
||||||
annotations: Annotations | None = None
|
annotations: Annotations | None = None
|
||||||
meta: dict[str, Any] | None = Field(alias="_meta", default=None)
|
meta: Meta | None = Field(alias="_meta", default=None)
|
||||||
"""
|
"""
|
||||||
See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
|
See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
|
||||||
for notes on _meta usage.
|
for notes on _meta usage.
|
||||||
|
|
@ -922,7 +924,7 @@ class ToolUseContent(MCPModel):
|
||||||
input: dict[str, Any]
|
input: dict[str, Any]
|
||||||
"""Arguments to pass to the tool. Must conform to the tool's inputSchema."""
|
"""Arguments to pass to the tool. Must conform to the tool's inputSchema."""
|
||||||
|
|
||||||
meta: dict[str, Any] | None = Field(alias="_meta", default=None)
|
meta: Meta | None = Field(alias="_meta", default=None)
|
||||||
"""
|
"""
|
||||||
See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
|
See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
|
||||||
for notes on _meta usage.
|
for notes on _meta usage.
|
||||||
|
|
@ -956,7 +958,7 @@ class ToolResultContent(MCPModel):
|
||||||
is_error: bool | None = None
|
is_error: bool | None = None
|
||||||
"""Whether the tool execution resulted in an error."""
|
"""Whether the tool execution resulted in an error."""
|
||||||
|
|
||||||
meta: dict[str, Any] | None = Field(alias="_meta", default=None)
|
meta: Meta | None = Field(alias="_meta", default=None)
|
||||||
"""
|
"""
|
||||||
See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
|
See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
|
||||||
for notes on _meta usage.
|
for notes on _meta usage.
|
||||||
|
|
@ -980,7 +982,7 @@ class SamplingMessage(MCPModel):
|
||||||
Message content. Can be a single content block or an array of content blocks
|
Message content. Can be a single content block or an array of content blocks
|
||||||
for multi-modal messages and tool interactions.
|
for multi-modal messages and tool interactions.
|
||||||
"""
|
"""
|
||||||
meta: dict[str, Any] | None = Field(alias="_meta", default=None)
|
meta: Meta | None = Field(alias="_meta", default=None)
|
||||||
"""
|
"""
|
||||||
See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
|
See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
|
||||||
for notes on _meta usage.
|
for notes on _meta usage.
|
||||||
|
|
@ -1003,7 +1005,7 @@ class EmbeddedResource(MCPModel):
|
||||||
type: Literal["resource"] = "resource"
|
type: Literal["resource"] = "resource"
|
||||||
resource: TextResourceContents | BlobResourceContents
|
resource: TextResourceContents | BlobResourceContents
|
||||||
annotations: Annotations | None = None
|
annotations: Annotations | None = None
|
||||||
meta: dict[str, Any] | None = Field(alias="_meta", default=None)
|
meta: Meta | None = Field(alias="_meta", default=None)
|
||||||
"""
|
"""
|
||||||
See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
|
See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
|
||||||
for notes on _meta usage.
|
for notes on _meta usage.
|
||||||
|
|
@ -1134,7 +1136,7 @@ class Tool(BaseMetadata):
|
||||||
"""An optional list of icons for this tool."""
|
"""An optional list of icons for this tool."""
|
||||||
annotations: ToolAnnotations | None = None
|
annotations: ToolAnnotations | None = None
|
||||||
"""Optional additional tool information."""
|
"""Optional additional tool information."""
|
||||||
meta: dict[str, Any] | None = Field(alias="_meta", default=None)
|
meta: Meta | None = Field(alias="_meta", default=None)
|
||||||
"""
|
"""
|
||||||
See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
|
See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
|
||||||
for notes on _meta usage.
|
for notes on _meta usage.
|
||||||
|
|
@ -1482,7 +1484,7 @@ class Root(MCPModel):
|
||||||
identifier for the root, which may be useful for display purposes or for
|
identifier for the root, which may be useful for display purposes or for
|
||||||
referencing the root in other parts of the application.
|
referencing the root in other parts of the application.
|
||||||
"""
|
"""
|
||||||
meta: dict[str, Any] | None = Field(alias="_meta", default=None)
|
meta: Meta | None = Field(alias="_meta", default=None)
|
||||||
"""
|
"""
|
||||||
See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
|
See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
|
||||||
for notes on _meta usage.
|
for notes on _meta usage.
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ from mcp.types import (
|
||||||
JSONRPCNotification,
|
JSONRPCNotification,
|
||||||
JSONRPCRequest,
|
JSONRPCRequest,
|
||||||
JSONRPCResponse,
|
JSONRPCResponse,
|
||||||
|
RequestParamsMeta,
|
||||||
ServerCapabilities,
|
ServerCapabilities,
|
||||||
TextContent,
|
TextContent,
|
||||||
client_notification_adapter,
|
client_notification_adapter,
|
||||||
|
|
@ -608,7 +609,7 @@ async def test_get_server_capabilities():
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
@pytest.mark.parametrize(argnames="meta", argvalues=[None, {"toolMeta": "value"}])
|
@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"""
|
"""Test that client tool call requests can include metadata"""
|
||||||
client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[SessionMessage](1)
|
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)
|
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)
|
ctx.experimental.validate_task_mode(TASK_REQUIRED)
|
||||||
|
|
||||||
# Capture the meta from the request (if present)
|
# Capture the meta from the request (if present)
|
||||||
if ctx.meta is not None and ctx.meta.model_extra: # pragma: no branch
|
if ctx.meta is not None: # pragma: no branch
|
||||||
received_meta[0] = ctx.meta.model_extra.get("custom_field")
|
received_meta[0] = ctx.meta.get("custom_field")
|
||||||
|
|
||||||
async def work(task: ServerTaskContext) -> CallToolResult:
|
async def work(task: ServerTaskContext) -> CallToolResult:
|
||||||
await task.update_status("Working...")
|
await task.update_status("Working...")
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import pytest
|
import pytest
|
||||||
from pydantic import AnyUrl
|
|
||||||
|
|
||||||
from mcp import Client
|
from mcp import Client
|
||||||
from mcp.server.fastmcp import FastMCP
|
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
|
assert "resource://users/{user_id}/profile" in templates
|
||||||
|
|
||||||
# Read a resource with valid parameters
|
# 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]
|
contents = result.contents[0]
|
||||||
assert isinstance(contents, TextResourceContents)
|
assert isinstance(contents, TextResourceContents)
|
||||||
assert contents.text == "Post 456 by user 123"
|
assert contents.text == "Post 456 by user 123"
|
||||||
assert contents.mime_type == "text/plain"
|
assert contents.mime_type == "text/plain"
|
||||||
|
|
||||||
# Read another resource with valid parameters
|
# 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]
|
contents = result.contents[0]
|
||||||
assert isinstance(contents, TextResourceContents)
|
assert isinstance(contents, TextResourceContents)
|
||||||
assert contents.text == "Profile for user 789"
|
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
|
# Verify invalid resource URIs raise appropriate errors
|
||||||
with pytest.raises(Exception): # Specific exception type may vary
|
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
|
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 base64
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from pydantic import AnyUrl
|
|
||||||
|
|
||||||
from mcp import Client, types
|
from mcp import Client, types
|
||||||
from mcp.server.fastmcp import FastMCP
|
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"
|
assert bytes_resource.mime_type == "image/png", "Bytes resource mime type not respected"
|
||||||
|
|
||||||
# Also verify the content can be read correctly
|
# 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 len(string_result.contents) == 1
|
||||||
assert getattr(string_result.contents[0], "text") == base64_string, "Base64 string mismatch"
|
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"
|
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 len(bytes_result.contents) == 1
|
||||||
assert base64.b64decode(getattr(bytes_result.contents[0], "blob")) == image_bytes, "Bytes mismatch"
|
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"
|
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"
|
assert bytes_resource.mime_type == "image/png", "Bytes resource mime type not respected"
|
||||||
|
|
||||||
# Also verify the content can be read correctly
|
# 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 len(string_result.contents) == 1
|
||||||
assert getattr(string_result.contents[0], "text") == base64_string, "Base64 string mismatch"
|
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"
|
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 len(bytes_result.contents) == 1
|
||||||
assert base64.b64decode(getattr(bytes_result.contents[0], "blob")) == image_bytes, "Bytes mismatch"
|
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"
|
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
|
import pytest
|
||||||
from pydantic import AnyUrl
|
|
||||||
|
|
||||||
from mcp import Client
|
from mcp import Client
|
||||||
from mcp.server.fastmcp import FastMCP
|
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:
|
async with Client(mcp) as client:
|
||||||
# Read the resource
|
# 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 len(result.contents) == 1
|
||||||
assert result.contents[0].mime_type == "text/html;profile=mcp-app"
|
assert result.contents[0].mime_type == "text/html;profile=mcp-app"
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue