refactor: use snake case instead of camel case in types (#1894)
This commit is contained in:
committed by
GitHub
parent
7080fcf1e6
commit
e94b386a13
@@ -442,7 +442,7 @@ def validated_tool() -> Annotated[CallToolResult, ValidationModel]:
|
||||
"""Return CallToolResult with structured output validation."""
|
||||
return CallToolResult(
|
||||
content=[TextContent(type="text", text="Validated response")],
|
||||
structuredContent={"status": "success", "data": {"result": 42}},
|
||||
structured_content={"status": "success", "data": {"result": 42}},
|
||||
_meta={"internal": "metadata"},
|
||||
)
|
||||
|
||||
@@ -757,8 +757,8 @@ async def run():
|
||||
# List available resource templates
|
||||
templates = await session.list_resource_templates()
|
||||
print("Available resource templates:")
|
||||
for template in templates.resourceTemplates:
|
||||
print(f" - {template.uriTemplate}")
|
||||
for template in templates.resource_templates:
|
||||
print(f" - {template.uri_template}")
|
||||
|
||||
# List available prompts
|
||||
prompts = await session.list_prompts()
|
||||
@@ -767,20 +767,20 @@ async def run():
|
||||
print(f" - {prompt.name}")
|
||||
|
||||
# Complete resource template arguments
|
||||
if templates.resourceTemplates:
|
||||
template = templates.resourceTemplates[0]
|
||||
print(f"\nCompleting arguments for resource template: {template.uriTemplate}")
|
||||
if templates.resource_templates:
|
||||
template = templates.resource_templates[0]
|
||||
print(f"\nCompleting arguments for resource template: {template.uri_template}")
|
||||
|
||||
# Complete without context
|
||||
result = await session.complete(
|
||||
ref=ResourceTemplateReference(type="ref/resource", uri=template.uriTemplate),
|
||||
ref=ResourceTemplateReference(type="ref/resource", uri=template.uri_template),
|
||||
argument={"name": "owner", "value": "model"},
|
||||
)
|
||||
print(f"Completions for 'owner' starting with 'model': {result.completion.values}")
|
||||
|
||||
# Complete with context - repo suggestions based on owner
|
||||
result = await session.complete(
|
||||
ref=ResourceTemplateReference(type="ref/resource", uri=template.uriTemplate),
|
||||
ref=ResourceTemplateReference(type="ref/resource", uri=template.uri_template),
|
||||
argument={"name": "repo", "value": ""},
|
||||
context_arguments={"owner": "modelcontextprotocol"},
|
||||
)
|
||||
@@ -910,7 +910,7 @@ async def connect_service(service_name: str, ctx: Context[ServerSession, None])
|
||||
mode="url",
|
||||
message=f"Authorization required to connect to {service_name}",
|
||||
url=f"https://{service_name}.example.com/oauth/authorize?elicit={elicitation_id}",
|
||||
elicitationId=elicitation_id,
|
||||
elicitation_id=elicitation_id,
|
||||
)
|
||||
]
|
||||
)
|
||||
@@ -1706,7 +1706,7 @@ async def handle_list_tools() -> list[types.Tool]:
|
||||
types.Tool(
|
||||
name="query_db",
|
||||
description="Query the database",
|
||||
inputSchema={
|
||||
input_schema={
|
||||
"type": "object",
|
||||
"properties": {"query": {"type": "string", "description": "SQL query to execute"}},
|
||||
"required": ["query"],
|
||||
@@ -1867,12 +1867,12 @@ async def list_tools() -> list[types.Tool]:
|
||||
types.Tool(
|
||||
name="get_weather",
|
||||
description="Get current weather for a city",
|
||||
inputSchema={
|
||||
input_schema={
|
||||
"type": "object",
|
||||
"properties": {"city": {"type": "string", "description": "City name"}},
|
||||
"required": ["city"],
|
||||
},
|
||||
outputSchema={
|
||||
output_schema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"temperature": {"type": "number", "description": "Temperature in Celsius"},
|
||||
@@ -1970,7 +1970,7 @@ async def list_tools() -> list[types.Tool]:
|
||||
types.Tool(
|
||||
name="advanced_tool",
|
||||
description="Tool with full control including _meta field",
|
||||
inputSchema={
|
||||
input_schema={
|
||||
"type": "object",
|
||||
"properties": {"message": {"type": "string"}},
|
||||
"required": ["message"],
|
||||
@@ -1986,7 +1986,7 @@ async def handle_call_tool(name: str, arguments: dict[str, Any]) -> types.CallTo
|
||||
message = str(arguments.get("message", ""))
|
||||
return types.CallToolResult(
|
||||
content=[types.TextContent(type="text", text=f"Processed: {message}")],
|
||||
structuredContent={"result": "success", "message": message},
|
||||
structured_content={"result": "success", "message": message},
|
||||
_meta={"hidden": "data for client applications only"},
|
||||
)
|
||||
|
||||
@@ -2062,7 +2062,7 @@ async def list_resources_paginated(request: types.ListResourcesRequest) -> types
|
||||
# Determine next cursor
|
||||
next_cursor = str(end) if end < len(ITEMS) else None
|
||||
|
||||
return types.ListResourcesResult(resources=page_items, nextCursor=next_cursor)
|
||||
return types.ListResourcesResult(resources=page_items, next_cursor=next_cursor)
|
||||
```
|
||||
|
||||
_Full example: [examples/snippets/servers/pagination_example.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/pagination_example.py)_
|
||||
@@ -2103,8 +2103,8 @@ async def list_all_resources() -> None:
|
||||
print(f"Fetched {len(result.resources)} resources")
|
||||
|
||||
# Check if there are more pages
|
||||
if result.nextCursor:
|
||||
cursor = result.nextCursor
|
||||
if result.next_cursor:
|
||||
cursor = result.next_cursor
|
||||
else:
|
||||
break
|
||||
|
||||
@@ -2167,7 +2167,7 @@ async def handle_sampling_message(
|
||||
text="Hello, world! from model",
|
||||
),
|
||||
model="gpt-3.5-turbo",
|
||||
stopReason="endTurn",
|
||||
stop_reason="endTurn",
|
||||
)
|
||||
|
||||
|
||||
@@ -2205,7 +2205,7 @@ async def run():
|
||||
result_unstructured = result.content[0]
|
||||
if isinstance(result_unstructured, types.TextContent):
|
||||
print(f"Tool result: {result_unstructured.text}")
|
||||
result_structured = result.structuredContent
|
||||
result_structured = result.structured_content
|
||||
print(f"Structured tool result: {result_structured}")
|
||||
|
||||
|
||||
@@ -2306,7 +2306,7 @@ async def display_resources(session: ClientSession):
|
||||
print(f"Resource: {display_name} ({resource.uri})")
|
||||
|
||||
templates_response = await session.list_resource_templates()
|
||||
for template in templates_response.resourceTemplates:
|
||||
for template in templates_response.resource_templates:
|
||||
display_name = get_display_name(template)
|
||||
print(f"Resource Template: {display_name}")
|
||||
|
||||
|
||||
@@ -112,7 +112,7 @@ class Server:
|
||||
|
||||
for item in tools_response:
|
||||
if item[0] == "tools":
|
||||
tools.extend(Tool(tool.name, tool.description, tool.inputSchema, tool.title) for tool in item[1])
|
||||
tools.extend(Tool(tool.name, tool.description, tool.input_schema, tool.title) for tool in item[1])
|
||||
|
||||
return tools
|
||||
|
||||
|
||||
@@ -25,13 +25,13 @@ async def run(url: str) -> None:
|
||||
arguments={},
|
||||
ttl=60000,
|
||||
)
|
||||
task_id = result.task.taskId
|
||||
task_id = result.task.task_id
|
||||
print(f"Task created: {task_id}")
|
||||
|
||||
status = None
|
||||
# Poll until done (respects server's pollInterval hint)
|
||||
async for status in session.experimental.poll_task(task_id):
|
||||
print(f" Status: {status.status} - {status.statusMessage or ''}")
|
||||
print(f" Status: {status.status} - {status.status_message or ''}")
|
||||
|
||||
# Check final status
|
||||
if status and status.status != "completed":
|
||||
|
||||
@@ -49,7 +49,7 @@ async def sampling_callback(context, params) -> CreateMessageResult:
|
||||
```python
|
||||
# Call a tool as a task (returns immediately with task reference)
|
||||
result = await session.experimental.call_tool_as_task("tool_name", {"arg": "value"})
|
||||
task_id = result.task.taskId
|
||||
task_id = result.task.task_id
|
||||
|
||||
# Get result - this delivers elicitation/sampling requests and blocks until complete
|
||||
final = await session.experimental.get_task_result(task_id, CallToolResult)
|
||||
|
||||
+2
-2
@@ -91,7 +91,7 @@ async def run(url: str) -> None:
|
||||
print("Calling confirm_delete tool...")
|
||||
|
||||
elicit_task = await session.experimental.call_tool_as_task("confirm_delete", {"filename": "important.txt"})
|
||||
elicit_task_id = elicit_task.task.taskId
|
||||
elicit_task_id = elicit_task.task.task_id
|
||||
print(f"Task created: {elicit_task_id}")
|
||||
|
||||
# Poll until terminal, calling tasks/result on input_required
|
||||
@@ -112,7 +112,7 @@ async def run(url: str) -> None:
|
||||
print("Calling write_haiku tool...")
|
||||
|
||||
sampling_task = await session.experimental.call_tool_as_task("write_haiku", {"topic": "autumn leaves"})
|
||||
sampling_task_id = sampling_task.task.taskId
|
||||
sampling_task_id = sampling_task.task.task_id
|
||||
print(f"Task created: {sampling_task_id}")
|
||||
|
||||
# Poll until terminal, calling tasks/result on input_required
|
||||
|
||||
@@ -20,5 +20,5 @@ class EchoResponse(BaseModel):
|
||||
def echo(text: str) -> Annotated[CallToolResult, EchoResponse]:
|
||||
"""Echo the input text with structure and metadata"""
|
||||
return CallToolResult(
|
||||
content=[TextContent(type="text", text=text)], structuredContent={"text": text}, _meta={"some": "metadata"}
|
||||
content=[TextContent(type="text", text=text)], structured_content={"text": text}, _meta={"some": "metadata"}
|
||||
)
|
||||
|
||||
@@ -14,7 +14,7 @@ icon_path = Path(__file__).parent / "mcp.png"
|
||||
icon_data = base64.standard_b64encode(icon_path.read_bytes()).decode()
|
||||
icon_data_uri = f"data:image/png;base64,{icon_data}"
|
||||
|
||||
icon_data = Icon(src=icon_data_uri, mimeType="image/png", sizes=["64x64"])
|
||||
icon_data = Icon(src=icon_data_uri, mime_type="image/png", sizes=["64x64"])
|
||||
|
||||
# Create server with icons in implementation
|
||||
mcp = FastMCP("Icons Demo Server", website_url="https://github.com/modelcontextprotocol/python-sdk", icons=[icon_data])
|
||||
@@ -40,9 +40,9 @@ def prompt_with_icon(text: str) -> str:
|
||||
|
||||
@mcp.tool(
|
||||
icons=[
|
||||
Icon(src=icon_data_uri, mimeType="image/png", sizes=["16x16"]),
|
||||
Icon(src=icon_data_uri, mimeType="image/png", sizes=["32x32"]),
|
||||
Icon(src=icon_data_uri, mimeType="image/png", sizes=["64x64"]),
|
||||
Icon(src=icon_data_uri, mime_type="image/png", sizes=["16x16"]),
|
||||
Icon(src=icon_data_uri, mime_type="image/png", sizes=["32x32"]),
|
||||
Icon(src=icon_data_uri, mime_type="image/png", sizes=["64x64"]),
|
||||
]
|
||||
)
|
||||
def multi_icon_tool(action: str) -> str:
|
||||
|
||||
@@ -161,32 +161,32 @@ if __name__ == "__main__":
|
||||
# Test get_weather
|
||||
result = await client.call_tool("get_weather", {"city": "London"})
|
||||
print("\nWeather in London:")
|
||||
print(json.dumps(result.structuredContent, indent=2))
|
||||
print(json.dumps(result.structured_content, indent=2))
|
||||
|
||||
# Test get_weather_summary
|
||||
result = await client.call_tool("get_weather_summary", {"city": "Paris"})
|
||||
print("\nWeather summary for Paris:")
|
||||
print(json.dumps(result.structuredContent, indent=2))
|
||||
print(json.dumps(result.structured_content, indent=2))
|
||||
|
||||
# Test get_weather_metrics
|
||||
result = await client.call_tool("get_weather_metrics", {"cities": ["Tokyo", "Sydney", "Mumbai"]})
|
||||
print("\nWeather metrics:")
|
||||
print(json.dumps(result.structuredContent, indent=2))
|
||||
print(json.dumps(result.structured_content, indent=2))
|
||||
|
||||
# Test get_weather_alerts
|
||||
result = await client.call_tool("get_weather_alerts", {"region": "California"})
|
||||
print("\nWeather alerts for California:")
|
||||
print(json.dumps(result.structuredContent, indent=2))
|
||||
print(json.dumps(result.structured_content, indent=2))
|
||||
|
||||
# Test get_temperature
|
||||
result = await client.call_tool("get_temperature", {"city": "Berlin", "unit": "fahrenheit"})
|
||||
print("\nTemperature in Berlin:")
|
||||
print(json.dumps(result.structuredContent, indent=2))
|
||||
print(json.dumps(result.structured_content, indent=2))
|
||||
|
||||
# Test get_weather_stats
|
||||
result = await client.call_tool("get_weather_stats", {"city": "Seattle", "days": 30})
|
||||
print("\nWeather stats for Seattle (30 days):")
|
||||
print(json.dumps(result.structuredContent, indent=2))
|
||||
print(json.dumps(result.structured_content, indent=2))
|
||||
|
||||
# Also show the text content for comparison
|
||||
print("\nText content for last result:")
|
||||
@@ -204,11 +204,11 @@ if __name__ == "__main__":
|
||||
print(f"\nTool: {tool.name}")
|
||||
print(f"Description: {tool.description}")
|
||||
print("Input Schema:")
|
||||
print(json.dumps(tool.inputSchema, indent=2))
|
||||
print(json.dumps(tool.input_schema, indent=2))
|
||||
|
||||
if tool.outputSchema:
|
||||
if tool.output_schema:
|
||||
print("Output Schema:")
|
||||
print(json.dumps(tool.outputSchema, indent=2))
|
||||
print(json.dumps(tool.output_schema, indent=2))
|
||||
else:
|
||||
print("Output Schema: None (returns unstructured content)")
|
||||
|
||||
|
||||
@@ -98,13 +98,13 @@ def test_simple_text() -> str:
|
||||
@mcp.tool()
|
||||
def test_image_content() -> list[ImageContent]:
|
||||
"""Tests image content response"""
|
||||
return [ImageContent(type="image", data=TEST_IMAGE_BASE64, mimeType="image/png")]
|
||||
return [ImageContent(type="image", data=TEST_IMAGE_BASE64, mime_type="image/png")]
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def test_audio_content() -> list[AudioContent]:
|
||||
"""Tests audio content response"""
|
||||
return [AudioContent(type="audio", data=TEST_AUDIO_BASE64, mimeType="audio/wav")]
|
||||
return [AudioContent(type="audio", data=TEST_AUDIO_BASE64, mime_type="audio/wav")]
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
@@ -115,7 +115,7 @@ def test_embedded_resource() -> list[EmbeddedResource]:
|
||||
type="resource",
|
||||
resource=TextResourceContents(
|
||||
uri="test://embedded-resource",
|
||||
mimeType="text/plain",
|
||||
mime_type="text/plain",
|
||||
text="This is an embedded resource content.",
|
||||
),
|
||||
)
|
||||
@@ -127,12 +127,12 @@ def test_multiple_content_types() -> list[TextContent | ImageContent | EmbeddedR
|
||||
"""Tests response with multiple content types (text, image, resource)"""
|
||||
return [
|
||||
TextContent(type="text", text="Multiple content types test:"),
|
||||
ImageContent(type="image", data=TEST_IMAGE_BASE64, mimeType="image/png"),
|
||||
ImageContent(type="image", data=TEST_IMAGE_BASE64, mime_type="image/png"),
|
||||
EmbeddedResource(
|
||||
type="resource",
|
||||
resource=TextResourceContents(
|
||||
uri="test://mixed-content-resource",
|
||||
mimeType="application/json",
|
||||
mime_type="application/json",
|
||||
text='{"test": "data", "value": 123}',
|
||||
),
|
||||
),
|
||||
@@ -164,7 +164,7 @@ 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")
|
||||
|
||||
# Return progress token as string
|
||||
progress_token = ctx.request_context.meta.progressToken if ctx.request_context and ctx.request_context.meta else 0
|
||||
progress_token = ctx.request_context.meta.progress_token if ctx.request_context and ctx.request_context.meta else 0
|
||||
return str(progress_token)
|
||||
|
||||
|
||||
@@ -373,7 +373,7 @@ def test_prompt_with_embedded_resource(resourceUri: str) -> list[UserMessage]:
|
||||
type="resource",
|
||||
resource=TextResourceContents(
|
||||
uri=resourceUri,
|
||||
mimeType="text/plain",
|
||||
mime_type="text/plain",
|
||||
text="Embedded resource content for testing.",
|
||||
),
|
||||
),
|
||||
@@ -386,7 +386,7 @@ def test_prompt_with_embedded_resource(resourceUri: str) -> list[UserMessage]:
|
||||
def test_prompt_with_image() -> list[UserMessage]:
|
||||
"""A prompt that includes image content"""
|
||||
return [
|
||||
UserMessage(role="user", content=ImageContent(type="image", data=TEST_IMAGE_BASE64, mimeType="image/png")),
|
||||
UserMessage(role="user", content=ImageContent(type="image", data=TEST_IMAGE_BASE64, mime_type="image/png")),
|
||||
UserMessage(role="user", content=TextContent(type="text", text="Please analyze the image above.")),
|
||||
]
|
||||
|
||||
@@ -427,7 +427,7 @@ async def _handle_completion(
|
||||
"""Handle completion requests"""
|
||||
# Basic completion support - returns empty array for conformance
|
||||
# Real implementations would provide contextual suggestions
|
||||
return Completion(values=[], total=0, hasMore=False)
|
||||
return Completion(values=[], total=0, has_more=False)
|
||||
|
||||
|
||||
# CLI
|
||||
|
||||
@@ -19,7 +19,7 @@ SAMPLE_TOOLS = [
|
||||
name=f"tool_{i}",
|
||||
title=f"Tool {i}",
|
||||
description=f"This is sample tool number {i}",
|
||||
inputSchema={"type": "object", "properties": {"input": {"type": "string"}}},
|
||||
input_schema={"type": "object", "properties": {"input": {"type": "string"}}},
|
||||
)
|
||||
for i in range(1, 26) # 25 tools total
|
||||
]
|
||||
@@ -71,7 +71,7 @@ def main(port: int, transport: str) -> int:
|
||||
start_idx = int(cursor)
|
||||
except (ValueError, TypeError):
|
||||
# Invalid cursor, return empty
|
||||
return types.ListToolsResult(tools=[], nextCursor=None)
|
||||
return types.ListToolsResult(tools=[], next_cursor=None)
|
||||
|
||||
# Get the page of tools
|
||||
page_tools = SAMPLE_TOOLS[start_idx : start_idx + page_size]
|
||||
@@ -81,7 +81,7 @@ def main(port: int, transport: str) -> int:
|
||||
if start_idx + page_size < len(SAMPLE_TOOLS):
|
||||
next_cursor = str(start_idx + page_size)
|
||||
|
||||
return types.ListToolsResult(tools=page_tools, nextCursor=next_cursor)
|
||||
return types.ListToolsResult(tools=page_tools, next_cursor=next_cursor)
|
||||
|
||||
# Paginated list_resources - returns 10 resources per page
|
||||
@app.list_resources()
|
||||
@@ -100,7 +100,7 @@ def main(port: int, transport: str) -> int:
|
||||
start_idx = int(cursor)
|
||||
except (ValueError, TypeError):
|
||||
# Invalid cursor, return empty
|
||||
return types.ListResourcesResult(resources=[], nextCursor=None)
|
||||
return types.ListResourcesResult(resources=[], next_cursor=None)
|
||||
|
||||
# Get the page of resources
|
||||
page_resources = SAMPLE_RESOURCES[start_idx : start_idx + page_size]
|
||||
@@ -110,7 +110,7 @@ def main(port: int, transport: str) -> int:
|
||||
if start_idx + page_size < len(SAMPLE_RESOURCES):
|
||||
next_cursor = str(start_idx + page_size)
|
||||
|
||||
return types.ListResourcesResult(resources=page_resources, nextCursor=next_cursor)
|
||||
return types.ListResourcesResult(resources=page_resources, next_cursor=next_cursor)
|
||||
|
||||
# Paginated list_prompts - returns 7 prompts per page
|
||||
@app.list_prompts()
|
||||
@@ -129,7 +129,7 @@ def main(port: int, transport: str) -> int:
|
||||
start_idx = int(cursor)
|
||||
except (ValueError, TypeError):
|
||||
# Invalid cursor, return empty
|
||||
return types.ListPromptsResult(prompts=[], nextCursor=None)
|
||||
return types.ListPromptsResult(prompts=[], next_cursor=None)
|
||||
|
||||
# Get the page of prompts
|
||||
page_prompts = SAMPLE_PROMPTS[start_idx : start_idx + page_size]
|
||||
@@ -139,7 +139,7 @@ def main(port: int, transport: str) -> int:
|
||||
if start_idx + page_size < len(SAMPLE_PROMPTS):
|
||||
next_cursor = str(start_idx + page_size)
|
||||
|
||||
return types.ListPromptsResult(prompts=page_prompts, nextCursor=next_cursor)
|
||||
return types.ListPromptsResult(prompts=page_prompts, next_cursor=next_cursor)
|
||||
|
||||
# Implement call_tool handler
|
||||
@app.call_tool()
|
||||
|
||||
@@ -40,7 +40,7 @@ def main(port: int, transport: str) -> int:
|
||||
name=name,
|
||||
title=SAMPLE_RESOURCES[name]["title"],
|
||||
description=f"A sample text resource named {name}",
|
||||
mimeType="text/plain",
|
||||
mime_type="text/plain",
|
||||
)
|
||||
for name in SAMPLE_RESOURCES.keys()
|
||||
]
|
||||
|
||||
+1
-1
@@ -73,7 +73,7 @@ def main(
|
||||
types.Tool(
|
||||
name="start-notification-stream",
|
||||
description=("Sends a stream of notifications with configurable count and interval"),
|
||||
inputSchema={
|
||||
input_schema={
|
||||
"type": "object",
|
||||
"required": ["interval", "count", "caller"],
|
||||
"properties": {
|
||||
|
||||
@@ -87,7 +87,7 @@ def main(
|
||||
types.Tool(
|
||||
name="start-notification-stream",
|
||||
description=("Sends a stream of notifications with configurable count and interval"),
|
||||
inputSchema={
|
||||
input_schema={
|
||||
"type": "object",
|
||||
"required": ["interval", "count", "caller"],
|
||||
"properties": {
|
||||
|
||||
@@ -31,17 +31,17 @@ async def list_tools() -> list[types.Tool]:
|
||||
types.Tool(
|
||||
name="confirm_delete",
|
||||
description="Asks for confirmation before deleting (demonstrates elicitation)",
|
||||
inputSchema={
|
||||
input_schema={
|
||||
"type": "object",
|
||||
"properties": {"filename": {"type": "string"}},
|
||||
},
|
||||
execution=types.ToolExecution(taskSupport=types.TASK_REQUIRED),
|
||||
execution=types.ToolExecution(task_support=types.TASK_REQUIRED),
|
||||
),
|
||||
types.Tool(
|
||||
name="write_haiku",
|
||||
description="Asks LLM to write a haiku (demonstrates sampling)",
|
||||
inputSchema={"type": "object", "properties": {"topic": {"type": "string"}}},
|
||||
execution=types.ToolExecution(taskSupport=types.TASK_REQUIRED),
|
||||
input_schema={"type": "object", "properties": {"topic": {"type": "string"}}},
|
||||
execution=types.ToolExecution(task_support=types.TASK_REQUIRED),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -59,7 +59,7 @@ async def handle_confirm_delete(arguments: dict[str, Any]) -> types.CreateTaskRe
|
||||
|
||||
result = await task.elicit(
|
||||
message=f"Are you sure you want to delete '{filename}'?",
|
||||
requestedSchema={
|
||||
requested_schema={
|
||||
"type": "object",
|
||||
"properties": {"confirm": {"type": "boolean"}},
|
||||
"required": ["confirm"],
|
||||
@@ -121,7 +121,7 @@ async def handle_call_tool(name: str, arguments: dict[str, Any]) -> types.CallTo
|
||||
else:
|
||||
return types.CallToolResult(
|
||||
content=[types.TextContent(type="text", text=f"Unknown tool: {name}")],
|
||||
isError=True,
|
||||
is_error=True,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -26,8 +26,8 @@ async def list_tools() -> list[types.Tool]:
|
||||
types.Tool(
|
||||
name="long_running_task",
|
||||
description="A task that takes a few seconds to complete with status updates",
|
||||
inputSchema={"type": "object", "properties": {}},
|
||||
execution=types.ToolExecution(taskSupport=types.TASK_REQUIRED),
|
||||
input_schema={"type": "object", "properties": {}},
|
||||
execution=types.ToolExecution(task_support=types.TASK_REQUIRED),
|
||||
)
|
||||
]
|
||||
|
||||
@@ -60,7 +60,7 @@ async def handle_call_tool(name: str, arguments: dict[str, Any]) -> types.CallTo
|
||||
else:
|
||||
return types.CallToolResult(
|
||||
content=[types.TextContent(type="text", text=f"Unknown tool: {name}")],
|
||||
isError=True,
|
||||
is_error=True,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ def main(port: int, transport: str) -> int:
|
||||
name="fetch",
|
||||
title="Website Fetcher",
|
||||
description="Fetches a website and returns its content",
|
||||
inputSchema={
|
||||
input_schema={
|
||||
"type": "object",
|
||||
"required": ["url"],
|
||||
"properties": {
|
||||
|
||||
@@ -120,7 +120,7 @@ def main(port: int, log_level: str, retry_interval: int) -> int:
|
||||
"Process a batch of items with periodic checkpoints. "
|
||||
"Demonstrates SSE polling where server closes stream periodically."
|
||||
),
|
||||
inputSchema={
|
||||
input_schema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"items": {
|
||||
|
||||
+2
-2
@@ -27,12 +27,12 @@ async def list_tools() -> list[types.Tool]:
|
||||
types.Tool(
|
||||
name="get_weather",
|
||||
description="Get weather information (simulated)",
|
||||
inputSchema={
|
||||
input_schema={
|
||||
"type": "object",
|
||||
"properties": {"city": {"type": "string", "description": "City name"}},
|
||||
"required": ["city"],
|
||||
},
|
||||
outputSchema={
|
||||
output_schema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"temperature": {"type": "number"},
|
||||
|
||||
@@ -28,8 +28,8 @@ async def run():
|
||||
# List available resource templates
|
||||
templates = await session.list_resource_templates()
|
||||
print("Available resource templates:")
|
||||
for template in templates.resourceTemplates:
|
||||
print(f" - {template.uriTemplate}")
|
||||
for template in templates.resource_templates:
|
||||
print(f" - {template.uri_template}")
|
||||
|
||||
# List available prompts
|
||||
prompts = await session.list_prompts()
|
||||
@@ -38,20 +38,20 @@ async def run():
|
||||
print(f" - {prompt.name}")
|
||||
|
||||
# Complete resource template arguments
|
||||
if templates.resourceTemplates:
|
||||
template = templates.resourceTemplates[0]
|
||||
print(f"\nCompleting arguments for resource template: {template.uriTemplate}")
|
||||
if templates.resource_templates:
|
||||
template = templates.resource_templates[0]
|
||||
print(f"\nCompleting arguments for resource template: {template.uri_template}")
|
||||
|
||||
# Complete without context
|
||||
result = await session.complete(
|
||||
ref=ResourceTemplateReference(type="ref/resource", uri=template.uriTemplate),
|
||||
ref=ResourceTemplateReference(type="ref/resource", uri=template.uri_template),
|
||||
argument={"name": "owner", "value": "model"},
|
||||
)
|
||||
print(f"Completions for 'owner' starting with 'model': {result.completion.values}")
|
||||
|
||||
# Complete with context - repo suggestions based on owner
|
||||
result = await session.complete(
|
||||
ref=ResourceTemplateReference(type="ref/resource", uri=template.uriTemplate),
|
||||
ref=ResourceTemplateReference(type="ref/resource", uri=template.uri_template),
|
||||
argument={"name": "repo", "value": ""},
|
||||
context_arguments={"owner": "modelcontextprotocol"},
|
||||
)
|
||||
|
||||
@@ -39,7 +39,7 @@ async def display_resources(session: ClientSession):
|
||||
print(f"Resource: {display_name} ({resource.uri})")
|
||||
|
||||
templates_response = await session.list_resource_templates()
|
||||
for template in templates_response.resourceTemplates:
|
||||
for template in templates_response.resource_templates:
|
||||
display_name = get_display_name(template)
|
||||
print(f"Resource Template: {display_name}")
|
||||
|
||||
|
||||
@@ -29,8 +29,8 @@ async def list_all_resources() -> None:
|
||||
print(f"Fetched {len(result.resources)} resources")
|
||||
|
||||
# Check if there are more pages
|
||||
if result.nextCursor:
|
||||
cursor = result.nextCursor
|
||||
if result.next_cursor:
|
||||
cursor = result.next_cursor
|
||||
else:
|
||||
break
|
||||
|
||||
|
||||
@@ -22,9 +22,9 @@ async def parse_tool_results():
|
||||
|
||||
# Example 2: Parsing structured content from JSON tools
|
||||
result = await session.call_tool("get_user", {"id": "123"})
|
||||
if hasattr(result, "structuredContent") and result.structuredContent:
|
||||
if hasattr(result, "structured_content") and result.structured_content:
|
||||
# Access structured data directly
|
||||
user_data = result.structuredContent
|
||||
user_data = result.structured_content
|
||||
print(f"User: {user_data.get('name')}, Age: {user_data.get('age')}")
|
||||
|
||||
# Example 3: Parsing embedded resources
|
||||
@@ -41,11 +41,11 @@ async def parse_tool_results():
|
||||
result = await session.call_tool("generate_chart", {"data": [1, 2, 3]})
|
||||
for content in result.content:
|
||||
if isinstance(content, types.ImageContent):
|
||||
print(f"Image ({content.mimeType}): {len(content.data)} bytes")
|
||||
print(f"Image ({content.mime_type}): {len(content.data)} bytes")
|
||||
|
||||
# Example 5: Handling errors
|
||||
result = await session.call_tool("failing_tool", {})
|
||||
if result.isError:
|
||||
if result.is_error:
|
||||
print("Tool execution failed!")
|
||||
for content in result.content:
|
||||
if isinstance(content, types.TextContent):
|
||||
|
||||
@@ -32,7 +32,7 @@ async def handle_sampling_message(
|
||||
text="Hello, world! from model",
|
||||
),
|
||||
model="gpt-3.5-turbo",
|
||||
stopReason="endTurn",
|
||||
stop_reason="endTurn",
|
||||
)
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ async def run():
|
||||
result_unstructured = result.content[0]
|
||||
if isinstance(result_unstructured, types.TextContent):
|
||||
print(f"Tool result: {result_unstructured.text}")
|
||||
result_structured = result.structuredContent
|
||||
result_structured = result.structured_content
|
||||
print(f"Structured tool result: {result_structured}")
|
||||
|
||||
|
||||
|
||||
@@ -154,7 +154,7 @@ async def call_tool_with_error_handling(
|
||||
result = await session.call_tool(tool_name, arguments)
|
||||
|
||||
# Check if the tool returned an error in the result
|
||||
if result.isError:
|
||||
if result.is_error:
|
||||
print(f"Tool returned error: {result.content}")
|
||||
return None
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ async def handle_completion(
|
||||
languages = ["python", "javascript", "typescript", "go", "rust"]
|
||||
return Completion(
|
||||
values=[lang for lang in languages if lang.startswith(argument.value)],
|
||||
hasMore=False,
|
||||
has_more=False,
|
||||
)
|
||||
|
||||
# Complete repository names for GitHub resources
|
||||
@@ -44,6 +44,6 @@ async def handle_completion(
|
||||
if ref.uri == "github://repos/{owner}/{repo}" and argument.name == "repo":
|
||||
if context and context.arguments and context.arguments.get("owner") == "modelcontextprotocol":
|
||||
repos = ["python-sdk", "typescript-sdk", "specification"]
|
||||
return Completion(values=repos, hasMore=False)
|
||||
return Completion(values=repos, has_more=False)
|
||||
|
||||
return None
|
||||
|
||||
@@ -31,7 +31,7 @@ def validated_tool() -> Annotated[CallToolResult, ValidationModel]:
|
||||
"""Return CallToolResult with structured output validation."""
|
||||
return CallToolResult(
|
||||
content=[TextContent(type="text", text="Validated response")],
|
||||
structuredContent={"status": "success", "data": {"result": 42}},
|
||||
structured_content={"status": "success", "data": {"result": 42}},
|
||||
_meta={"internal": "metadata"},
|
||||
)
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ async def connect_service(service_name: str, ctx: Context[ServerSession, None])
|
||||
mode="url",
|
||||
message=f"Authorization required to connect to {service_name}",
|
||||
url=f"https://{service_name}.example.com/oauth/authorize?elicit={elicitation_id}",
|
||||
elicitationId=elicitation_id,
|
||||
elicitation_id=elicitation_id,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
@@ -21,7 +21,7 @@ async def list_tools() -> list[types.Tool]:
|
||||
types.Tool(
|
||||
name="advanced_tool",
|
||||
description="Tool with full control including _meta field",
|
||||
inputSchema={
|
||||
input_schema={
|
||||
"type": "object",
|
||||
"properties": {"message": {"type": "string"}},
|
||||
"required": ["message"],
|
||||
@@ -37,7 +37,7 @@ async def handle_call_tool(name: str, arguments: dict[str, Any]) -> types.CallTo
|
||||
message = str(arguments.get("message", ""))
|
||||
return types.CallToolResult(
|
||||
content=[types.TextContent(type="text", text=f"Processed: {message}")],
|
||||
structuredContent={"result": "success", "message": message},
|
||||
structured_content={"result": "success", "message": message},
|
||||
_meta={"hidden": "data for client applications only"},
|
||||
)
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ async def handle_list_tools() -> list[types.Tool]:
|
||||
types.Tool(
|
||||
name="query_db",
|
||||
description="Query the database",
|
||||
inputSchema={
|
||||
input_schema={
|
||||
"type": "object",
|
||||
"properties": {"query": {"type": "string", "description": "SQL query to execute"}},
|
||||
"required": ["query"],
|
||||
|
||||
@@ -21,12 +21,12 @@ async def list_tools() -> list[types.Tool]:
|
||||
types.Tool(
|
||||
name="get_weather",
|
||||
description="Get current weather for a city",
|
||||
inputSchema={
|
||||
input_schema={
|
||||
"type": "object",
|
||||
"properties": {"city": {"type": "string", "description": "City name"}},
|
||||
"required": ["city"],
|
||||
},
|
||||
outputSchema={
|
||||
output_schema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"temperature": {"type": "number", "description": "Temperature in Celsius"},
|
||||
|
||||
@@ -33,4 +33,4 @@ async def list_resources_paginated(request: types.ListResourcesRequest) -> types
|
||||
# Determine next cursor
|
||||
next_cursor = str(end) if end < len(ITEMS) else None
|
||||
|
||||
return types.ListResourcesResult(resources=page_items, nextCursor=next_cursor)
|
||||
return types.ListResourcesResult(resources=page_items, next_cursor=next_cursor)
|
||||
|
||||
@@ -225,7 +225,7 @@ class ExperimentalTaskHandlers:
|
||||
requests_capability: types.ClientTasksRequestsCapability | None = None
|
||||
if has_sampling or has_elicitation:
|
||||
requests_capability = types.ClientTasksRequestsCapability(
|
||||
sampling=types.TasksSamplingCapability(createMessage=types.TasksCreateMessageCapability())
|
||||
sampling=types.TasksSamplingCapability(create_message=types.TasksCreateMessageCapability())
|
||||
if has_sampling
|
||||
else None,
|
||||
elicitation=types.TasksElicitationCapability(create=types.TasksCreateElicitationCapability())
|
||||
|
||||
@@ -8,7 +8,7 @@ WARNING: These APIs are experimental and may change without notice.
|
||||
Example:
|
||||
# Call a tool as a task
|
||||
result = await session.experimental.call_tool_as_task("tool_name", {"arg": "value"})
|
||||
task_id = result.task.taskId
|
||||
task_id = result.task.task_id
|
||||
|
||||
# Get task status
|
||||
status = await session.experimental.get_task(task_id)
|
||||
@@ -77,7 +77,7 @@ class ExperimentalClientFeatures:
|
||||
result = await session.experimental.call_tool_as_task(
|
||||
"long_running_tool", {"input": "data"}
|
||||
)
|
||||
task_id = result.task.taskId
|
||||
task_id = result.task.task_id
|
||||
|
||||
# Poll for completion
|
||||
while True:
|
||||
@@ -120,7 +120,7 @@ class ExperimentalClientFeatures:
|
||||
return await self._session.send_request(
|
||||
types.ClientRequest(
|
||||
types.GetTaskRequest(
|
||||
params=types.GetTaskRequestParams(taskId=task_id),
|
||||
params=types.GetTaskRequestParams(task_id=task_id),
|
||||
)
|
||||
),
|
||||
types.GetTaskResult,
|
||||
@@ -148,7 +148,7 @@ class ExperimentalClientFeatures:
|
||||
return await self._session.send_request(
|
||||
types.ClientRequest(
|
||||
types.GetTaskPayloadRequest(
|
||||
params=types.GetTaskPayloadRequestParams(taskId=task_id),
|
||||
params=types.GetTaskPayloadRequestParams(task_id=task_id),
|
||||
)
|
||||
),
|
||||
result_type,
|
||||
@@ -188,7 +188,7 @@ class ExperimentalClientFeatures:
|
||||
return await self._session.send_request(
|
||||
types.ClientRequest(
|
||||
types.CancelTaskRequest(
|
||||
params=types.CancelTaskRequestParams(taskId=task_id),
|
||||
params=types.CancelTaskRequestParams(task_id=task_id),
|
||||
)
|
||||
),
|
||||
types.CancelTaskResult,
|
||||
|
||||
+10
-10
@@ -161,7 +161,7 @@ class ClientSession(
|
||||
# TODO: Should this be based on whether we
|
||||
# _will_ send notifications, or only whether
|
||||
# they're supported?
|
||||
types.RootsCapability(listChanged=True)
|
||||
types.RootsCapability(list_changed=True)
|
||||
if self._list_roots_callback is not _default_list_roots_callback
|
||||
else None
|
||||
)
|
||||
@@ -170,7 +170,7 @@ class ClientSession(
|
||||
types.ClientRequest(
|
||||
types.InitializeRequest(
|
||||
params=types.InitializeRequestParams(
|
||||
protocolVersion=types.LATEST_PROTOCOL_VERSION,
|
||||
protocol_version=types.LATEST_PROTOCOL_VERSION,
|
||||
capabilities=types.ClientCapabilities(
|
||||
sampling=sampling,
|
||||
elicitation=elicitation,
|
||||
@@ -178,15 +178,15 @@ class ClientSession(
|
||||
roots=roots,
|
||||
tasks=self._task_handlers.build_capability(),
|
||||
),
|
||||
clientInfo=self._client_info,
|
||||
client_info=self._client_info,
|
||||
),
|
||||
)
|
||||
),
|
||||
types.InitializeResult,
|
||||
)
|
||||
|
||||
if result.protocolVersion not in SUPPORTED_PROTOCOL_VERSIONS:
|
||||
raise RuntimeError(f"Unsupported protocol version from the server: {result.protocolVersion}")
|
||||
if result.protocol_version not in SUPPORTED_PROTOCOL_VERSIONS:
|
||||
raise RuntimeError(f"Unsupported protocol version from the server: {result.protocol_version}")
|
||||
|
||||
self._server_capabilities = result.capabilities
|
||||
|
||||
@@ -235,7 +235,7 @@ class ClientSession(
|
||||
types.ClientNotification(
|
||||
types.ProgressNotification(
|
||||
params=types.ProgressNotificationParams(
|
||||
progressToken=progress_token,
|
||||
progress_token=progress_token,
|
||||
progress=progress,
|
||||
total=total,
|
||||
message=message,
|
||||
@@ -326,7 +326,7 @@ class ClientSession(
|
||||
progress_callback=progress_callback,
|
||||
)
|
||||
|
||||
if not result.isError:
|
||||
if not result.is_error:
|
||||
await self._validate_tool_result(name, result)
|
||||
|
||||
return result
|
||||
@@ -346,12 +346,12 @@ class ClientSession(
|
||||
if output_schema is not None:
|
||||
from jsonschema import SchemaError, ValidationError, validate
|
||||
|
||||
if result.structuredContent is None:
|
||||
if result.structured_content is None:
|
||||
raise RuntimeError(
|
||||
f"Tool {name} has an output schema but did not return structured content"
|
||||
) # pragma: no cover
|
||||
try:
|
||||
validate(result.structuredContent, output_schema)
|
||||
validate(result.structured_content, output_schema)
|
||||
except ValidationError as e:
|
||||
raise RuntimeError(f"Invalid structured content returned by tool {name}: {e}") # pragma: no cover
|
||||
except SchemaError as e: # pragma: no cover
|
||||
@@ -418,7 +418,7 @@ class ClientSession(
|
||||
# Cache tool output schemas for future validation
|
||||
# Note: don't clear the cache, as we may be using a cursor
|
||||
for tool in result.tools:
|
||||
self._tool_output_schemas[tool.name] = tool.outputSchema
|
||||
self._tool_output_schemas[tool.name] = tool.output_schema
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@@ -119,9 +119,9 @@ class ClientSessionGroup:
|
||||
_exit_stack: contextlib.AsyncExitStack
|
||||
_session_exit_stacks: dict[mcp.ClientSession, contextlib.AsyncExitStack]
|
||||
|
||||
# Optional fn consuming (component_name, serverInfo) for custom names.
|
||||
# Optional fn consuming (component_name, server_info) for custom names.
|
||||
# This is provide a means to mitigate naming conflicts across servers.
|
||||
# Example: (tool_name, serverInfo) => "{result.serverInfo.name}.{tool_name}"
|
||||
# Example: (tool_name, server_info) => "{result.server_info.name}.{tool_name}"
|
||||
_ComponentNameHook: TypeAlias = Callable[[str, types.Implementation], str]
|
||||
_component_name_hook: _ComponentNameHook | None
|
||||
|
||||
@@ -324,7 +324,7 @@ class ClientSessionGroup:
|
||||
# main _exit_stack.
|
||||
await self._exit_stack.enter_async_context(session_stack)
|
||||
|
||||
return result.serverInfo, session
|
||||
return result.server_info, session
|
||||
except Exception: # pragma: no cover
|
||||
# If anything during this setup fails, ensure the session-specific
|
||||
# stack is closed.
|
||||
|
||||
@@ -110,7 +110,7 @@ async def sse_client(
|
||||
continue
|
||||
try:
|
||||
message = types.JSONRPCMessage.model_validate_json( # noqa: E501
|
||||
sse.data
|
||||
sse.data, by_name=False
|
||||
)
|
||||
logger.debug(f"Received server message: {message}")
|
||||
except Exception as exc: # pragma: no cover
|
||||
|
||||
@@ -152,7 +152,7 @@ async def stdio_client(server: StdioServerParameters, errlog: TextIO = sys.stder
|
||||
|
||||
for line in lines:
|
||||
try:
|
||||
message = types.JSONRPCMessage.model_validate_json(line)
|
||||
message = types.JSONRPCMessage.model_validate_json(line, by_name=False)
|
||||
except Exception as exc: # pragma: no cover
|
||||
logger.exception("Failed to parse JSONRPC message from server")
|
||||
await read_stream_writer.send(exc)
|
||||
|
||||
@@ -113,8 +113,8 @@ class StreamableHTTPTransport:
|
||||
if isinstance(message.root, JSONRPCResponse) and message.root.result: # pragma: no branch
|
||||
try:
|
||||
# Parse the result as InitializeResult for type safety
|
||||
init_result = InitializeResult.model_validate(message.root.result)
|
||||
self.protocol_version = str(init_result.protocolVersion)
|
||||
init_result = InitializeResult.model_validate(message.root.result, by_name=False)
|
||||
self.protocol_version = str(init_result.protocol_version)
|
||||
logger.info(f"Negotiated protocol version: {self.protocol_version}")
|
||||
except Exception: # pragma: no cover
|
||||
logger.warning("Failed to parse initialization response as InitializeResult", exc_info=True)
|
||||
@@ -137,7 +137,7 @@ class StreamableHTTPTransport:
|
||||
await resumption_callback(sse.id)
|
||||
return False
|
||||
try:
|
||||
message = JSONRPCMessage.model_validate_json(sse.data)
|
||||
message = JSONRPCMessage.model_validate_json(sse.data, by_name=False)
|
||||
logger.debug(f"SSE message: {message}")
|
||||
|
||||
# Extract protocol version from initialization response
|
||||
@@ -291,7 +291,7 @@ class StreamableHTTPTransport:
|
||||
"""Handle JSON response from the server."""
|
||||
try:
|
||||
content = await response.aread()
|
||||
message = JSONRPCMessage.model_validate_json(content)
|
||||
message = JSONRPCMessage.model_validate_json(content, by_name=False)
|
||||
|
||||
# Extract protocol version from initialization response
|
||||
if is_initialization:
|
||||
|
||||
@@ -53,7 +53,7 @@ async def websocket_client(
|
||||
async with read_stream_writer:
|
||||
async for raw_text in ws:
|
||||
try:
|
||||
message = types.JSONRPCMessage.model_validate_json(raw_text)
|
||||
message = types.JSONRPCMessage.model_validate_json(raw_text, by_name=False)
|
||||
session_message = SessionMessage(message)
|
||||
await read_stream_writer.send(session_message)
|
||||
except ValidationError as exc: # pragma: no cover
|
||||
|
||||
@@ -125,7 +125,7 @@ async def elicit_with_validation(
|
||||
|
||||
result = await session.elicit_form(
|
||||
message=message,
|
||||
requestedSchema=json_schema,
|
||||
requested_schema=json_schema,
|
||||
related_request_id=related_request_id,
|
||||
)
|
||||
|
||||
|
||||
@@ -122,7 +122,7 @@ class Experimental:
|
||||
Returns:
|
||||
None if valid, ErrorData if invalid and raise_error=False
|
||||
"""
|
||||
mode = tool.execution.taskSupport if tool.execution else None
|
||||
mode = tool.execution.task_support if tool.execution else None
|
||||
return self.validate_task_mode(mode, raise_error=raise_error)
|
||||
|
||||
def can_use_tool(self, tool_task_mode: TaskExecutionMode | None) -> bool:
|
||||
|
||||
@@ -52,7 +52,7 @@ class ExperimentalServerSessionFeatures:
|
||||
GetTaskResult containing the task status
|
||||
"""
|
||||
return await self._session.send_request(
|
||||
types.ServerRequest(types.GetTaskRequest(params=types.GetTaskRequestParams(taskId=task_id))),
|
||||
types.ServerRequest(types.GetTaskRequest(params=types.GetTaskRequestParams(task_id=task_id))),
|
||||
types.GetTaskResult,
|
||||
)
|
||||
|
||||
@@ -72,7 +72,7 @@ class ExperimentalServerSessionFeatures:
|
||||
The task result, validated against result_type
|
||||
"""
|
||||
return await self._session.send_request(
|
||||
types.ServerRequest(types.GetTaskPayloadRequest(params=types.GetTaskPayloadRequestParams(taskId=task_id))),
|
||||
types.ServerRequest(types.GetTaskPayloadRequest(params=types.GetTaskPayloadRequestParams(task_id=task_id))),
|
||||
result_type,
|
||||
)
|
||||
|
||||
@@ -97,7 +97,7 @@ class ExperimentalServerSessionFeatures:
|
||||
async def elicit_as_task(
|
||||
self,
|
||||
message: str,
|
||||
requestedSchema: types.ElicitRequestedSchema,
|
||||
requested_schema: types.ElicitRequestedSchema,
|
||||
*,
|
||||
ttl: int = 60000,
|
||||
) -> types.ElicitResult:
|
||||
@@ -113,7 +113,7 @@ class ExperimentalServerSessionFeatures:
|
||||
|
||||
Args:
|
||||
message: The message to present to the user
|
||||
requestedSchema: Schema defining the expected response
|
||||
requested_schema: Schema defining the expected response
|
||||
ttl: Task time-to-live in milliseconds
|
||||
|
||||
Returns:
|
||||
@@ -130,7 +130,7 @@ class ExperimentalServerSessionFeatures:
|
||||
types.ElicitRequest(
|
||||
params=types.ElicitRequestFormParams(
|
||||
message=message,
|
||||
requestedSchema=requestedSchema,
|
||||
requested_schema=requested_schema,
|
||||
task=types.TaskMetadata(ttl=ttl),
|
||||
)
|
||||
)
|
||||
@@ -138,7 +138,7 @@ class ExperimentalServerSessionFeatures:
|
||||
types.CreateTaskResult,
|
||||
)
|
||||
|
||||
task_id = create_result.task.taskId
|
||||
task_id = create_result.task.task_id
|
||||
|
||||
async for _ in self.poll_task(task_id):
|
||||
pass
|
||||
@@ -196,15 +196,15 @@ class ExperimentalServerSessionFeatures:
|
||||
types.CreateMessageRequest(
|
||||
params=types.CreateMessageRequestParams(
|
||||
messages=messages,
|
||||
maxTokens=max_tokens,
|
||||
systemPrompt=system_prompt,
|
||||
includeContext=include_context,
|
||||
max_tokens=max_tokens,
|
||||
system_prompt=system_prompt,
|
||||
include_context=include_context,
|
||||
temperature=temperature,
|
||||
stopSequences=stop_sequences,
|
||||
stop_sequences=stop_sequences,
|
||||
metadata=metadata,
|
||||
modelPreferences=model_preferences,
|
||||
model_preferences=model_preferences,
|
||||
tools=tools,
|
||||
toolChoice=tool_choice,
|
||||
tool_choice=tool_choice,
|
||||
task=types.TaskMetadata(ttl=ttl),
|
||||
)
|
||||
)
|
||||
@@ -212,7 +212,7 @@ class ExperimentalServerSessionFeatures:
|
||||
types.CreateTaskResult,
|
||||
)
|
||||
|
||||
task_id = create_result.task.taskId
|
||||
task_id = create_result.task.task_id
|
||||
|
||||
async for _ in self.poll_task(task_id):
|
||||
pass
|
||||
|
||||
@@ -65,7 +65,7 @@ class ServerTaskContext:
|
||||
|
||||
result = await task.elicit(
|
||||
message="Continue?",
|
||||
requestedSchema={"type": "object", "properties": {"ok": {"type": "boolean"}}}
|
||||
requested_schema={"type": "object", "properties": {"ok": {"type": "boolean"}}}
|
||||
)
|
||||
|
||||
if result.content.get("ok"):
|
||||
@@ -165,13 +165,13 @@ class ServerTaskContext:
|
||||
ServerNotification(
|
||||
TaskStatusNotification(
|
||||
params=TaskStatusNotificationParams(
|
||||
taskId=task.taskId,
|
||||
task_id=task.task_id,
|
||||
status=task.status,
|
||||
statusMessage=task.statusMessage,
|
||||
createdAt=task.createdAt,
|
||||
lastUpdatedAt=task.lastUpdatedAt,
|
||||
status_message=task.status_message,
|
||||
created_at=task.created_at,
|
||||
last_updated_at=task.last_updated_at,
|
||||
ttl=task.ttl,
|
||||
pollInterval=task.pollInterval,
|
||||
poll_interval=task.poll_interval,
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -202,7 +202,7 @@ class ServerTaskContext:
|
||||
async def elicit(
|
||||
self,
|
||||
message: str,
|
||||
requestedSchema: ElicitRequestedSchema,
|
||||
requested_schema: ElicitRequestedSchema,
|
||||
) -> ElicitResult:
|
||||
"""
|
||||
Send an elicitation request via the task message queue.
|
||||
@@ -217,7 +217,7 @@ class ServerTaskContext:
|
||||
|
||||
Args:
|
||||
message: The message to present to the user
|
||||
requestedSchema: Schema defining the expected response structure
|
||||
requested_schema: Schema defining the expected response structure
|
||||
|
||||
Returns:
|
||||
The client's response
|
||||
@@ -236,7 +236,7 @@ class ServerTaskContext:
|
||||
# Build the request using session's helper
|
||||
request = self._session._build_elicit_form_request( # pyright: ignore[reportPrivateUsage]
|
||||
message=message,
|
||||
requestedSchema=requestedSchema,
|
||||
requested_schema=requested_schema,
|
||||
related_task_id=self.task_id,
|
||||
)
|
||||
request_id: RequestId = request.id
|
||||
@@ -430,7 +430,7 @@ class ServerTaskContext:
|
||||
async def elicit_as_task(
|
||||
self,
|
||||
message: str,
|
||||
requestedSchema: ElicitRequestedSchema,
|
||||
requested_schema: ElicitRequestedSchema,
|
||||
*,
|
||||
ttl: int = 60000,
|
||||
) -> ElicitResult:
|
||||
@@ -444,7 +444,7 @@ class ServerTaskContext:
|
||||
|
||||
Args:
|
||||
message: The message to present to the user
|
||||
requestedSchema: Schema defining the expected response structure
|
||||
requested_schema: Schema defining the expected response structure
|
||||
ttl: Task time-to-live in milliseconds for the client's task
|
||||
|
||||
Returns:
|
||||
@@ -465,7 +465,7 @@ class ServerTaskContext:
|
||||
|
||||
request = self._session._build_elicit_form_request( # pyright: ignore[reportPrivateUsage]
|
||||
message=message,
|
||||
requestedSchema=requestedSchema,
|
||||
requested_schema=requested_schema,
|
||||
related_task_id=self.task_id,
|
||||
task=TaskMetadata(ttl=ttl),
|
||||
)
|
||||
@@ -486,7 +486,7 @@ class ServerTaskContext:
|
||||
# Wait for initial response (CreateTaskResult from client)
|
||||
response_data = await resolver.wait()
|
||||
create_result = CreateTaskResult.model_validate(response_data)
|
||||
client_task_id = create_result.task.taskId
|
||||
client_task_id = create_result.task.task_id
|
||||
|
||||
# Poll the client's task using session.experimental
|
||||
async for _ in self._session.experimental.poll_task(client_task_id):
|
||||
@@ -592,7 +592,7 @@ class ServerTaskContext:
|
||||
# Wait for initial response (CreateTaskResult from client)
|
||||
response_data = await resolver.wait()
|
||||
create_result = CreateTaskResult.model_validate(response_data)
|
||||
client_task_id = create_result.task.taskId
|
||||
client_task_id = create_result.task.task_id
|
||||
|
||||
# Poll the client's task using session.experimental
|
||||
async for _ in self._session.experimental.poll_task(client_task_id):
|
||||
|
||||
@@ -106,7 +106,7 @@ class TaskResultHandler:
|
||||
Returns:
|
||||
GetTaskPayloadResult with the task's final payload
|
||||
"""
|
||||
task_id = request.params.taskId
|
||||
task_id = request.params.task_id
|
||||
|
||||
while True:
|
||||
task = await self._store.get_task(task_id)
|
||||
@@ -126,7 +126,7 @@ class TaskResultHandler:
|
||||
# GetTaskPayloadResult is a Result with extra="allow"
|
||||
# The stored result contains the actual payload data
|
||||
# Per spec: tasks/result MUST include _meta with related-task metadata
|
||||
related_task = RelatedTaskMetadata(taskId=task_id)
|
||||
related_task = RelatedTaskMetadata(task_id=task_id)
|
||||
related_task_meta: dict[str, Any] = {RELATED_TASK_METADATA_KEY: related_task.model_dump(by_alias=True)}
|
||||
if result is not None:
|
||||
result_data = result.model_dump(by_alias=True)
|
||||
|
||||
@@ -305,8 +305,8 @@ class FastMCP(Generic[LifespanResultT]):
|
||||
name=info.name,
|
||||
title=info.title,
|
||||
description=info.description,
|
||||
inputSchema=info.parameters,
|
||||
outputSchema=info.output_schema,
|
||||
input_schema=info.parameters,
|
||||
output_schema=info.output_schema,
|
||||
annotations=info.annotations,
|
||||
icons=info.icons,
|
||||
_meta=info.meta,
|
||||
@@ -340,7 +340,7 @@ class FastMCP(Generic[LifespanResultT]):
|
||||
name=resource.name or "",
|
||||
title=resource.title,
|
||||
description=resource.description,
|
||||
mimeType=resource.mime_type,
|
||||
mime_type=resource.mime_type,
|
||||
icons=resource.icons,
|
||||
annotations=resource.annotations,
|
||||
_meta=resource.meta,
|
||||
@@ -352,11 +352,11 @@ class FastMCP(Generic[LifespanResultT]):
|
||||
templates = self._resource_manager.list_templates()
|
||||
return [
|
||||
MCPResourceTemplate(
|
||||
uriTemplate=template.uri_template,
|
||||
uri_template=template.uri_template,
|
||||
name=template.name,
|
||||
title=template.title,
|
||||
description=template.description,
|
||||
mimeType=template.mime_type,
|
||||
mime_type=template.mime_type,
|
||||
icons=template.icons,
|
||||
annotations=template.annotations,
|
||||
_meta=template.meta,
|
||||
@@ -1104,7 +1104,7 @@ class Context(BaseModel, Generic[ServerSessionT, LifespanContextT, RequestT]):
|
||||
total: Optional total value e.g. 100
|
||||
message: Optional message e.g. Starting render...
|
||||
"""
|
||||
progress_token = self.request_context.meta.progressToken if self.request_context.meta else None
|
||||
progress_token = self.request_context.meta.progress_token if self.request_context.meta else None
|
||||
|
||||
if progress_token is None: # pragma: no cover
|
||||
return
|
||||
|
||||
@@ -113,7 +113,7 @@ class FuncMetadata(BaseModel):
|
||||
if isinstance(result, CallToolResult):
|
||||
if self.output_schema is not None:
|
||||
assert self.output_model is not None, "Output model must be set if output schema is defined"
|
||||
self.output_model.model_validate(result.structuredContent)
|
||||
self.output_model.model_validate(result.structured_content)
|
||||
return result
|
||||
|
||||
unstructured_content = _convert_to_content(result)
|
||||
|
||||
@@ -51,7 +51,7 @@ class Image:
|
||||
else: # pragma: no cover
|
||||
raise ValueError("No image data available")
|
||||
|
||||
return ImageContent(type="image", data=data, mimeType=self._mime_type)
|
||||
return ImageContent(type="image", data=data, mime_type=self._mime_type)
|
||||
|
||||
|
||||
class Audio:
|
||||
@@ -98,4 +98,4 @@ class Audio:
|
||||
else: # pragma: no cover
|
||||
raise ValueError("No audio data available")
|
||||
|
||||
return AudioContent(type="audio", data=data, mimeType=self._mime_type)
|
||||
return AudioContent(type="audio", data=data, mime_type=self._mime_type)
|
||||
|
||||
@@ -134,23 +134,23 @@ class ExperimentalHandlers:
|
||||
if GetTaskRequest not in self._request_handlers:
|
||||
|
||||
async def _default_get_task(req: GetTaskRequest) -> ServerResult:
|
||||
task = await support.store.get_task(req.params.taskId)
|
||||
task = await support.store.get_task(req.params.task_id)
|
||||
if task is None:
|
||||
raise McpError(
|
||||
ErrorData(
|
||||
code=INVALID_PARAMS,
|
||||
message=f"Task not found: {req.params.taskId}",
|
||||
message=f"Task not found: {req.params.task_id}",
|
||||
)
|
||||
)
|
||||
return ServerResult(
|
||||
GetTaskResult(
|
||||
taskId=task.taskId,
|
||||
task_id=task.task_id,
|
||||
status=task.status,
|
||||
statusMessage=task.statusMessage,
|
||||
createdAt=task.createdAt,
|
||||
lastUpdatedAt=task.lastUpdatedAt,
|
||||
status_message=task.status_message,
|
||||
created_at=task.created_at,
|
||||
last_updated_at=task.last_updated_at,
|
||||
ttl=task.ttl,
|
||||
pollInterval=task.pollInterval,
|
||||
poll_interval=task.poll_interval,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -172,7 +172,7 @@ class ExperimentalHandlers:
|
||||
async def _default_list_tasks(req: ListTasksRequest) -> ServerResult:
|
||||
cursor = req.params.cursor if req.params else None
|
||||
tasks, next_cursor = await support.store.list_tasks(cursor)
|
||||
return ServerResult(ListTasksResult(tasks=tasks, nextCursor=next_cursor))
|
||||
return ServerResult(ListTasksResult(tasks=tasks, next_cursor=next_cursor))
|
||||
|
||||
self._request_handlers[ListTasksRequest] = _default_list_tasks
|
||||
|
||||
@@ -180,7 +180,7 @@ class ExperimentalHandlers:
|
||||
if CancelTaskRequest not in self._request_handlers:
|
||||
|
||||
async def _default_cancel_task(req: CancelTaskRequest) -> ServerResult:
|
||||
result = await cancel_task(support.store, req.params.taskId)
|
||||
result = await cancel_task(support.store, req.params.task_id)
|
||||
return ServerResult(result)
|
||||
|
||||
self._request_handlers[CancelTaskRequest] = _default_cancel_task
|
||||
|
||||
@@ -209,17 +209,17 @@ class Server(Generic[LifespanResultT, RequestT]):
|
||||
|
||||
# Set prompt capabilities if handler exists
|
||||
if types.ListPromptsRequest in self.request_handlers:
|
||||
prompts_capability = types.PromptsCapability(listChanged=notification_options.prompts_changed)
|
||||
prompts_capability = types.PromptsCapability(list_changed=notification_options.prompts_changed)
|
||||
|
||||
# Set resource capabilities if handler exists
|
||||
if types.ListResourcesRequest in self.request_handlers:
|
||||
resources_capability = types.ResourcesCapability(
|
||||
subscribe=False, listChanged=notification_options.resources_changed
|
||||
subscribe=False, list_changed=notification_options.resources_changed
|
||||
)
|
||||
|
||||
# Set tool capabilities if handler exists
|
||||
if types.ListToolsRequest in self.request_handlers:
|
||||
tools_capability = types.ToolsCapability(listChanged=notification_options.tools_changed)
|
||||
tools_capability = types.ToolsCapability(list_changed=notification_options.tools_changed)
|
||||
|
||||
# Set logging capabilities if handler exists
|
||||
if types.SetLevelRequest in self.request_handlers: # pragma: no cover
|
||||
@@ -327,7 +327,7 @@ class Server(Generic[LifespanResultT, RequestT]):
|
||||
|
||||
async def handler(_: Any):
|
||||
templates = await func()
|
||||
return types.ServerResult(types.ListResourceTemplatesResult(resourceTemplates=templates))
|
||||
return types.ServerResult(types.ListResourceTemplatesResult(resource_templates=templates))
|
||||
|
||||
self.request_handlers[types.ListResourceTemplatesRequest] = handler
|
||||
return func
|
||||
@@ -351,14 +351,14 @@ class Server(Generic[LifespanResultT, RequestT]):
|
||||
return types.TextResourceContents(
|
||||
uri=req.params.uri,
|
||||
text=data,
|
||||
mimeType=mime_type or "text/plain",
|
||||
mime_type=mime_type or "text/plain",
|
||||
**meta_kwargs,
|
||||
)
|
||||
case bytes() as data: # pragma: no cover
|
||||
return types.BlobResourceContents(
|
||||
uri=req.params.uri,
|
||||
blob=base64.b64encode(data).decode(),
|
||||
mimeType=mime_type or "application/octet-stream",
|
||||
mime_type=mime_type or "application/octet-stream",
|
||||
**meta_kwargs,
|
||||
)
|
||||
|
||||
@@ -474,7 +474,7 @@ class Server(Generic[LifespanResultT, RequestT]):
|
||||
return types.ServerResult(
|
||||
types.CallToolResult(
|
||||
content=[types.TextContent(type="text", text=error_message)],
|
||||
isError=True,
|
||||
is_error=True,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -532,7 +532,7 @@ class Server(Generic[LifespanResultT, RequestT]):
|
||||
# input validation
|
||||
if validate_input and tool:
|
||||
try:
|
||||
jsonschema.validate(instance=arguments, schema=tool.inputSchema)
|
||||
jsonschema.validate(instance=arguments, schema=tool.input_schema)
|
||||
except jsonschema.ValidationError as e:
|
||||
return self._make_error_result(f"Input validation error: {e.message}")
|
||||
|
||||
@@ -562,14 +562,14 @@ class Server(Generic[LifespanResultT, RequestT]):
|
||||
return self._make_error_result(f"Unexpected return type from tool: {type(results).__name__}")
|
||||
|
||||
# output validation
|
||||
if tool and tool.outputSchema is not None:
|
||||
if tool and tool.output_schema is not None:
|
||||
if maybe_structured_content is None:
|
||||
return self._make_error_result(
|
||||
"Output validation error: outputSchema defined but no structured output returned"
|
||||
)
|
||||
else:
|
||||
try:
|
||||
jsonschema.validate(instance=maybe_structured_content, schema=tool.outputSchema)
|
||||
jsonschema.validate(instance=maybe_structured_content, schema=tool.output_schema)
|
||||
except jsonschema.ValidationError as e:
|
||||
return self._make_error_result(f"Output validation error: {e.message}")
|
||||
|
||||
@@ -577,8 +577,8 @@ class Server(Generic[LifespanResultT, RequestT]):
|
||||
return types.ServerResult(
|
||||
types.CallToolResult(
|
||||
content=list(unstructured_content),
|
||||
structuredContent=maybe_structured_content,
|
||||
isError=False,
|
||||
structured_content=maybe_structured_content,
|
||||
is_error=False,
|
||||
)
|
||||
)
|
||||
except UrlElicitationRequiredError:
|
||||
@@ -601,7 +601,7 @@ class Server(Generic[LifespanResultT, RequestT]):
|
||||
|
||||
async def handler(req: types.ProgressNotification):
|
||||
await func(
|
||||
req.params.progressToken,
|
||||
req.params.progress_token,
|
||||
req.params.progress,
|
||||
req.params.total,
|
||||
req.params.message,
|
||||
@@ -633,7 +633,7 @@ class Server(Generic[LifespanResultT, RequestT]):
|
||||
types.CompleteResult(
|
||||
completion=completion
|
||||
if completion is not None
|
||||
else types.Completion(values=[], total=None, hasMore=None),
|
||||
else types.Completion(values=[], total=None, has_more=None),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
+33
-33
@@ -129,7 +129,7 @@ class ServerSession(
|
||||
if capability.roots is not None:
|
||||
if client_caps.roots is None:
|
||||
return False
|
||||
if capability.roots.listChanged and not client_caps.roots.listChanged:
|
||||
if capability.roots.list_changed and not client_caps.roots.list_changed:
|
||||
return False
|
||||
|
||||
if capability.sampling is not None:
|
||||
@@ -165,23 +165,23 @@ class ServerSession(
|
||||
async def _received_request(self, responder: RequestResponder[types.ClientRequest, types.ServerResult]):
|
||||
match responder.request.root:
|
||||
case types.InitializeRequest(params=params):
|
||||
requested_version = params.protocolVersion
|
||||
requested_version = params.protocol_version
|
||||
self._initialization_state = InitializationState.Initializing
|
||||
self._client_params = params
|
||||
with responder:
|
||||
await responder.respond(
|
||||
types.ServerResult(
|
||||
types.InitializeResult(
|
||||
protocolVersion=requested_version
|
||||
protocol_version=requested_version
|
||||
if requested_version in SUPPORTED_PROTOCOL_VERSIONS
|
||||
else types.LATEST_PROTOCOL_VERSION,
|
||||
capabilities=self._init_options.capabilities,
|
||||
serverInfo=types.Implementation(
|
||||
server_info=types.Implementation(
|
||||
name=self._init_options.server_name,
|
||||
title=self._init_options.title,
|
||||
description=self._init_options.description,
|
||||
version=self._init_options.server_version,
|
||||
websiteUrl=self._init_options.website_url,
|
||||
website_url=self._init_options.website_url,
|
||||
icons=self._init_options.icons,
|
||||
),
|
||||
instructions=self._init_options.instructions,
|
||||
@@ -327,15 +327,15 @@ class ServerSession(
|
||||
types.CreateMessageRequest(
|
||||
params=types.CreateMessageRequestParams(
|
||||
messages=messages,
|
||||
systemPrompt=system_prompt,
|
||||
includeContext=include_context,
|
||||
system_prompt=system_prompt,
|
||||
include_context=include_context,
|
||||
temperature=temperature,
|
||||
maxTokens=max_tokens,
|
||||
stopSequences=stop_sequences,
|
||||
max_tokens=max_tokens,
|
||||
stop_sequences=stop_sequences,
|
||||
metadata=metadata,
|
||||
modelPreferences=model_preferences,
|
||||
model_preferences=model_preferences,
|
||||
tools=tools,
|
||||
toolChoice=tool_choice,
|
||||
tool_choice=tool_choice,
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -366,14 +366,14 @@ class ServerSession(
|
||||
async def elicit(
|
||||
self,
|
||||
message: str,
|
||||
requestedSchema: types.ElicitRequestedSchema,
|
||||
requested_schema: types.ElicitRequestedSchema,
|
||||
related_request_id: types.RequestId | None = None,
|
||||
) -> types.ElicitResult:
|
||||
"""Send a form mode elicitation/create request.
|
||||
|
||||
Args:
|
||||
message: The message to present to the user
|
||||
requestedSchema: Schema defining the expected response structure
|
||||
requested_schema: Schema defining the expected response structure
|
||||
related_request_id: Optional ID of the request that triggered this elicitation
|
||||
|
||||
Returns:
|
||||
@@ -383,19 +383,19 @@ class ServerSession(
|
||||
This method is deprecated in favor of elicit_form(). It remains for
|
||||
backward compatibility but new code should use elicit_form().
|
||||
"""
|
||||
return await self.elicit_form(message, requestedSchema, related_request_id)
|
||||
return await self.elicit_form(message, requested_schema, related_request_id)
|
||||
|
||||
async def elicit_form(
|
||||
self,
|
||||
message: str,
|
||||
requestedSchema: types.ElicitRequestedSchema,
|
||||
requested_schema: types.ElicitRequestedSchema,
|
||||
related_request_id: types.RequestId | None = None,
|
||||
) -> types.ElicitResult:
|
||||
"""Send a form mode elicitation/create request.
|
||||
|
||||
Args:
|
||||
message: The message to present to the user
|
||||
requestedSchema: Schema defining the expected response structure
|
||||
requested_schema: Schema defining the expected response structure
|
||||
related_request_id: Optional ID of the request that triggered this elicitation
|
||||
|
||||
Returns:
|
||||
@@ -411,7 +411,7 @@ class ServerSession(
|
||||
types.ElicitRequest(
|
||||
params=types.ElicitRequestFormParams(
|
||||
message=message,
|
||||
requestedSchema=requestedSchema,
|
||||
requested_schema=requested_schema,
|
||||
),
|
||||
)
|
||||
),
|
||||
@@ -451,7 +451,7 @@ class ServerSession(
|
||||
params=types.ElicitRequestURLParams(
|
||||
message=message,
|
||||
url=url,
|
||||
elicitationId=elicitation_id,
|
||||
elicitation_id=elicitation_id,
|
||||
),
|
||||
)
|
||||
),
|
||||
@@ -479,7 +479,7 @@ class ServerSession(
|
||||
types.ServerNotification(
|
||||
types.ProgressNotification(
|
||||
params=types.ProgressNotificationParams(
|
||||
progressToken=progress_token,
|
||||
progress_token=progress_token,
|
||||
progress=progress,
|
||||
total=total,
|
||||
message=message,
|
||||
@@ -519,7 +519,7 @@ class ServerSession(
|
||||
await self.send_notification(
|
||||
types.ServerNotification(
|
||||
types.ElicitCompleteNotification(
|
||||
params=types.ElicitCompleteNotificationParams(elicitationId=elicitation_id)
|
||||
params=types.ElicitCompleteNotificationParams(elicitation_id=elicitation_id)
|
||||
)
|
||||
),
|
||||
related_request_id,
|
||||
@@ -528,7 +528,7 @@ class ServerSession(
|
||||
def _build_elicit_form_request(
|
||||
self,
|
||||
message: str,
|
||||
requestedSchema: types.ElicitRequestedSchema,
|
||||
requested_schema: types.ElicitRequestedSchema,
|
||||
related_task_id: str | None = None,
|
||||
task: types.TaskMetadata | None = None,
|
||||
) -> types.JSONRPCRequest:
|
||||
@@ -536,7 +536,7 @@ class ServerSession(
|
||||
|
||||
Args:
|
||||
message: The message to present to the user
|
||||
requestedSchema: Schema defining the expected response structure
|
||||
requested_schema: Schema defining the expected response structure
|
||||
related_task_id: If provided, adds io.modelcontextprotocol/related-task metadata
|
||||
task: If provided, makes this a task-augmented request
|
||||
|
||||
@@ -545,7 +545,7 @@ class ServerSession(
|
||||
"""
|
||||
params = types.ElicitRequestFormParams(
|
||||
message=message,
|
||||
requestedSchema=requestedSchema,
|
||||
requested_schema=requested_schema,
|
||||
task=task,
|
||||
)
|
||||
params_data = params.model_dump(by_alias=True, mode="json", exclude_none=True)
|
||||
@@ -556,7 +556,7 @@ class ServerSession(
|
||||
if "_meta" not in params_data: # pragma: no cover
|
||||
params_data["_meta"] = {}
|
||||
params_data["_meta"][RELATED_TASK_METADATA_KEY] = types.RelatedTaskMetadata(
|
||||
taskId=related_task_id
|
||||
task_id=related_task_id
|
||||
).model_dump(by_alias=True)
|
||||
|
||||
request_id = f"task-{related_task_id}-{id(params)}" if related_task_id else self._request_id
|
||||
@@ -591,7 +591,7 @@ class ServerSession(
|
||||
params = types.ElicitRequestURLParams(
|
||||
message=message,
|
||||
url=url,
|
||||
elicitationId=elicitation_id,
|
||||
elicitation_id=elicitation_id,
|
||||
)
|
||||
params_data = params.model_dump(by_alias=True, mode="json", exclude_none=True)
|
||||
|
||||
@@ -601,7 +601,7 @@ class ServerSession(
|
||||
if "_meta" not in params_data: # pragma: no cover
|
||||
params_data["_meta"] = {}
|
||||
params_data["_meta"][RELATED_TASK_METADATA_KEY] = types.RelatedTaskMetadata(
|
||||
taskId=related_task_id
|
||||
task_id=related_task_id
|
||||
).model_dump(by_alias=True)
|
||||
|
||||
request_id = f"task-{related_task_id}-{id(params)}" if related_task_id else self._request_id
|
||||
@@ -652,15 +652,15 @@ class ServerSession(
|
||||
"""
|
||||
params = types.CreateMessageRequestParams(
|
||||
messages=messages,
|
||||
systemPrompt=system_prompt,
|
||||
includeContext=include_context,
|
||||
system_prompt=system_prompt,
|
||||
include_context=include_context,
|
||||
temperature=temperature,
|
||||
maxTokens=max_tokens,
|
||||
stopSequences=stop_sequences,
|
||||
max_tokens=max_tokens,
|
||||
stop_sequences=stop_sequences,
|
||||
metadata=metadata,
|
||||
modelPreferences=model_preferences,
|
||||
model_preferences=model_preferences,
|
||||
tools=tools,
|
||||
toolChoice=tool_choice,
|
||||
tool_choice=tool_choice,
|
||||
task=task,
|
||||
)
|
||||
params_data = params.model_dump(by_alias=True, mode="json", exclude_none=True)
|
||||
@@ -671,7 +671,7 @@ class ServerSession(
|
||||
if "_meta" not in params_data: # pragma: no cover
|
||||
params_data["_meta"] = {}
|
||||
params_data["_meta"][RELATED_TASK_METADATA_KEY] = types.RelatedTaskMetadata(
|
||||
taskId=related_task_id
|
||||
task_id=related_task_id
|
||||
).model_dump(by_alias=True)
|
||||
|
||||
request_id = f"task-{related_task_id}-{id(params)}" if related_task_id else self._request_id
|
||||
|
||||
@@ -231,7 +231,7 @@ class SseServerTransport:
|
||||
logger.debug(f"Received JSON: {body}")
|
||||
|
||||
try:
|
||||
message = types.JSONRPCMessage.model_validate_json(body)
|
||||
message = types.JSONRPCMessage.model_validate_json(body, by_name=False)
|
||||
logger.debug(f"Validated client message: {message}")
|
||||
except ValidationError as err:
|
||||
logger.exception("Failed to parse message")
|
||||
|
||||
@@ -62,7 +62,7 @@ async def stdio_server(
|
||||
async with read_stream_writer:
|
||||
async for line in stdin:
|
||||
try:
|
||||
message = types.JSONRPCMessage.model_validate_json(line)
|
||||
message = types.JSONRPCMessage.model_validate_json(line, by_name=False)
|
||||
except Exception as exc: # pragma: no cover
|
||||
await read_stream_writer.send(exc)
|
||||
continue
|
||||
|
||||
@@ -471,7 +471,7 @@ class StreamableHTTPServerTransport:
|
||||
return
|
||||
|
||||
try: # pragma: no cover
|
||||
message = JSONRPCMessage.model_validate(raw_message)
|
||||
message = JSONRPCMessage.model_validate(raw_message, by_name=False)
|
||||
except ValidationError as e: # pragma: no cover
|
||||
response = self._create_error_response(
|
||||
f"Validation error: {str(e)}",
|
||||
|
||||
@@ -99,6 +99,6 @@ def validate_tool_use_result_messages(messages: list[SamplingMessage]) -> None:
|
||||
|
||||
if has_previous_tool_use and previous_content:
|
||||
tool_use_ids = {c.id for c in previous_content if c.type == "tool_use"}
|
||||
tool_result_ids = {c.toolUseId for c in last_content if c.type == "tool_result"}
|
||||
tool_result_ids = {c.tool_use_id for c in last_content if c.type == "tool_result"}
|
||||
if tool_use_ids != tool_result_ids:
|
||||
raise ValueError("ids of tool_result blocks and tool_use blocks from previous message do not match")
|
||||
|
||||
@@ -37,7 +37,7 @@ async def websocket_server(scope: Scope, receive: Receive, send: Send):
|
||||
async with read_stream_writer:
|
||||
async for msg in websocket.iter_text():
|
||||
try:
|
||||
client_message = types.JSONRPCMessage.model_validate_json(msg)
|
||||
client_message = types.JSONRPCMessage.model_validate_json(msg, by_name=False)
|
||||
except ValidationError as exc:
|
||||
await read_stream_writer.send(exc)
|
||||
continue
|
||||
|
||||
@@ -49,7 +49,7 @@ class UrlElicitationRequiredError(McpError):
|
||||
mode="url",
|
||||
message="Authorization required for your files",
|
||||
url="https://example.com/oauth/authorize",
|
||||
elicitationId="auth-001"
|
||||
elicitation_id="auth-001"
|
||||
)
|
||||
])
|
||||
"""
|
||||
|
||||
@@ -48,8 +48,8 @@ def check_tasks_capability(
|
||||
if required.requests.sampling is not None:
|
||||
if client.requests.sampling is None:
|
||||
return False
|
||||
if required.requests.sampling.createMessage is not None:
|
||||
if client.requests.sampling.createMessage is None:
|
||||
if required.requests.sampling.create_message is not None:
|
||||
if client.requests.sampling.create_message is None:
|
||||
return False
|
||||
|
||||
return True
|
||||
@@ -74,7 +74,7 @@ def has_task_augmented_sampling(caps: ClientCapabilities) -> bool:
|
||||
return False
|
||||
if caps.tasks.requests.sampling is None:
|
||||
return False
|
||||
return caps.tasks.requests.sampling.createMessage is not None
|
||||
return caps.tasks.requests.sampling.create_message is not None
|
||||
|
||||
|
||||
def require_task_augmented_elicitation(client_caps: ClientCapabilities | None) -> None:
|
||||
|
||||
@@ -41,7 +41,7 @@ class TaskContext:
|
||||
@property
|
||||
def task_id(self) -> str:
|
||||
"""The task identifier."""
|
||||
return self._task.taskId
|
||||
return self._task.task_id
|
||||
|
||||
@property
|
||||
def task(self) -> Task:
|
||||
|
||||
@@ -125,12 +125,12 @@ def create_task_state(
|
||||
"""
|
||||
now = datetime.now(timezone.utc)
|
||||
return Task(
|
||||
taskId=task_id or generate_task_id(),
|
||||
task_id=task_id or generate_task_id(),
|
||||
status=TASK_STATUS_WORKING,
|
||||
createdAt=now,
|
||||
lastUpdatedAt=now,
|
||||
created_at=now,
|
||||
last_updated_at=now,
|
||||
ttl=metadata.ttl,
|
||||
pollInterval=500, # Default 500ms poll interval
|
||||
poll_interval=500, # Default 500ms poll interval
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -79,14 +79,14 @@ class InMemoryTaskStore(TaskStore):
|
||||
|
||||
task = create_task_state(metadata, task_id)
|
||||
|
||||
if task.taskId in self._tasks:
|
||||
raise ValueError(f"Task with ID {task.taskId} already exists")
|
||||
if task.task_id in self._tasks:
|
||||
raise ValueError(f"Task with ID {task.task_id} already exists")
|
||||
|
||||
stored = StoredTask(
|
||||
task=task,
|
||||
expires_at=self._calculate_expiry(metadata.ttl),
|
||||
)
|
||||
self._tasks[task.taskId] = stored
|
||||
self._tasks[task.task_id] = stored
|
||||
|
||||
# Return a copy to prevent external modification
|
||||
return Task(**task.model_dump())
|
||||
@@ -124,10 +124,10 @@ class InMemoryTaskStore(TaskStore):
|
||||
status_changed = True
|
||||
|
||||
if status_message is not None:
|
||||
stored.task.statusMessage = status_message
|
||||
stored.task.status_message = status_message
|
||||
|
||||
# Update lastUpdatedAt on any change
|
||||
stored.task.lastUpdatedAt = datetime.now(timezone.utc)
|
||||
# Update last_updated_at on any change
|
||||
stored.task.last_updated_at = datetime.now(timezone.utc)
|
||||
|
||||
# If task is now terminal and has TTL, reset expiry timer
|
||||
if status is not None and is_terminal(status) and stored.task.ttl is not None:
|
||||
|
||||
@@ -41,5 +41,5 @@ async def poll_until_terminal(
|
||||
if is_terminal(status.status):
|
||||
break
|
||||
|
||||
interval_ms = status.pollInterval if status.pollInterval is not None else default_interval_ms
|
||||
interval_ms = status.poll_interval if status.poll_interval is not None else default_interval_ms
|
||||
await anyio.sleep(interval_ms / 1000)
|
||||
|
||||
@@ -48,10 +48,10 @@ def progress(
|
||||
ProgressContext[SendRequestT, SendNotificationT, SendResultT, ReceiveRequestT, ReceiveNotificationT],
|
||||
None,
|
||||
]:
|
||||
if ctx.meta is None or ctx.meta.progressToken is None: # pragma: no cover
|
||||
if ctx.meta is None or ctx.meta.progress_token is None: # pragma: no cover
|
||||
raise ValueError("No progress token provided")
|
||||
|
||||
progress_ctx = ProgressContext(ctx.session, ctx.meta.progressToken, total)
|
||||
progress_ctx = ProgressContext(ctx.session, ctx.meta.progress_token, total)
|
||||
try:
|
||||
yield progress_ctx
|
||||
finally:
|
||||
|
||||
@@ -301,7 +301,7 @@ class BaseSession(
|
||||
if isinstance(response_or_error, JSONRPCError):
|
||||
raise McpError(response_or_error.error)
|
||||
else:
|
||||
return result_type.model_validate(response_or_error.result)
|
||||
return result_type.model_validate(response_or_error.result, by_name=False)
|
||||
|
||||
finally:
|
||||
self._response_streams.pop(request_id, None)
|
||||
@@ -356,7 +356,8 @@ class BaseSession(
|
||||
elif isinstance(message.message.root, JSONRPCRequest):
|
||||
try:
|
||||
validated_request = self._receive_request_type.model_validate(
|
||||
message.message.root.model_dump(by_alias=True, mode="json", exclude_none=True)
|
||||
message.message.root.model_dump(by_alias=True, mode="json", exclude_none=True),
|
||||
by_name=False,
|
||||
)
|
||||
responder = RequestResponder(
|
||||
request_id=message.message.root.id,
|
||||
@@ -393,17 +394,18 @@ class BaseSession(
|
||||
elif isinstance(message.message.root, JSONRPCNotification):
|
||||
try:
|
||||
notification = self._receive_notification_type.model_validate(
|
||||
message.message.root.model_dump(by_alias=True, mode="json", exclude_none=True)
|
||||
message.message.root.model_dump(by_alias=True, mode="json", exclude_none=True),
|
||||
by_name=False,
|
||||
)
|
||||
# Handle cancellation notifications
|
||||
if isinstance(notification.root, CancelledNotification):
|
||||
cancelled_id = notification.root.params.requestId
|
||||
cancelled_id = notification.root.params.request_id
|
||||
if cancelled_id in self._in_flight: # pragma: no branch
|
||||
await self._in_flight[cancelled_id].cancel()
|
||||
else:
|
||||
# Handle progress notifications callback
|
||||
if isinstance(notification.root, ProgressNotification): # pragma: no cover
|
||||
progress_token = notification.root.params.progressToken
|
||||
progress_token = notification.root.params.progress_token
|
||||
# If there is a progress callback for this token,
|
||||
# call it with the progress information
|
||||
if progress_token in self._progress_callbacks:
|
||||
|
||||
+64
-63
@@ -5,6 +5,7 @@ from datetime import datetime
|
||||
from typing import Annotated, Any, Final, Generic, Literal, TypeAlias, TypeVar
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, FileUrl, RootModel
|
||||
from pydantic.alias_generators import to_camel
|
||||
|
||||
LATEST_PROTOCOL_VERSION = "2025-11-25"
|
||||
|
||||
@@ -31,7 +32,7 @@ TASK_REQUIRED: Final[Literal["required"]] = "required"
|
||||
class MCPModel(BaseModel):
|
||||
"""Base class for all MCP protocol types. Allows extra fields for forward compatibility."""
|
||||
|
||||
model_config = ConfigDict(extra="allow")
|
||||
model_config = ConfigDict(extra="allow", alias_generator=to_camel, populate_by_name=True)
|
||||
|
||||
|
||||
class TaskMetadata(MCPModel):
|
||||
@@ -46,7 +47,7 @@ class TaskMetadata(MCPModel):
|
||||
|
||||
class RequestParams(MCPModel):
|
||||
class Meta(MCPModel):
|
||||
progressToken: ProgressToken | None = None
|
||||
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
|
||||
@@ -123,7 +124,7 @@ class Result(MCPModel):
|
||||
|
||||
|
||||
class PaginatedResult(Result):
|
||||
nextCursor: Cursor | None = None
|
||||
next_cursor: Cursor | None = None
|
||||
"""
|
||||
An opaque token representing the pagination position after the last returned result.
|
||||
If present, there may be more results available.
|
||||
@@ -228,7 +229,7 @@ class Icon(MCPModel):
|
||||
src: str
|
||||
"""URL or data URI for the icon."""
|
||||
|
||||
mimeType: str | None = None
|
||||
mime_type: str | None = None
|
||||
"""Optional MIME type for the icon."""
|
||||
|
||||
sizes: list[str] | None = None
|
||||
@@ -246,7 +247,7 @@ class Implementation(BaseMetadata):
|
||||
description: str | None = None
|
||||
"""An optional human-readable description of what this implementation does."""
|
||||
|
||||
websiteUrl: str | None = None
|
||||
website_url: str | None = None
|
||||
"""An optional URL of the website for this implementation."""
|
||||
|
||||
icons: list[Icon] | None = None
|
||||
@@ -256,7 +257,7 @@ class Implementation(BaseMetadata):
|
||||
class RootsCapability(MCPModel):
|
||||
"""Capability for root operations."""
|
||||
|
||||
listChanged: bool | None = None
|
||||
list_changed: bool | None = None
|
||||
"""Whether the client supports notifications for changes to the roots list."""
|
||||
|
||||
|
||||
@@ -331,7 +332,7 @@ class TasksCreateMessageCapability(MCPModel):
|
||||
class TasksSamplingCapability(MCPModel):
|
||||
"""Capability for tasks sampling operations."""
|
||||
|
||||
createMessage: TasksCreateMessageCapability | None = None
|
||||
create_message: TasksCreateMessageCapability | None = None
|
||||
|
||||
|
||||
class TasksCreateElicitationCapability(MCPModel):
|
||||
@@ -386,7 +387,7 @@ class ClientCapabilities(MCPModel):
|
||||
class PromptsCapability(MCPModel):
|
||||
"""Capability for prompts operations."""
|
||||
|
||||
listChanged: bool | None = None
|
||||
list_changed: bool | None = None
|
||||
"""Whether this server supports notifications for changes to the prompt list."""
|
||||
|
||||
|
||||
@@ -395,14 +396,14 @@ class ResourcesCapability(MCPModel):
|
||||
|
||||
subscribe: bool | None = None
|
||||
"""Whether this server supports subscribing to resource updates."""
|
||||
listChanged: bool | None = None
|
||||
list_changed: bool | None = None
|
||||
"""Whether this server supports notifications for changes to the resource list."""
|
||||
|
||||
|
||||
class ToolsCapability(MCPModel):
|
||||
"""Capability for tools operations."""
|
||||
|
||||
listChanged: bool | None = None
|
||||
list_changed: bool | None = None
|
||||
"""Whether this server supports notifications for changes to the tool list."""
|
||||
|
||||
|
||||
@@ -474,20 +475,20 @@ class RelatedTaskMetadata(MCPModel):
|
||||
Include this in the `_meta` field under the key `io.modelcontextprotocol/related-task`.
|
||||
"""
|
||||
|
||||
taskId: str
|
||||
task_id: str
|
||||
"""The task identifier this message is associated with."""
|
||||
|
||||
|
||||
class Task(MCPModel):
|
||||
"""Data associated with a task."""
|
||||
|
||||
taskId: str
|
||||
task_id: str
|
||||
"""The task identifier."""
|
||||
|
||||
status: TaskStatus
|
||||
"""Current task state."""
|
||||
|
||||
statusMessage: str | None = None
|
||||
status_message: str | None = None
|
||||
"""
|
||||
Optional human-readable message describing the current task state.
|
||||
This can provide context for any status, including:
|
||||
@@ -496,16 +497,16 @@ class Task(MCPModel):
|
||||
- Diagnostic information for "failed" status (e.g., error details, what went wrong)
|
||||
"""
|
||||
|
||||
createdAt: datetime # Pydantic will enforce ISO 8601 and re-serialize as a string later
|
||||
created_at: datetime # Pydantic will enforce ISO 8601 and re-serialize as a string later
|
||||
"""ISO 8601 timestamp when the task was created."""
|
||||
|
||||
lastUpdatedAt: datetime
|
||||
last_updated_at: datetime
|
||||
"""ISO 8601 timestamp when the task was last updated."""
|
||||
|
||||
ttl: Annotated[int, Field(strict=True)] | None
|
||||
"""Actual retention duration from creation in milliseconds, null for unlimited."""
|
||||
|
||||
pollInterval: Annotated[int, Field(strict=True)] | None = None
|
||||
poll_interval: Annotated[int, Field(strict=True)] | None = None
|
||||
"""Suggested polling interval in milliseconds."""
|
||||
|
||||
|
||||
@@ -516,7 +517,7 @@ class CreateTaskResult(Result):
|
||||
|
||||
|
||||
class GetTaskRequestParams(RequestParams):
|
||||
taskId: str
|
||||
task_id: str
|
||||
"""The task identifier to query."""
|
||||
|
||||
|
||||
@@ -533,7 +534,7 @@ class GetTaskResult(Result, Task):
|
||||
|
||||
|
||||
class GetTaskPayloadRequestParams(RequestParams):
|
||||
taskId: str
|
||||
task_id: str
|
||||
"""The task identifier to retrieve results for."""
|
||||
|
||||
|
||||
@@ -553,7 +554,7 @@ class GetTaskPayloadResult(Result):
|
||||
|
||||
|
||||
class CancelTaskRequestParams(RequestParams):
|
||||
taskId: str
|
||||
task_id: str
|
||||
"""The task identifier to cancel."""
|
||||
|
||||
|
||||
@@ -597,10 +598,10 @@ class TaskStatusNotification(Notification[TaskStatusNotificationParams, Literal[
|
||||
class InitializeRequestParams(RequestParams):
|
||||
"""Parameters for the initialize request."""
|
||||
|
||||
protocolVersion: str | int
|
||||
protocol_version: str | int
|
||||
"""The latest version of the Model Context Protocol that the client supports."""
|
||||
capabilities: ClientCapabilities
|
||||
clientInfo: Implementation
|
||||
client_info: Implementation
|
||||
|
||||
|
||||
class InitializeRequest(Request[InitializeRequestParams, Literal["initialize"]]):
|
||||
@@ -616,10 +617,10 @@ class InitializeRequest(Request[InitializeRequestParams, Literal["initialize"]])
|
||||
class InitializeResult(Result):
|
||||
"""After receiving an initialize request from the client, the server sends this."""
|
||||
|
||||
protocolVersion: str | int
|
||||
protocol_version: str | int
|
||||
"""The version of the Model Context Protocol that the server wants to use."""
|
||||
capabilities: ServerCapabilities
|
||||
serverInfo: Implementation
|
||||
server_info: Implementation
|
||||
instructions: str | None = None
|
||||
"""Instructions describing how to use the server and its features."""
|
||||
|
||||
@@ -647,7 +648,7 @@ class PingRequest(Request[RequestParams | None, Literal["ping"]]):
|
||||
class ProgressNotificationParams(NotificationParams):
|
||||
"""Parameters for progress notifications."""
|
||||
|
||||
progressToken: ProgressToken
|
||||
progress_token: ProgressToken
|
||||
"""
|
||||
The progress token which was given in the initial request, used to associate this
|
||||
notification with the request that is proceeding.
|
||||
@@ -694,7 +695,7 @@ class Resource(BaseMetadata):
|
||||
"""The URI of this resource."""
|
||||
description: str | None = None
|
||||
"""A description of what this resource represents."""
|
||||
mimeType: str | None = None
|
||||
mime_type: str | None = None
|
||||
"""The MIME type of this resource, if known."""
|
||||
size: int | None = None
|
||||
"""
|
||||
@@ -716,14 +717,14 @@ class Resource(BaseMetadata):
|
||||
class ResourceTemplate(BaseMetadata):
|
||||
"""A template description for resources available on the server."""
|
||||
|
||||
uriTemplate: str
|
||||
uri_template: str
|
||||
"""
|
||||
A URI template (according to RFC 6570) that can be used to construct resource
|
||||
URIs.
|
||||
"""
|
||||
description: str | None = None
|
||||
"""A human-readable description of what this template is for."""
|
||||
mimeType: str | None = None
|
||||
mime_type: str | None = None
|
||||
"""
|
||||
The MIME type for all resources that match this template. This should only be
|
||||
included if all resources matching this template have the same type.
|
||||
@@ -753,7 +754,7 @@ class ListResourceTemplatesRequest(PaginatedRequest[Literal["resources/templates
|
||||
class ListResourceTemplatesResult(PaginatedResult):
|
||||
"""The server's response to a resources/templates/list request from the client."""
|
||||
|
||||
resourceTemplates: list[ResourceTemplate]
|
||||
resource_templates: list[ResourceTemplate]
|
||||
|
||||
|
||||
class ReadResourceRequestParams(RequestParams):
|
||||
@@ -778,7 +779,7 @@ class ResourceContents(MCPModel):
|
||||
|
||||
uri: str
|
||||
"""The URI of this resource."""
|
||||
mimeType: str | None = None
|
||||
mime_type: str | None = None
|
||||
"""The MIME type of this resource, if known."""
|
||||
meta: dict[str, Any] | None = Field(alias="_meta", default=None)
|
||||
"""
|
||||
@@ -956,7 +957,7 @@ class ImageContent(MCPModel):
|
||||
type: Literal["image"] = "image"
|
||||
data: str
|
||||
"""The base64-encoded image data."""
|
||||
mimeType: str
|
||||
mime_type: str
|
||||
"""
|
||||
The MIME type of the image. Different providers may support different
|
||||
image types.
|
||||
@@ -975,7 +976,7 @@ class AudioContent(MCPModel):
|
||||
type: Literal["audio"] = "audio"
|
||||
data: str
|
||||
"""The base64-encoded audio data."""
|
||||
mimeType: str
|
||||
mime_type: str
|
||||
"""
|
||||
The MIME type of the audio. Different providers may support different
|
||||
audio types.
|
||||
@@ -1027,7 +1028,7 @@ class ToolResultContent(MCPModel):
|
||||
type: Literal["tool_result"] = "tool_result"
|
||||
"""Discriminator for tool result content."""
|
||||
|
||||
toolUseId: str
|
||||
tool_use_id: str
|
||||
"""The unique identifier that corresponds to the tool call's id field."""
|
||||
|
||||
content: list[ContentBlock] = []
|
||||
@@ -1036,12 +1037,12 @@ class ToolResultContent(MCPModel):
|
||||
Defaults to empty list if not provided.
|
||||
"""
|
||||
|
||||
structuredContent: dict[str, Any] | None = None
|
||||
structured_content: dict[str, Any] | None = None
|
||||
"""
|
||||
Optional structured tool output that matches the tool's outputSchema (if defined).
|
||||
"""
|
||||
|
||||
isError: bool | None = None
|
||||
is_error: bool | None = None
|
||||
"""Whether the tool execution resulted in an error."""
|
||||
|
||||
meta: dict[str, Any] | None = Field(alias="_meta", default=None)
|
||||
@@ -1161,29 +1162,29 @@ class ToolAnnotations(MCPModel):
|
||||
title: str | None = None
|
||||
"""A human-readable title for the tool."""
|
||||
|
||||
readOnlyHint: bool | None = None
|
||||
read_only_hint: bool | None = None
|
||||
"""
|
||||
If true, the tool does not modify its environment.
|
||||
Default: false
|
||||
"""
|
||||
|
||||
destructiveHint: bool | None = None
|
||||
destructive_hint: bool | None = None
|
||||
"""
|
||||
If true, the tool may perform destructive updates to its environment.
|
||||
If false, the tool performs only additive updates.
|
||||
(This property is meaningful only when `readOnlyHint == false`)
|
||||
(This property is meaningful only when `read_only_hint == false`)
|
||||
Default: true
|
||||
"""
|
||||
|
||||
idempotentHint: bool | None = None
|
||||
idempotent_hint: bool | None = None
|
||||
"""
|
||||
If true, calling the tool repeatedly with the same arguments
|
||||
will have no additional effect on the its environment.
|
||||
(This property is meaningful only when `readOnlyHint == false`)
|
||||
(This property is meaningful only when `read_only_hint == false`)
|
||||
Default: false
|
||||
"""
|
||||
|
||||
openWorldHint: bool | None = None
|
||||
open_world_hint: bool | None = None
|
||||
"""
|
||||
If true, this tool may interact with an "open world" of external
|
||||
entities. If false, the tool's domain of interaction is closed.
|
||||
@@ -1196,7 +1197,7 @@ class ToolAnnotations(MCPModel):
|
||||
class ToolExecution(MCPModel):
|
||||
"""Execution-related properties for a tool."""
|
||||
|
||||
taskSupport: TaskExecutionMode | None = None
|
||||
task_support: TaskExecutionMode | None = None
|
||||
"""
|
||||
Indicates whether this tool supports task-augmented execution.
|
||||
This allows clients to handle long-running operations through polling
|
||||
@@ -1215,12 +1216,12 @@ class Tool(BaseMetadata):
|
||||
|
||||
description: str | None = None
|
||||
"""A human-readable description of the tool."""
|
||||
inputSchema: dict[str, Any]
|
||||
input_schema: dict[str, Any]
|
||||
"""A JSON Schema object defining the expected parameters for the tool."""
|
||||
outputSchema: dict[str, Any] | None = None
|
||||
output_schema: dict[str, Any] | None = None
|
||||
"""
|
||||
An optional JSON Schema object defining the structure of the tool's output
|
||||
returned in the structuredContent field of a CallToolResult.
|
||||
returned in the structured_content field of a CallToolResult.
|
||||
"""
|
||||
icons: list[Icon] | None = None
|
||||
"""An optional list of icons for this tool."""
|
||||
@@ -1259,9 +1260,9 @@ class CallToolResult(Result):
|
||||
"""The server's response to a tool call."""
|
||||
|
||||
content: list[ContentBlock]
|
||||
structuredContent: dict[str, Any] | None = None
|
||||
structured_content: dict[str, Any] | None = None
|
||||
"""An optional JSON object that represents the structured result of the tool call."""
|
||||
isError: bool = False
|
||||
is_error: bool = False
|
||||
|
||||
|
||||
class ToolListChangedNotification(Notification[NotificationParams | None, Literal["notifications/tools/list_changed"]]):
|
||||
@@ -1349,21 +1350,21 @@ class ModelPreferences(MCPModel):
|
||||
MAY still use the priorities to select from ambiguous matches.
|
||||
"""
|
||||
|
||||
costPriority: float | None = None
|
||||
cost_priority: float | None = None
|
||||
"""
|
||||
How much to prioritize cost when selecting a model. A value of 0 means cost
|
||||
is not important, while a value of 1 means cost is the most important
|
||||
factor.
|
||||
"""
|
||||
|
||||
speedPriority: float | None = None
|
||||
speed_priority: float | None = None
|
||||
"""
|
||||
How much to prioritize sampling speed (latency) when selecting a model. A
|
||||
value of 0 means speed is not important, while a value of 1 means speed is
|
||||
the most important factor.
|
||||
"""
|
||||
|
||||
intelligencePriority: float | None = None
|
||||
intelligence_priority: float | None = None
|
||||
"""
|
||||
How much to prioritize intelligence and capabilities when selecting a
|
||||
model. A value of 0 means intelligence is not important, while a value of 1
|
||||
@@ -1392,22 +1393,22 @@ class CreateMessageRequestParams(RequestParams):
|
||||
"""Parameters for creating a message."""
|
||||
|
||||
messages: list[SamplingMessage]
|
||||
modelPreferences: ModelPreferences | None = None
|
||||
model_preferences: ModelPreferences | None = None
|
||||
"""
|
||||
The server's preferences for which model to select. The client MAY ignore
|
||||
these preferences.
|
||||
"""
|
||||
systemPrompt: str | None = None
|
||||
system_prompt: str | None = None
|
||||
"""An optional system prompt the server wants to use for sampling."""
|
||||
includeContext: IncludeContext | None = None
|
||||
include_context: IncludeContext | None = None
|
||||
"""
|
||||
A request to include context from one or more MCP servers (including the caller), to
|
||||
be attached to the prompt.
|
||||
"""
|
||||
temperature: float | None = None
|
||||
maxTokens: int
|
||||
max_tokens: int
|
||||
"""The maximum number of tokens to sample, as requested by the server."""
|
||||
stopSequences: list[str] | None = None
|
||||
stop_sequences: list[str] | None = None
|
||||
metadata: dict[str, Any] | None = None
|
||||
"""Optional metadata to pass through to the LLM provider."""
|
||||
tools: list[Tool] | None = None
|
||||
@@ -1415,7 +1416,7 @@ class CreateMessageRequestParams(RequestParams):
|
||||
Tool definitions for the LLM to use during sampling.
|
||||
Requires clientCapabilities.sampling.tools to be present.
|
||||
"""
|
||||
toolChoice: ToolChoice | None = None
|
||||
tool_choice: ToolChoice | None = None
|
||||
"""
|
||||
Controls tool usage behavior.
|
||||
Requires clientCapabilities.sampling.tools and the tools parameter to be present.
|
||||
@@ -1445,7 +1446,7 @@ class CreateMessageResult(Result):
|
||||
"""Response content. Single content block (text, image, or audio)."""
|
||||
model: str
|
||||
"""The name of the model that generated the message."""
|
||||
stopReason: StopReason | None = None
|
||||
stop_reason: StopReason | None = None
|
||||
"""The reason why sampling stopped, if known."""
|
||||
|
||||
|
||||
@@ -1460,11 +1461,11 @@ class CreateMessageResultWithTools(Result):
|
||||
content: SamplingMessageContentBlock | list[SamplingMessageContentBlock]
|
||||
"""
|
||||
Response content. May be a single content block or an array.
|
||||
May include ToolUseContent if stopReason is 'toolUse'.
|
||||
May include ToolUseContent if stop_reason is 'toolUse'.
|
||||
"""
|
||||
model: str
|
||||
"""The name of the model that generated the message."""
|
||||
stopReason: StopReason | None = None
|
||||
stop_reason: StopReason | None = None
|
||||
"""
|
||||
The reason why sampling stopped, if known.
|
||||
'toolUse' indicates the model wants to use a tool.
|
||||
@@ -1535,7 +1536,7 @@ class Completion(MCPModel):
|
||||
The total number of completion options available. This can exceed the number of
|
||||
values actually sent in the response.
|
||||
"""
|
||||
hasMore: bool | None = None
|
||||
has_more: bool | None = None
|
||||
"""
|
||||
Indicates whether there are additional completion options beyond those provided in
|
||||
the current response, even if the exact total is unknown.
|
||||
@@ -1614,7 +1615,7 @@ class RootsListChangedNotification(
|
||||
class CancelledNotificationParams(NotificationParams):
|
||||
"""Parameters for cancellation notifications."""
|
||||
|
||||
requestId: RequestId | None = None
|
||||
request_id: RequestId | None = None
|
||||
"""
|
||||
The ID of the request to cancel.
|
||||
|
||||
@@ -1639,7 +1640,7 @@ class CancelledNotification(Notification[CancelledNotificationParams, Literal["n
|
||||
class ElicitCompleteNotificationParams(NotificationParams):
|
||||
"""Parameters for elicitation completion notifications."""
|
||||
|
||||
elicitationId: str
|
||||
elicitation_id: str
|
||||
"""The unique identifier of the elicitation that was completed."""
|
||||
|
||||
|
||||
@@ -1716,7 +1717,7 @@ class ElicitRequestFormParams(RequestParams):
|
||||
message: str
|
||||
"""The message to present to the user describing what information is being requested."""
|
||||
|
||||
requestedSchema: ElicitRequestedSchema
|
||||
requested_schema: ElicitRequestedSchema
|
||||
"""
|
||||
A restricted subset of JSON Schema defining the structure of expected response.
|
||||
Only top-level properties are allowed, without nesting.
|
||||
@@ -1739,7 +1740,7 @@ class ElicitRequestURLParams(RequestParams):
|
||||
url: str
|
||||
"""The URL that the user should navigate to."""
|
||||
|
||||
elicitationId: str
|
||||
elicitation_id: str
|
||||
"""
|
||||
The ID of the elicitation, which must be unique within the context of the server.
|
||||
The client MUST treat this ID as an opaque value.
|
||||
|
||||
@@ -61,7 +61,7 @@ def run_unicode_server(port: int) -> None: # pragma: no cover
|
||||
Tool(
|
||||
name="echo_unicode",
|
||||
description="🔤 Echo Unicode text - Hello 👋 World 🌍 - Testing 🧪 Unicode ✨",
|
||||
inputSchema={
|
||||
input_schema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"text": {"type": "string", "description": "Text to echo back"},
|
||||
|
||||
@@ -45,7 +45,7 @@ async def test_list_roots_callback():
|
||||
async with create_session(server._mcp_server, list_roots_callback=list_roots_callback) as client_session:
|
||||
# Make a request to trigger sampling callback
|
||||
result = await client_session.call_tool("test_list_roots", {"message": "test message"})
|
||||
assert result.isError is False
|
||||
assert result.is_error is False
|
||||
assert isinstance(result.content[0], TextContent)
|
||||
assert result.content[0].text == "true"
|
||||
|
||||
@@ -53,6 +53,6 @@ async def test_list_roots_callback():
|
||||
async with create_session(server._mcp_server) as client_session:
|
||||
# Make a request to trigger sampling callback
|
||||
result = await client_session.call_tool("test_list_roots", {"message": "test message"})
|
||||
assert result.isError is True
|
||||
assert result.is_error is True
|
||||
assert isinstance(result.content[0], TextContent)
|
||||
assert result.content[0].text == "Error executing tool test_list_roots: List roots not supported"
|
||||
|
||||
@@ -78,7 +78,7 @@ async def test_logging_callback():
|
||||
) as client_session:
|
||||
# First verify our test tool works
|
||||
result = await client_session.call_tool("test_tool", {})
|
||||
assert result.isError is False
|
||||
assert result.is_error is False
|
||||
assert isinstance(result.content[0], TextContent)
|
||||
assert result.content[0].text == "true"
|
||||
|
||||
@@ -101,8 +101,8 @@ async def test_logging_callback():
|
||||
"extra_dict": {"a": 1, "b": 2, "c": 3},
|
||||
},
|
||||
)
|
||||
assert log_result.isError is False
|
||||
assert log_result_with_extra.isError is False
|
||||
assert log_result.is_error is False
|
||||
assert log_result_with_extra.is_error is False
|
||||
assert len(logging_collector.log_messages) == 2
|
||||
# Create meta object with related_request_id added dynamically
|
||||
log = logging_collector.log_messages[0]
|
||||
|
||||
@@ -66,8 +66,8 @@ class TestClientOutputSchemaValidation:
|
||||
Tool(
|
||||
name="get_user",
|
||||
description="Get user data",
|
||||
inputSchema={"type": "object"},
|
||||
outputSchema=output_schema,
|
||||
input_schema={"type": "object"},
|
||||
output_schema=output_schema,
|
||||
)
|
||||
]
|
||||
|
||||
@@ -105,8 +105,8 @@ class TestClientOutputSchemaValidation:
|
||||
Tool(
|
||||
name="calculate",
|
||||
description="Calculate something",
|
||||
inputSchema={"type": "object"},
|
||||
outputSchema=output_schema,
|
||||
input_schema={"type": "object"},
|
||||
output_schema=output_schema,
|
||||
)
|
||||
]
|
||||
|
||||
@@ -136,8 +136,8 @@ class TestClientOutputSchemaValidation:
|
||||
Tool(
|
||||
name="get_scores",
|
||||
description="Get scores",
|
||||
inputSchema={"type": "object"},
|
||||
outputSchema=output_schema,
|
||||
input_schema={"type": "object"},
|
||||
output_schema=output_schema,
|
||||
)
|
||||
]
|
||||
|
||||
@@ -171,8 +171,8 @@ class TestClientOutputSchemaValidation:
|
||||
Tool(
|
||||
name="get_person",
|
||||
description="Get person data",
|
||||
inputSchema={"type": "object"},
|
||||
outputSchema=output_schema,
|
||||
input_schema={"type": "object"},
|
||||
output_schema=output_schema,
|
||||
)
|
||||
]
|
||||
|
||||
@@ -190,7 +190,7 @@ class TestClientOutputSchemaValidation:
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_tool_not_listed_warning(self, caplog: pytest.LogCaptureFixture):
|
||||
"""Test that client logs warning when tool is not in list_tools but has outputSchema"""
|
||||
"""Test that client logs warning when tool is not in list_tools but has output_schema"""
|
||||
server = Server("test-server")
|
||||
|
||||
@server.list_tools()
|
||||
@@ -210,8 +210,8 @@ class TestClientOutputSchemaValidation:
|
||||
async with client_session(server) as client:
|
||||
# Call a tool that wasn't listed
|
||||
result = await client.call_tool("mystery_tool", {})
|
||||
assert result.structuredContent == {"result": 42}
|
||||
assert result.isError is False
|
||||
assert result.structured_content == {"result": 42}
|
||||
assert result.is_error is False
|
||||
|
||||
# Check that warning was logged
|
||||
assert "Tool mystery_tool not listed" in caplog.text
|
||||
|
||||
@@ -25,7 +25,7 @@ async def test_sampling_callback():
|
||||
role="assistant",
|
||||
content=TextContent(type="text", text="This is a response from the sampling callback"),
|
||||
model="test-model",
|
||||
stopReason="endTurn",
|
||||
stop_reason="endTurn",
|
||||
)
|
||||
|
||||
async def sampling_callback(
|
||||
@@ -47,7 +47,7 @@ async def test_sampling_callback():
|
||||
async with create_session(server._mcp_server, sampling_callback=sampling_callback) as client_session:
|
||||
# Make a request to trigger sampling callback
|
||||
result = await client_session.call_tool("test_sampling", {"message": "Test message for sampling"})
|
||||
assert result.isError is False
|
||||
assert result.is_error is False
|
||||
assert isinstance(result.content[0], TextContent)
|
||||
assert result.content[0].text == "true"
|
||||
|
||||
@@ -55,7 +55,7 @@ async def test_sampling_callback():
|
||||
async with create_session(server._mcp_server) as client_session:
|
||||
# Make a request to trigger sampling callback
|
||||
result = await client_session.call_tool("test_sampling", {"message": "Test message for sampling"})
|
||||
assert result.isError is True
|
||||
assert result.is_error is True
|
||||
assert isinstance(result.content[0], TextContent)
|
||||
assert result.content[0].text == "Error executing tool test_sampling: Sampling not supported"
|
||||
|
||||
@@ -72,7 +72,7 @@ async def test_create_message_backwards_compat_single_content():
|
||||
role="assistant",
|
||||
content=TextContent(type="text", text="Hello from LLM"),
|
||||
model="test-model",
|
||||
stopReason="endTurn",
|
||||
stop_reason="endTurn",
|
||||
)
|
||||
|
||||
async def sampling_callback(
|
||||
@@ -99,7 +99,7 @@ async def test_create_message_backwards_compat_single_content():
|
||||
|
||||
async with create_session(server._mcp_server, sampling_callback=sampling_callback) as client_session:
|
||||
result = await client_session.call_tool("test_backwards_compat", {"message": "Test"})
|
||||
assert result.isError is False
|
||||
assert result.is_error is False
|
||||
assert isinstance(result.content[0], TextContent)
|
||||
assert result.content[0].text == "true"
|
||||
|
||||
@@ -112,7 +112,7 @@ async def test_create_message_result_with_tools_type():
|
||||
role="assistant",
|
||||
content=ToolUseContent(type="tool_use", id="call_123", name="get_weather", input={"city": "SF"}),
|
||||
model="test-model",
|
||||
stopReason="toolUse",
|
||||
stop_reason="toolUse",
|
||||
)
|
||||
|
||||
# CreateMessageResultWithTools should have content_as_list
|
||||
@@ -128,7 +128,7 @@ async def test_create_message_result_with_tools_type():
|
||||
ToolUseContent(type="tool_use", id="call_456", name="get_weather", input={"city": "NYC"}),
|
||||
],
|
||||
model="test-model",
|
||||
stopReason="toolUse",
|
||||
stop_reason="toolUse",
|
||||
)
|
||||
content_list_array = result_array.content_as_list
|
||||
assert len(content_list_array) == 2
|
||||
|
||||
@@ -49,7 +49,7 @@ async def test_client_session_initialize():
|
||||
|
||||
result = ServerResult(
|
||||
InitializeResult(
|
||||
protocolVersion=LATEST_PROTOCOL_VERSION,
|
||||
protocol_version=LATEST_PROTOCOL_VERSION,
|
||||
capabilities=ServerCapabilities(
|
||||
logging=None,
|
||||
resources=None,
|
||||
@@ -57,7 +57,7 @@ async def test_client_session_initialize():
|
||||
experimental=None,
|
||||
prompts=None,
|
||||
),
|
||||
serverInfo=Implementation(name="mock-server", version="0.1.0"),
|
||||
server_info=Implementation(name="mock-server", version="0.1.0"),
|
||||
instructions="The server instructions.",
|
||||
)
|
||||
)
|
||||
@@ -105,9 +105,9 @@ async def test_client_session_initialize():
|
||||
|
||||
# Assert the result
|
||||
assert isinstance(result, InitializeResult)
|
||||
assert result.protocolVersion == LATEST_PROTOCOL_VERSION
|
||||
assert result.protocol_version == LATEST_PROTOCOL_VERSION
|
||||
assert isinstance(result.capabilities, ServerCapabilities)
|
||||
assert result.serverInfo == Implementation(name="mock-server", version="0.1.0")
|
||||
assert result.server_info == Implementation(name="mock-server", version="0.1.0")
|
||||
assert result.instructions == "The server instructions."
|
||||
|
||||
# Check that the client sent the initialized notification
|
||||
@@ -133,13 +133,13 @@ async def test_client_session_custom_client_info():
|
||||
jsonrpc_request.model_dump(by_alias=True, mode="json", exclude_none=True)
|
||||
)
|
||||
assert isinstance(request.root, InitializeRequest)
|
||||
received_client_info = request.root.params.clientInfo
|
||||
received_client_info = request.root.params.client_info
|
||||
|
||||
result = ServerResult(
|
||||
InitializeResult(
|
||||
protocolVersion=LATEST_PROTOCOL_VERSION,
|
||||
protocol_version=LATEST_PROTOCOL_VERSION,
|
||||
capabilities=ServerCapabilities(),
|
||||
serverInfo=Implementation(name="mock-server", version="0.1.0"),
|
||||
server_info=Implementation(name="mock-server", version="0.1.0"),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -194,13 +194,13 @@ async def test_client_session_default_client_info():
|
||||
jsonrpc_request.model_dump(by_alias=True, mode="json", exclude_none=True)
|
||||
)
|
||||
assert isinstance(request.root, InitializeRequest)
|
||||
received_client_info = request.root.params.clientInfo
|
||||
received_client_info = request.root.params.client_info
|
||||
|
||||
result = ServerResult(
|
||||
InitializeResult(
|
||||
protocolVersion=LATEST_PROTOCOL_VERSION,
|
||||
protocol_version=LATEST_PROTOCOL_VERSION,
|
||||
capabilities=ServerCapabilities(),
|
||||
serverInfo=Implementation(name="mock-server", version="0.1.0"),
|
||||
server_info=Implementation(name="mock-server", version="0.1.0"),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -254,14 +254,14 @@ async def test_client_session_version_negotiation_success():
|
||||
assert isinstance(request.root, InitializeRequest)
|
||||
|
||||
# Verify client sent the latest protocol version
|
||||
assert request.root.params.protocolVersion == LATEST_PROTOCOL_VERSION
|
||||
assert request.root.params.protocol_version == LATEST_PROTOCOL_VERSION
|
||||
|
||||
# Server responds with a supported older version
|
||||
result = ServerResult(
|
||||
InitializeResult(
|
||||
protocolVersion="2024-11-05",
|
||||
protocol_version="2024-11-05",
|
||||
capabilities=ServerCapabilities(),
|
||||
serverInfo=Implementation(name="mock-server", version="0.1.0"),
|
||||
server_info=Implementation(name="mock-server", version="0.1.0"),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -296,8 +296,8 @@ async def test_client_session_version_negotiation_success():
|
||||
|
||||
# Assert the result with negotiated version
|
||||
assert isinstance(result, InitializeResult)
|
||||
assert result.protocolVersion == "2024-11-05"
|
||||
assert result.protocolVersion in SUPPORTED_PROTOCOL_VERSIONS
|
||||
assert result.protocol_version == "2024-11-05"
|
||||
assert result.protocol_version in SUPPORTED_PROTOCOL_VERSIONS
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@@ -318,9 +318,9 @@ async def test_client_session_version_negotiation_failure():
|
||||
# Server responds with an unsupported version
|
||||
result = ServerResult(
|
||||
InitializeResult(
|
||||
protocolVersion="2020-01-01", # Unsupported old version
|
||||
protocol_version="2020-01-01", # Unsupported old version
|
||||
capabilities=ServerCapabilities(),
|
||||
serverInfo=Implementation(name="mock-server", version="0.1.0"),
|
||||
server_info=Implementation(name="mock-server", version="0.1.0"),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -377,9 +377,9 @@ async def test_client_capabilities_default():
|
||||
|
||||
result = ServerResult(
|
||||
InitializeResult(
|
||||
protocolVersion=LATEST_PROTOCOL_VERSION,
|
||||
protocol_version=LATEST_PROTOCOL_VERSION,
|
||||
capabilities=ServerCapabilities(),
|
||||
serverInfo=Implementation(name="mock-server", version="0.1.0"),
|
||||
server_info=Implementation(name="mock-server", version="0.1.0"),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -455,9 +455,9 @@ async def test_client_capabilities_with_custom_callbacks():
|
||||
|
||||
result = ServerResult(
|
||||
InitializeResult(
|
||||
protocolVersion=LATEST_PROTOCOL_VERSION,
|
||||
protocol_version=LATEST_PROTOCOL_VERSION,
|
||||
capabilities=ServerCapabilities(),
|
||||
serverInfo=Implementation(name="mock-server", version="0.1.0"),
|
||||
server_info=Implementation(name="mock-server", version="0.1.0"),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -503,7 +503,7 @@ async def test_client_capabilities_with_custom_callbacks():
|
||||
assert received_capabilities.roots is not None
|
||||
assert isinstance(received_capabilities.roots, types.RootsCapability)
|
||||
# Should be True for custom callback
|
||||
assert received_capabilities.roots.listChanged is True
|
||||
assert received_capabilities.roots.list_changed is True
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@@ -538,9 +538,9 @@ async def test_client_capabilities_with_sampling_tools():
|
||||
|
||||
result = ServerResult(
|
||||
InitializeResult(
|
||||
protocolVersion=LATEST_PROTOCOL_VERSION,
|
||||
protocol_version=LATEST_PROTOCOL_VERSION,
|
||||
capabilities=ServerCapabilities(),
|
||||
serverInfo=Implementation(name="mock-server", version="0.1.0"),
|
||||
server_info=Implementation(name="mock-server", version="0.1.0"),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -592,9 +592,9 @@ async def test_get_server_capabilities():
|
||||
|
||||
expected_capabilities = ServerCapabilities(
|
||||
logging=types.LoggingCapability(),
|
||||
prompts=types.PromptsCapability(listChanged=True),
|
||||
resources=types.ResourcesCapability(subscribe=True, listChanged=True),
|
||||
tools=types.ToolsCapability(listChanged=False),
|
||||
prompts=types.PromptsCapability(list_changed=True),
|
||||
resources=types.ResourcesCapability(subscribe=True, list_changed=True),
|
||||
tools=types.ToolsCapability(list_changed=False),
|
||||
)
|
||||
|
||||
async def mock_server():
|
||||
@@ -608,9 +608,9 @@ async def test_get_server_capabilities():
|
||||
|
||||
result = ServerResult(
|
||||
InitializeResult(
|
||||
protocolVersion=LATEST_PROTOCOL_VERSION,
|
||||
protocol_version=LATEST_PROTOCOL_VERSION,
|
||||
capabilities=expected_capabilities,
|
||||
serverInfo=Implementation(name="mock-server", version="0.1.0"),
|
||||
server_info=Implementation(name="mock-server", version="0.1.0"),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -649,11 +649,11 @@ async def test_get_server_capabilities():
|
||||
assert capabilities == expected_capabilities
|
||||
assert capabilities.logging is not None
|
||||
assert capabilities.prompts is not None
|
||||
assert capabilities.prompts.listChanged is True
|
||||
assert capabilities.prompts.list_changed is True
|
||||
assert capabilities.resources is not None
|
||||
assert capabilities.resources.subscribe is True
|
||||
assert capabilities.tools is not None
|
||||
assert capabilities.tools.listChanged is False
|
||||
assert capabilities.tools.list_changed is False
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@@ -663,7 +663,7 @@ async def test_client_tool_call_with_meta(meta: dict[str, Any] | None):
|
||||
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)
|
||||
|
||||
mocked_tool = types.Tool(name="sample_tool", inputSchema={})
|
||||
mocked_tool = types.Tool(name="sample_tool", input_schema={})
|
||||
|
||||
async def mock_server():
|
||||
# Receive initialization request from client
|
||||
@@ -677,9 +677,9 @@ async def test_client_tool_call_with_meta(meta: dict[str, Any] | None):
|
||||
|
||||
result = ServerResult(
|
||||
InitializeResult(
|
||||
protocolVersion=LATEST_PROTOCOL_VERSION,
|
||||
protocol_version=LATEST_PROTOCOL_VERSION,
|
||||
capabilities=ServerCapabilities(),
|
||||
serverInfo=Implementation(name="mock-server", version="0.1.0"),
|
||||
server_info=Implementation(name="mock-server", version="0.1.0"),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -712,7 +712,7 @@ async def test_client_tool_call_with_meta(meta: dict[str, Any] | None):
|
||||
assert jsonrpc_request.root.params["_meta"] == meta
|
||||
|
||||
result = ServerResult(
|
||||
CallToolResult(content=[TextContent(type="text", text="Called successfully")], isError=False)
|
||||
CallToolResult(content=[TextContent(type="text", text="Called successfully")], is_error=False)
|
||||
)
|
||||
|
||||
# Send the tools/call result
|
||||
|
||||
@@ -59,7 +59,7 @@ class TestClientSessionGroup:
|
||||
return f"{(server_info.name)}-{name}"
|
||||
|
||||
mcp_session_group = ClientSessionGroup(component_name_hook=hook)
|
||||
mcp_session_group._tools = {"server1-my_tool": types.Tool(name="my_tool", inputSchema={})}
|
||||
mcp_session_group._tools = {"server1-my_tool": types.Tool(name="my_tool", input_schema={})}
|
||||
mcp_session_group._tool_to_session = {"server1-my_tool": mock_session}
|
||||
text_content = types.TextContent(type="text", text="OK")
|
||||
mock_session.call_tool.return_value = types.CallToolResult(content=[text_content])
|
||||
@@ -324,7 +324,7 @@ class TestClientSessionGroup:
|
||||
|
||||
# Mock session.initialize()
|
||||
mock_initialize_result = mock.AsyncMock(name="InitializeResult")
|
||||
mock_initialize_result.serverInfo = types.Implementation(name="foo", version="1")
|
||||
mock_initialize_result.server_info = types.Implementation(name="foo", version="1")
|
||||
mock_entered_session.initialize.return_value = mock_initialize_result
|
||||
|
||||
# --- Test Execution ---
|
||||
@@ -381,5 +381,5 @@ class TestClientSessionGroup:
|
||||
mock_entered_session.initialize.assert_awaited_once()
|
||||
|
||||
# 3. Assert returned values
|
||||
assert returned_server_info is mock_initialize_result.serverInfo
|
||||
assert returned_server_info is mock_initialize_result.server_info
|
||||
assert returned_session is mock_entered_session
|
||||
|
||||
@@ -45,9 +45,9 @@ async def test_client_capabilities_without_tasks():
|
||||
|
||||
result = ServerResult(
|
||||
InitializeResult(
|
||||
protocolVersion=LATEST_PROTOCOL_VERSION,
|
||||
protocol_version=LATEST_PROTOCOL_VERSION,
|
||||
capabilities=ServerCapabilities(),
|
||||
serverInfo=Implementation(name="mock-server", version="0.1.0"),
|
||||
server_info=Implementation(name="mock-server", version="0.1.0"),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -119,9 +119,9 @@ async def test_client_capabilities_with_tasks():
|
||||
|
||||
result = ServerResult(
|
||||
InitializeResult(
|
||||
protocolVersion=LATEST_PROTOCOL_VERSION,
|
||||
protocol_version=LATEST_PROTOCOL_VERSION,
|
||||
capabilities=ServerCapabilities(),
|
||||
serverInfo=Implementation(name="mock-server", version="0.1.0"),
|
||||
server_info=Implementation(name="mock-server", version="0.1.0"),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -203,9 +203,9 @@ async def test_client_capabilities_auto_built_from_handlers():
|
||||
|
||||
result = ServerResult(
|
||||
InitializeResult(
|
||||
protocolVersion=LATEST_PROTOCOL_VERSION,
|
||||
protocol_version=LATEST_PROTOCOL_VERSION,
|
||||
capabilities=ServerCapabilities(),
|
||||
serverInfo=Implementation(name="mock-server", version="0.1.0"),
|
||||
server_info=Implementation(name="mock-server", version="0.1.0"),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -283,9 +283,9 @@ async def test_client_capabilities_with_task_augmented_handlers():
|
||||
|
||||
result = ServerResult(
|
||||
InitializeResult(
|
||||
protocolVersion=LATEST_PROTOCOL_VERSION,
|
||||
protocol_version=LATEST_PROTOCOL_VERSION,
|
||||
capabilities=ServerCapabilities(),
|
||||
serverInfo=Implementation(name="mock-server", version="0.1.0"),
|
||||
server_info=Implementation(name="mock-server", version="0.1.0"),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -117,17 +117,17 @@ async def test_client_handles_get_task_request(client_streams: ClientTestStreams
|
||||
params: GetTaskRequestParams,
|
||||
) -> GetTaskResult | ErrorData:
|
||||
nonlocal received_task_id
|
||||
received_task_id = params.taskId
|
||||
task = await store.get_task(params.taskId)
|
||||
assert task is not None, f"Test setup error: task {params.taskId} should exist"
|
||||
received_task_id = params.task_id
|
||||
task = await store.get_task(params.task_id)
|
||||
assert task is not None, f"Test setup error: task {params.task_id} should exist"
|
||||
return GetTaskResult(
|
||||
taskId=task.taskId,
|
||||
task_id=task.task_id,
|
||||
status=task.status,
|
||||
statusMessage=task.statusMessage,
|
||||
createdAt=task.createdAt,
|
||||
lastUpdatedAt=task.lastUpdatedAt,
|
||||
status_message=task.status_message,
|
||||
created_at=task.created_at,
|
||||
last_updated_at=task.last_updated_at,
|
||||
ttl=task.ttl,
|
||||
pollInterval=task.pollInterval,
|
||||
poll_interval=task.poll_interval,
|
||||
)
|
||||
|
||||
await store.create_task(TaskMetadata(ttl=60000), task_id="test-task-123")
|
||||
@@ -150,7 +150,7 @@ async def test_client_handles_get_task_request(client_streams: ClientTestStreams
|
||||
tg.start_soon(run_client)
|
||||
await client_ready.wait()
|
||||
|
||||
typed_request = GetTaskRequest(params=GetTaskRequestParams(taskId="test-task-123"))
|
||||
typed_request = GetTaskRequest(params=GetTaskRequestParams(task_id="test-task-123"))
|
||||
request = types.JSONRPCRequest(
|
||||
jsonrpc="2.0",
|
||||
id="req-1",
|
||||
@@ -164,7 +164,7 @@ async def test_client_handles_get_task_request(client_streams: ClientTestStreams
|
||||
assert response.id == "req-1"
|
||||
|
||||
result = GetTaskResult.model_validate(response.result)
|
||||
assert result.taskId == "test-task-123"
|
||||
assert result.task_id == "test-task-123"
|
||||
assert result.status == "working"
|
||||
assert received_task_id == "test-task-123"
|
||||
|
||||
@@ -183,8 +183,8 @@ async def test_client_handles_get_task_result_request(client_streams: ClientTest
|
||||
context: RequestContext[ClientSession, None],
|
||||
params: GetTaskPayloadRequestParams,
|
||||
) -> GetTaskPayloadResult | ErrorData:
|
||||
result = await store.get_result(params.taskId)
|
||||
assert result is not None, f"Test setup error: result for {params.taskId} should exist"
|
||||
result = await store.get_result(params.task_id)
|
||||
assert result is not None, f"Test setup error: result for {params.task_id} should exist"
|
||||
assert isinstance(result, types.CallToolResult)
|
||||
return GetTaskPayloadResult(**result.model_dump())
|
||||
|
||||
@@ -213,7 +213,7 @@ async def test_client_handles_get_task_result_request(client_streams: ClientTest
|
||||
tg.start_soon(run_client)
|
||||
await client_ready.wait()
|
||||
|
||||
typed_request = GetTaskPayloadRequest(params=GetTaskPayloadRequestParams(taskId="test-task-456"))
|
||||
typed_request = GetTaskPayloadRequest(params=GetTaskPayloadRequestParams(task_id="test-task-456"))
|
||||
request = types.JSONRPCRequest(
|
||||
jsonrpc="2.0",
|
||||
id="req-2",
|
||||
@@ -248,7 +248,7 @@ async def test_client_handles_list_tasks_request(client_streams: ClientTestStrea
|
||||
) -> ListTasksResult | ErrorData:
|
||||
cursor = params.cursor if params else None
|
||||
tasks_list, next_cursor = await store.list_tasks(cursor=cursor)
|
||||
return ListTasksResult(tasks=tasks_list, nextCursor=next_cursor)
|
||||
return ListTasksResult(tasks=tasks_list, next_cursor=next_cursor)
|
||||
|
||||
await store.create_task(TaskMetadata(ttl=60000), task_id="task-1")
|
||||
await store.create_task(TaskMetadata(ttl=60000), task_id="task-2")
|
||||
@@ -301,16 +301,16 @@ async def test_client_handles_cancel_task_request(client_streams: ClientTestStre
|
||||
context: RequestContext[ClientSession, None],
|
||||
params: CancelTaskRequestParams,
|
||||
) -> CancelTaskResult | ErrorData:
|
||||
task = await store.get_task(params.taskId)
|
||||
assert task is not None, f"Test setup error: task {params.taskId} should exist"
|
||||
await store.update_task(params.taskId, status="cancelled")
|
||||
updated = await store.get_task(params.taskId)
|
||||
task = await store.get_task(params.task_id)
|
||||
assert task is not None, f"Test setup error: task {params.task_id} should exist"
|
||||
await store.update_task(params.task_id, status="cancelled")
|
||||
updated = await store.get_task(params.task_id)
|
||||
assert updated is not None
|
||||
return CancelTaskResult(
|
||||
taskId=updated.taskId,
|
||||
task_id=updated.task_id,
|
||||
status=updated.status,
|
||||
createdAt=updated.createdAt,
|
||||
lastUpdatedAt=updated.lastUpdatedAt,
|
||||
created_at=updated.created_at,
|
||||
last_updated_at=updated.last_updated_at,
|
||||
ttl=updated.ttl,
|
||||
)
|
||||
|
||||
@@ -334,7 +334,7 @@ async def test_client_handles_cancel_task_request(client_streams: ClientTestStre
|
||||
tg.start_soon(run_client)
|
||||
await client_ready.wait()
|
||||
|
||||
typed_request = CancelTaskRequest(params=CancelTaskRequestParams(taskId="task-to-cancel"))
|
||||
typed_request = CancelTaskRequest(params=CancelTaskRequestParams(task_id="task-to-cancel"))
|
||||
request = types.JSONRPCRequest(
|
||||
jsonrpc="2.0",
|
||||
id="req-4",
|
||||
@@ -347,7 +347,7 @@ async def test_client_handles_cancel_task_request(client_streams: ClientTestStre
|
||||
assert isinstance(response, types.JSONRPCResponse)
|
||||
|
||||
result = CancelTaskResult.model_validate(response.result)
|
||||
assert result.taskId == "task-to-cancel"
|
||||
assert result.task_id == "task-to-cancel"
|
||||
assert result.status == "cancelled"
|
||||
|
||||
tg.cancel_scope.cancel()
|
||||
@@ -370,17 +370,17 @@ async def test_client_task_augmented_sampling(client_streams: ClientTestStreams)
|
||||
task_metadata: TaskMetadata,
|
||||
) -> CreateTaskResult:
|
||||
task = await store.create_task(task_metadata)
|
||||
created_task_id[0] = task.taskId
|
||||
created_task_id[0] = task.task_id
|
||||
|
||||
async def do_sampling() -> None:
|
||||
result = CreateMessageResult(
|
||||
role="assistant",
|
||||
content=TextContent(type="text", text="Sampled response"),
|
||||
model="test-model",
|
||||
stopReason="endTurn",
|
||||
stop_reason="endTurn",
|
||||
)
|
||||
await store.store_result(task.taskId, result)
|
||||
await store.update_task(task.taskId, status="completed")
|
||||
await store.store_result(task.task_id, result)
|
||||
await store.update_task(task.task_id, status="completed")
|
||||
sampling_completed.set()
|
||||
|
||||
assert background_tg[0] is not None
|
||||
@@ -391,24 +391,24 @@ async def test_client_task_augmented_sampling(client_streams: ClientTestStreams)
|
||||
context: RequestContext[ClientSession, None],
|
||||
params: GetTaskRequestParams,
|
||||
) -> GetTaskResult | ErrorData:
|
||||
task = await store.get_task(params.taskId)
|
||||
assert task is not None, f"Test setup error: task {params.taskId} should exist"
|
||||
task = await store.get_task(params.task_id)
|
||||
assert task is not None, f"Test setup error: task {params.task_id} should exist"
|
||||
return GetTaskResult(
|
||||
taskId=task.taskId,
|
||||
task_id=task.task_id,
|
||||
status=task.status,
|
||||
statusMessage=task.statusMessage,
|
||||
createdAt=task.createdAt,
|
||||
lastUpdatedAt=task.lastUpdatedAt,
|
||||
status_message=task.status_message,
|
||||
created_at=task.created_at,
|
||||
last_updated_at=task.last_updated_at,
|
||||
ttl=task.ttl,
|
||||
pollInterval=task.pollInterval,
|
||||
poll_interval=task.poll_interval,
|
||||
)
|
||||
|
||||
async def get_task_result_handler(
|
||||
context: RequestContext[ClientSession, None],
|
||||
params: GetTaskPayloadRequestParams,
|
||||
) -> GetTaskPayloadResult | ErrorData:
|
||||
result = await store.get_result(params.taskId)
|
||||
assert result is not None, f"Test setup error: result for {params.taskId} should exist"
|
||||
result = await store.get_result(params.task_id)
|
||||
assert result is not None, f"Test setup error: result for {params.task_id} should exist"
|
||||
assert isinstance(result, CreateMessageResult)
|
||||
return GetTaskPayloadResult(**result.model_dump())
|
||||
|
||||
@@ -439,7 +439,7 @@ async def test_client_task_augmented_sampling(client_streams: ClientTestStreams)
|
||||
typed_request = CreateMessageRequest(
|
||||
params=CreateMessageRequestParams(
|
||||
messages=[SamplingMessage(role="user", content=TextContent(type="text", text="Hello"))],
|
||||
maxTokens=100,
|
||||
max_tokens=100,
|
||||
task=TaskMetadata(ttl=60000),
|
||||
)
|
||||
)
|
||||
@@ -456,14 +456,14 @@ async def test_client_task_augmented_sampling(client_streams: ClientTestStreams)
|
||||
assert isinstance(response, types.JSONRPCResponse)
|
||||
|
||||
task_result = CreateTaskResult.model_validate(response.result)
|
||||
task_id = task_result.task.taskId
|
||||
task_id = task_result.task.task_id
|
||||
assert task_id == created_task_id[0]
|
||||
|
||||
# Step 3: Wait for background sampling
|
||||
await sampling_completed.wait()
|
||||
|
||||
# Step 4: Server polls task status
|
||||
typed_poll = GetTaskRequest(params=GetTaskRequestParams(taskId=task_id))
|
||||
typed_poll = GetTaskRequest(params=GetTaskRequestParams(task_id=task_id))
|
||||
poll_request = types.JSONRPCRequest(
|
||||
jsonrpc="2.0",
|
||||
id="req-poll",
|
||||
@@ -479,7 +479,7 @@ async def test_client_task_augmented_sampling(client_streams: ClientTestStreams)
|
||||
assert status.status == "completed"
|
||||
|
||||
# Step 5: Server gets result
|
||||
typed_result_req = GetTaskPayloadRequest(params=GetTaskPayloadRequestParams(taskId=task_id))
|
||||
typed_result_req = GetTaskPayloadRequest(params=GetTaskPayloadRequestParams(task_id=task_id))
|
||||
result_request = types.JSONRPCRequest(
|
||||
jsonrpc="2.0",
|
||||
id="req-result",
|
||||
@@ -514,13 +514,13 @@ async def test_client_task_augmented_elicitation(client_streams: ClientTestStrea
|
||||
task_metadata: TaskMetadata,
|
||||
) -> CreateTaskResult | ErrorData:
|
||||
task = await store.create_task(task_metadata)
|
||||
created_task_id[0] = task.taskId
|
||||
created_task_id[0] = task.task_id
|
||||
|
||||
async def do_elicitation() -> None:
|
||||
# Simulate user providing elicitation response
|
||||
result = ElicitResult(action="accept", content={"name": "Test User"})
|
||||
await store.store_result(task.taskId, result)
|
||||
await store.update_task(task.taskId, status="completed")
|
||||
await store.store_result(task.task_id, result)
|
||||
await store.update_task(task.task_id, status="completed")
|
||||
elicitation_completed.set()
|
||||
|
||||
assert background_tg[0] is not None
|
||||
@@ -531,24 +531,24 @@ async def test_client_task_augmented_elicitation(client_streams: ClientTestStrea
|
||||
context: RequestContext[ClientSession, None],
|
||||
params: GetTaskRequestParams,
|
||||
) -> GetTaskResult | ErrorData:
|
||||
task = await store.get_task(params.taskId)
|
||||
assert task is not None, f"Test setup error: task {params.taskId} should exist"
|
||||
task = await store.get_task(params.task_id)
|
||||
assert task is not None, f"Test setup error: task {params.task_id} should exist"
|
||||
return GetTaskResult(
|
||||
taskId=task.taskId,
|
||||
task_id=task.task_id,
|
||||
status=task.status,
|
||||
statusMessage=task.statusMessage,
|
||||
createdAt=task.createdAt,
|
||||
lastUpdatedAt=task.lastUpdatedAt,
|
||||
status_message=task.status_message,
|
||||
created_at=task.created_at,
|
||||
last_updated_at=task.last_updated_at,
|
||||
ttl=task.ttl,
|
||||
pollInterval=task.pollInterval,
|
||||
poll_interval=task.poll_interval,
|
||||
)
|
||||
|
||||
async def get_task_result_handler(
|
||||
context: RequestContext[ClientSession, None],
|
||||
params: GetTaskPayloadRequestParams,
|
||||
) -> GetTaskPayloadResult | ErrorData:
|
||||
result = await store.get_result(params.taskId)
|
||||
assert result is not None, f"Test setup error: result for {params.taskId} should exist"
|
||||
result = await store.get_result(params.task_id)
|
||||
assert result is not None, f"Test setup error: result for {params.task_id} should exist"
|
||||
assert isinstance(result, ElicitResult)
|
||||
return GetTaskPayloadResult(**result.model_dump())
|
||||
|
||||
@@ -579,7 +579,7 @@ async def test_client_task_augmented_elicitation(client_streams: ClientTestStrea
|
||||
typed_request = ElicitRequest(
|
||||
params=ElicitRequestFormParams(
|
||||
message="What is your name?",
|
||||
requestedSchema={"type": "object", "properties": {"name": {"type": "string"}}},
|
||||
requested_schema={"type": "object", "properties": {"name": {"type": "string"}}},
|
||||
task=TaskMetadata(ttl=60000),
|
||||
)
|
||||
)
|
||||
@@ -596,14 +596,14 @@ async def test_client_task_augmented_elicitation(client_streams: ClientTestStrea
|
||||
assert isinstance(response, types.JSONRPCResponse)
|
||||
|
||||
task_result = CreateTaskResult.model_validate(response.result)
|
||||
task_id = task_result.task.taskId
|
||||
task_id = task_result.task.task_id
|
||||
assert task_id == created_task_id[0]
|
||||
|
||||
# Step 3: Wait for background elicitation
|
||||
await elicitation_completed.wait()
|
||||
|
||||
# Step 4: Server polls task status
|
||||
typed_poll = GetTaskRequest(params=GetTaskRequestParams(taskId=task_id))
|
||||
typed_poll = GetTaskRequest(params=GetTaskRequestParams(task_id=task_id))
|
||||
poll_request = types.JSONRPCRequest(
|
||||
jsonrpc="2.0",
|
||||
id="req-poll",
|
||||
@@ -619,7 +619,7 @@ async def test_client_task_augmented_elicitation(client_streams: ClientTestStrea
|
||||
assert status.status == "completed"
|
||||
|
||||
# Step 5: Server gets result
|
||||
typed_result_req = GetTaskPayloadRequest(params=GetTaskPayloadRequestParams(taskId=task_id))
|
||||
typed_result_req = GetTaskPayloadRequest(params=GetTaskPayloadRequestParams(task_id=task_id))
|
||||
result_request = types.JSONRPCRequest(
|
||||
jsonrpc="2.0",
|
||||
id="req-result",
|
||||
@@ -661,7 +661,7 @@ async def test_client_returns_error_for_unhandled_task_request(client_streams: C
|
||||
tg.start_soon(run_client)
|
||||
await client_ready.wait()
|
||||
|
||||
typed_request = GetTaskRequest(params=GetTaskRequestParams(taskId="nonexistent"))
|
||||
typed_request = GetTaskRequest(params=GetTaskRequestParams(task_id="nonexistent"))
|
||||
request = types.JSONRPCRequest(
|
||||
jsonrpc="2.0",
|
||||
id="req-unhandled",
|
||||
@@ -700,7 +700,7 @@ async def test_client_returns_error_for_unhandled_task_result_request(client_str
|
||||
tg.start_soon(run_client)
|
||||
await client_ready.wait()
|
||||
|
||||
typed_request = GetTaskPayloadRequest(params=GetTaskPayloadRequestParams(taskId="nonexistent"))
|
||||
typed_request = GetTaskPayloadRequest(params=GetTaskPayloadRequestParams(task_id="nonexistent"))
|
||||
request = types.JSONRPCRequest(
|
||||
jsonrpc="2.0",
|
||||
id="req-result",
|
||||
@@ -772,7 +772,7 @@ async def test_client_returns_error_for_unhandled_cancel_task_request(client_str
|
||||
tg.start_soon(run_client)
|
||||
await client_ready.wait()
|
||||
|
||||
typed_request = CancelTaskRequest(params=CancelTaskRequestParams(taskId="nonexistent"))
|
||||
typed_request = CancelTaskRequest(params=CancelTaskRequestParams(task_id="nonexistent"))
|
||||
request = types.JSONRPCRequest(
|
||||
jsonrpc="2.0",
|
||||
id="req-cancel",
|
||||
@@ -813,7 +813,7 @@ async def test_client_returns_error_for_unhandled_task_augmented_sampling(client
|
||||
typed_request = CreateMessageRequest(
|
||||
params=CreateMessageRequestParams(
|
||||
messages=[SamplingMessage(role="user", content=TextContent(type="text", text="Hello"))],
|
||||
maxTokens=100,
|
||||
max_tokens=100,
|
||||
task=TaskMetadata(ttl=60000),
|
||||
)
|
||||
)
|
||||
@@ -859,7 +859,7 @@ async def test_client_returns_error_for_unhandled_task_augmented_elicitation(
|
||||
typed_request = ElicitRequest(
|
||||
params=ElicitRequestFormParams(
|
||||
message="What is your name?",
|
||||
requestedSchema={"type": "object", "properties": {"name": {"type": "string"}}},
|
||||
requested_schema={"type": "object", "properties": {"name": {"type": "string"}}},
|
||||
task=TaskMetadata(ttl=60000),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -20,13 +20,13 @@ def make_task_result(
|
||||
"""Create GetTaskResult with sensible defaults."""
|
||||
now = datetime.now(timezone.utc)
|
||||
return GetTaskResult(
|
||||
taskId=task_id,
|
||||
task_id=task_id,
|
||||
status=status,
|
||||
statusMessage=status_message,
|
||||
createdAt=now,
|
||||
lastUpdatedAt=now,
|
||||
status_message=status_message,
|
||||
created_at=now,
|
||||
last_updated_at=now,
|
||||
ttl=60000,
|
||||
pollInterval=poll_interval,
|
||||
poll_interval=poll_interval,
|
||||
)
|
||||
|
||||
|
||||
@@ -117,5 +117,5 @@ async def test_poll_task_yields_full_result(features: ExperimentalClientFeatures
|
||||
|
||||
assert len(results) == 1
|
||||
assert results[0].status == "completed"
|
||||
assert results[0].statusMessage == "All done!"
|
||||
assert results[0].taskId == "test-task"
|
||||
assert results[0].status_message == "All done!"
|
||||
assert results[0].task_id == "test-task"
|
||||
|
||||
@@ -58,7 +58,7 @@ async def test_session_experimental_get_task() -> None:
|
||||
|
||||
@server.list_tools()
|
||||
async def list_tools():
|
||||
return [Tool(name="test_tool", description="Test", inputSchema={"type": "object"})]
|
||||
return [Tool(name="test_tool", description="Test", input_schema={"type": "object"})]
|
||||
|
||||
@server.call_tool()
|
||||
async def handle_call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent] | CreateTaskResult:
|
||||
@@ -70,10 +70,10 @@ async def test_session_experimental_get_task() -> None:
|
||||
task = await app.store.create_task(task_metadata)
|
||||
|
||||
done_event = Event()
|
||||
app.task_done_events[task.taskId] = done_event
|
||||
app.task_done_events[task.task_id] = done_event
|
||||
|
||||
async def do_work():
|
||||
async with task_execution(task.taskId, app.store) as task_ctx:
|
||||
async with task_execution(task.task_id, app.store) as task_ctx:
|
||||
await task_ctx.complete(CallToolResult(content=[TextContent(type="text", text="Done")]))
|
||||
done_event.set()
|
||||
|
||||
@@ -85,16 +85,16 @@ async def test_session_experimental_get_task() -> None:
|
||||
@server.experimental.get_task()
|
||||
async def handle_get_task(request: GetTaskRequest) -> GetTaskResult:
|
||||
app = server.request_context.lifespan_context
|
||||
task = await app.store.get_task(request.params.taskId)
|
||||
assert task is not None, f"Test setup error: task {request.params.taskId} should exist"
|
||||
task = await app.store.get_task(request.params.task_id)
|
||||
assert task is not None, f"Test setup error: task {request.params.task_id} should exist"
|
||||
return GetTaskResult(
|
||||
taskId=task.taskId,
|
||||
task_id=task.task_id,
|
||||
status=task.status,
|
||||
statusMessage=task.statusMessage,
|
||||
createdAt=task.createdAt,
|
||||
lastUpdatedAt=task.lastUpdatedAt,
|
||||
status_message=task.status_message,
|
||||
created_at=task.created_at,
|
||||
last_updated_at=task.last_updated_at,
|
||||
ttl=task.ttl,
|
||||
pollInterval=task.pollInterval,
|
||||
poll_interval=task.poll_interval,
|
||||
)
|
||||
|
||||
# Set up streams
|
||||
@@ -145,7 +145,7 @@ async def test_session_experimental_get_task() -> None:
|
||||
),
|
||||
CreateTaskResult,
|
||||
)
|
||||
task_id = create_result.task.taskId
|
||||
task_id = create_result.task.task_id
|
||||
|
||||
# Wait for task to complete
|
||||
await app_context.task_done_events[task_id].wait()
|
||||
@@ -153,7 +153,7 @@ async def test_session_experimental_get_task() -> None:
|
||||
# Use session.experimental to get task status
|
||||
task_status = await client_session.experimental.get_task(task_id)
|
||||
|
||||
assert task_status.taskId == task_id
|
||||
assert task_status.task_id == task_id
|
||||
assert task_status.status == "completed"
|
||||
|
||||
tg.cancel_scope.cancel()
|
||||
@@ -167,7 +167,7 @@ async def test_session_experimental_get_task_result() -> None:
|
||||
|
||||
@server.list_tools()
|
||||
async def list_tools():
|
||||
return [Tool(name="test_tool", description="Test", inputSchema={"type": "object"})]
|
||||
return [Tool(name="test_tool", description="Test", input_schema={"type": "object"})]
|
||||
|
||||
@server.call_tool()
|
||||
async def handle_call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent] | CreateTaskResult:
|
||||
@@ -179,10 +179,10 @@ async def test_session_experimental_get_task_result() -> None:
|
||||
task = await app.store.create_task(task_metadata)
|
||||
|
||||
done_event = Event()
|
||||
app.task_done_events[task.taskId] = done_event
|
||||
app.task_done_events[task.task_id] = done_event
|
||||
|
||||
async def do_work():
|
||||
async with task_execution(task.taskId, app.store) as task_ctx:
|
||||
async with task_execution(task.task_id, app.store) as task_ctx:
|
||||
await task_ctx.complete(
|
||||
CallToolResult(content=[TextContent(type="text", text="Task result content")])
|
||||
)
|
||||
@@ -198,8 +198,8 @@ async def test_session_experimental_get_task_result() -> None:
|
||||
request: GetTaskPayloadRequest,
|
||||
) -> GetTaskPayloadResult:
|
||||
app = server.request_context.lifespan_context
|
||||
result = await app.store.get_result(request.params.taskId)
|
||||
assert result is not None, f"Test setup error: result for {request.params.taskId} should exist"
|
||||
result = await app.store.get_result(request.params.task_id)
|
||||
assert result is not None, f"Test setup error: result for {request.params.task_id} should exist"
|
||||
assert isinstance(result, CallToolResult)
|
||||
return GetTaskPayloadResult(**result.model_dump())
|
||||
|
||||
@@ -251,7 +251,7 @@ async def test_session_experimental_get_task_result() -> None:
|
||||
),
|
||||
CreateTaskResult,
|
||||
)
|
||||
task_id = create_result.task.taskId
|
||||
task_id = create_result.task.task_id
|
||||
|
||||
# Wait for task to complete
|
||||
await app_context.task_done_events[task_id].wait()
|
||||
@@ -275,7 +275,7 @@ async def test_session_experimental_list_tasks() -> None:
|
||||
|
||||
@server.list_tools()
|
||||
async def list_tools():
|
||||
return [Tool(name="test_tool", description="Test", inputSchema={"type": "object"})]
|
||||
return [Tool(name="test_tool", description="Test", input_schema={"type": "object"})]
|
||||
|
||||
@server.call_tool()
|
||||
async def handle_call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent] | CreateTaskResult:
|
||||
@@ -287,10 +287,10 @@ async def test_session_experimental_list_tasks() -> None:
|
||||
task = await app.store.create_task(task_metadata)
|
||||
|
||||
done_event = Event()
|
||||
app.task_done_events[task.taskId] = done_event
|
||||
app.task_done_events[task.task_id] = done_event
|
||||
|
||||
async def do_work():
|
||||
async with task_execution(task.taskId, app.store) as task_ctx:
|
||||
async with task_execution(task.task_id, app.store) as task_ctx:
|
||||
await task_ctx.complete(CallToolResult(content=[TextContent(type="text", text="Done")]))
|
||||
done_event.set()
|
||||
|
||||
@@ -303,7 +303,7 @@ async def test_session_experimental_list_tasks() -> None:
|
||||
async def handle_list_tasks(request: ListTasksRequest) -> ListTasksResult:
|
||||
app = server.request_context.lifespan_context
|
||||
tasks_list, next_cursor = await app.store.list_tasks(cursor=request.params.cursor if request.params else None)
|
||||
return ListTasksResult(tasks=tasks_list, nextCursor=next_cursor)
|
||||
return ListTasksResult(tasks=tasks_list, next_cursor=next_cursor)
|
||||
|
||||
# Set up streams
|
||||
server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[SessionMessage](10)
|
||||
@@ -354,7 +354,7 @@ async def test_session_experimental_list_tasks() -> None:
|
||||
),
|
||||
CreateTaskResult,
|
||||
)
|
||||
await app_context.task_done_events[create_result.task.taskId].wait()
|
||||
await app_context.task_done_events[create_result.task.task_id].wait()
|
||||
|
||||
# Use TaskClient to list tasks
|
||||
list_result = await client_session.experimental.list_tasks()
|
||||
@@ -372,7 +372,7 @@ async def test_session_experimental_cancel_task() -> None:
|
||||
|
||||
@server.list_tools()
|
||||
async def list_tools():
|
||||
return [Tool(name="test_tool", description="Test", inputSchema={"type": "object"})]
|
||||
return [Tool(name="test_tool", description="Test", input_schema={"type": "object"})]
|
||||
|
||||
@server.call_tool()
|
||||
async def handle_call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent] | CreateTaskResult:
|
||||
@@ -390,32 +390,32 @@ async def test_session_experimental_cancel_task() -> None:
|
||||
@server.experimental.get_task()
|
||||
async def handle_get_task(request: GetTaskRequest) -> GetTaskResult:
|
||||
app = server.request_context.lifespan_context
|
||||
task = await app.store.get_task(request.params.taskId)
|
||||
assert task is not None, f"Test setup error: task {request.params.taskId} should exist"
|
||||
task = await app.store.get_task(request.params.task_id)
|
||||
assert task is not None, f"Test setup error: task {request.params.task_id} should exist"
|
||||
return GetTaskResult(
|
||||
taskId=task.taskId,
|
||||
task_id=task.task_id,
|
||||
status=task.status,
|
||||
statusMessage=task.statusMessage,
|
||||
createdAt=task.createdAt,
|
||||
lastUpdatedAt=task.lastUpdatedAt,
|
||||
status_message=task.status_message,
|
||||
created_at=task.created_at,
|
||||
last_updated_at=task.last_updated_at,
|
||||
ttl=task.ttl,
|
||||
pollInterval=task.pollInterval,
|
||||
poll_interval=task.poll_interval,
|
||||
)
|
||||
|
||||
@server.experimental.cancel_task()
|
||||
async def handle_cancel_task(request: CancelTaskRequest) -> CancelTaskResult:
|
||||
app = server.request_context.lifespan_context
|
||||
task = await app.store.get_task(request.params.taskId)
|
||||
assert task is not None, f"Test setup error: task {request.params.taskId} should exist"
|
||||
await app.store.update_task(request.params.taskId, status="cancelled")
|
||||
task = await app.store.get_task(request.params.task_id)
|
||||
assert task is not None, f"Test setup error: task {request.params.task_id} should exist"
|
||||
await app.store.update_task(request.params.task_id, status="cancelled")
|
||||
# CancelTaskResult extends Task, so we need to return the updated task info
|
||||
updated_task = await app.store.get_task(request.params.taskId)
|
||||
updated_task = await app.store.get_task(request.params.task_id)
|
||||
assert updated_task is not None
|
||||
return CancelTaskResult(
|
||||
taskId=updated_task.taskId,
|
||||
task_id=updated_task.task_id,
|
||||
status=updated_task.status,
|
||||
createdAt=updated_task.createdAt,
|
||||
lastUpdatedAt=updated_task.lastUpdatedAt,
|
||||
created_at=updated_task.created_at,
|
||||
last_updated_at=updated_task.last_updated_at,
|
||||
ttl=updated_task.ttl,
|
||||
)
|
||||
|
||||
@@ -467,7 +467,7 @@ async def test_session_experimental_cancel_task() -> None:
|
||||
),
|
||||
CreateTaskResult,
|
||||
)
|
||||
task_id = create_result.task.taskId
|
||||
task_id = create_result.task.task_id
|
||||
|
||||
# Verify task is working
|
||||
status_before = await client_session.experimental.get_task(task_id)
|
||||
|
||||
@@ -15,8 +15,8 @@ async def test_task_context_properties() -> None:
|
||||
task = await store.create_task(metadata=TaskMetadata(ttl=60000))
|
||||
ctx = TaskContext(task, store)
|
||||
|
||||
assert ctx.task_id == task.taskId
|
||||
assert ctx.task.taskId == task.taskId
|
||||
assert ctx.task_id == task.task_id
|
||||
assert ctx.task.task_id == task.task_id
|
||||
assert ctx.task.status == "working"
|
||||
assert ctx.is_cancelled is False
|
||||
|
||||
@@ -33,9 +33,9 @@ async def test_task_context_update_status() -> None:
|
||||
await ctx.update_status("Processing step 1...")
|
||||
|
||||
# Check status message was updated
|
||||
updated = await store.get_task(task.taskId)
|
||||
updated = await store.get_task(task.task_id)
|
||||
assert updated is not None
|
||||
assert updated.statusMessage == "Processing step 1..."
|
||||
assert updated.status_message == "Processing step 1..."
|
||||
|
||||
store.cleanup()
|
||||
|
||||
@@ -51,12 +51,12 @@ async def test_task_context_complete() -> None:
|
||||
await ctx.complete(result)
|
||||
|
||||
# Check task status
|
||||
updated = await store.get_task(task.taskId)
|
||||
updated = await store.get_task(task.task_id)
|
||||
assert updated is not None
|
||||
assert updated.status == "completed"
|
||||
|
||||
# Check result is stored
|
||||
stored_result = await store.get_result(task.taskId)
|
||||
stored_result = await store.get_result(task.task_id)
|
||||
assert stored_result is not None
|
||||
|
||||
store.cleanup()
|
||||
@@ -72,10 +72,10 @@ async def test_task_context_fail() -> None:
|
||||
await ctx.fail("Something went wrong!")
|
||||
|
||||
# Check task status
|
||||
updated = await store.get_task(task.taskId)
|
||||
updated = await store.get_task(task.task_id)
|
||||
assert updated is not None
|
||||
assert updated.status == "failed"
|
||||
assert updated.statusMessage == "Something went wrong!"
|
||||
assert updated.status_message == "Something went wrong!"
|
||||
|
||||
store.cleanup()
|
||||
|
||||
@@ -101,13 +101,13 @@ def test_create_task_state_generates_id() -> None:
|
||||
task1 = create_task_state(TaskMetadata(ttl=60000))
|
||||
task2 = create_task_state(TaskMetadata(ttl=60000))
|
||||
|
||||
assert task1.taskId != task2.taskId
|
||||
assert task1.task_id != task2.task_id
|
||||
|
||||
|
||||
def test_create_task_state_uses_provided_id() -> None:
|
||||
"""create_task_state uses the provided task ID."""
|
||||
task = create_task_state(TaskMetadata(ttl=60000), task_id="my-task-123")
|
||||
assert task.taskId == "my-task-123"
|
||||
assert task.task_id == "my-task-123"
|
||||
|
||||
|
||||
def test_create_task_state_null_ttl() -> None:
|
||||
@@ -119,7 +119,7 @@ def test_create_task_state_null_ttl() -> None:
|
||||
def test_create_task_state_has_created_at() -> None:
|
||||
"""create_task_state sets createdAt timestamp."""
|
||||
task = create_task_state(TaskMetadata(ttl=60000))
|
||||
assert task.createdAt is not None
|
||||
assert task.created_at is not None
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@@ -148,7 +148,7 @@ async def test_task_execution_auto_fails_on_exception() -> None:
|
||||
failed_task = await store.get_task("exec-fail-1")
|
||||
assert failed_task is not None
|
||||
assert failed_task.status == "failed"
|
||||
assert "Oops!" in (failed_task.statusMessage or "")
|
||||
assert "Oops!" in (failed_task.status_message or "")
|
||||
|
||||
store.cleanup()
|
||||
|
||||
|
||||
@@ -81,11 +81,11 @@ async def test_task_lifecycle_with_task_execution() -> None:
|
||||
Tool(
|
||||
name="process_data",
|
||||
description="Process data asynchronously",
|
||||
inputSchema={
|
||||
input_schema={
|
||||
"type": "object",
|
||||
"properties": {"input": {"type": "string"}},
|
||||
},
|
||||
execution=ToolExecution(taskSupport=TASK_REQUIRED),
|
||||
execution=ToolExecution(task_support=TASK_REQUIRED),
|
||||
)
|
||||
]
|
||||
|
||||
@@ -101,11 +101,11 @@ async def test_task_lifecycle_with_task_execution() -> None:
|
||||
|
||||
# 2. Create event to signal completion (for testing)
|
||||
done_event = Event()
|
||||
app.task_done_events[task.taskId] = done_event
|
||||
app.task_done_events[task.task_id] = done_event
|
||||
|
||||
# 3. Define work function using task_execution for safety
|
||||
async def do_work():
|
||||
async with task_execution(task.taskId, app.store) as task_ctx:
|
||||
async with task_execution(task.task_id, app.store) as task_ctx:
|
||||
await task_ctx.update_status("Processing input...")
|
||||
# Simulate work
|
||||
input_value = arguments.get("input", "")
|
||||
@@ -126,16 +126,16 @@ async def test_task_lifecycle_with_task_execution() -> None:
|
||||
@server.experimental.get_task()
|
||||
async def handle_get_task(request: GetTaskRequest) -> GetTaskResult:
|
||||
app = server.request_context.lifespan_context
|
||||
task = await app.store.get_task(request.params.taskId)
|
||||
assert task is not None, f"Test setup error: task {request.params.taskId} should exist"
|
||||
task = await app.store.get_task(request.params.task_id)
|
||||
assert task is not None, f"Test setup error: task {request.params.task_id} should exist"
|
||||
return GetTaskResult(
|
||||
taskId=task.taskId,
|
||||
task_id=task.task_id,
|
||||
status=task.status,
|
||||
statusMessage=task.statusMessage,
|
||||
createdAt=task.createdAt,
|
||||
lastUpdatedAt=task.lastUpdatedAt,
|
||||
status_message=task.status_message,
|
||||
created_at=task.created_at,
|
||||
last_updated_at=task.last_updated_at,
|
||||
ttl=task.ttl,
|
||||
pollInterval=task.pollInterval,
|
||||
poll_interval=task.poll_interval,
|
||||
)
|
||||
|
||||
@server.experimental.get_task_result()
|
||||
@@ -143,8 +143,8 @@ async def test_task_lifecycle_with_task_execution() -> None:
|
||||
request: GetTaskPayloadRequest,
|
||||
) -> GetTaskPayloadResult:
|
||||
app = server.request_context.lifespan_context
|
||||
result = await app.store.get_result(request.params.taskId)
|
||||
assert result is not None, f"Test setup error: result for {request.params.taskId} should exist"
|
||||
result = await app.store.get_result(request.params.task_id)
|
||||
assert result is not None, f"Test setup error: result for {request.params.task_id} should exist"
|
||||
assert isinstance(result, CallToolResult)
|
||||
# Return as GetTaskPayloadResult (which accepts extra fields)
|
||||
return GetTaskPayloadResult(**result.model_dump())
|
||||
@@ -205,22 +205,22 @@ async def test_task_lifecycle_with_task_execution() -> None:
|
||||
|
||||
assert isinstance(create_result, CreateTaskResult)
|
||||
assert create_result.task.status == "working"
|
||||
task_id = create_result.task.taskId
|
||||
task_id = create_result.task.task_id
|
||||
|
||||
# === Step 2: Wait for task to complete ===
|
||||
await app_context.task_done_events[task_id].wait()
|
||||
|
||||
task_status = await client_session.send_request(
|
||||
ClientRequest(GetTaskRequest(params=GetTaskRequestParams(taskId=task_id))),
|
||||
ClientRequest(GetTaskRequest(params=GetTaskRequestParams(task_id=task_id))),
|
||||
GetTaskResult,
|
||||
)
|
||||
|
||||
assert task_status.taskId == task_id
|
||||
assert task_status.task_id == task_id
|
||||
assert task_status.status == "completed"
|
||||
|
||||
# === Step 3: Retrieve the actual result ===
|
||||
task_result = await client_session.send_request(
|
||||
ClientRequest(GetTaskPayloadRequest(params=GetTaskPayloadRequestParams(taskId=task_id))),
|
||||
ClientRequest(GetTaskPayloadRequest(params=GetTaskPayloadRequestParams(task_id=task_id))),
|
||||
CallToolResult,
|
||||
)
|
||||
|
||||
@@ -245,7 +245,7 @@ async def test_task_auto_fails_on_exception() -> None:
|
||||
Tool(
|
||||
name="failing_task",
|
||||
description="A task that fails",
|
||||
inputSchema={"type": "object", "properties": {}},
|
||||
input_schema={"type": "object", "properties": {}},
|
||||
)
|
||||
]
|
||||
|
||||
@@ -260,10 +260,10 @@ async def test_task_auto_fails_on_exception() -> None:
|
||||
|
||||
# Create event to signal completion (for testing)
|
||||
done_event = Event()
|
||||
app.task_done_events[task.taskId] = done_event
|
||||
app.task_done_events[task.task_id] = done_event
|
||||
|
||||
async def do_failing_work():
|
||||
async with task_execution(task.taskId, app.store) as task_ctx:
|
||||
async with task_execution(task.task_id, app.store) as task_ctx:
|
||||
await task_ctx.update_status("About to fail...")
|
||||
raise RuntimeError("Something went wrong!")
|
||||
# Note: complete() is never called, but task_execution
|
||||
@@ -279,16 +279,16 @@ async def test_task_auto_fails_on_exception() -> None:
|
||||
@server.experimental.get_task()
|
||||
async def handle_get_task(request: GetTaskRequest) -> GetTaskResult:
|
||||
app = server.request_context.lifespan_context
|
||||
task = await app.store.get_task(request.params.taskId)
|
||||
assert task is not None, f"Test setup error: task {request.params.taskId} should exist"
|
||||
task = await app.store.get_task(request.params.task_id)
|
||||
assert task is not None, f"Test setup error: task {request.params.task_id} should exist"
|
||||
return GetTaskResult(
|
||||
taskId=task.taskId,
|
||||
task_id=task.task_id,
|
||||
status=task.status,
|
||||
statusMessage=task.statusMessage,
|
||||
createdAt=task.createdAt,
|
||||
lastUpdatedAt=task.lastUpdatedAt,
|
||||
status_message=task.status_message,
|
||||
created_at=task.created_at,
|
||||
last_updated_at=task.last_updated_at,
|
||||
ttl=task.ttl,
|
||||
pollInterval=task.pollInterval,
|
||||
poll_interval=task.poll_interval,
|
||||
)
|
||||
|
||||
# Set up streams
|
||||
@@ -340,18 +340,18 @@ async def test_task_auto_fails_on_exception() -> None:
|
||||
CreateTaskResult,
|
||||
)
|
||||
|
||||
task_id = create_result.task.taskId
|
||||
task_id = create_result.task.task_id
|
||||
|
||||
# Wait for task to complete (even though it fails)
|
||||
await app_context.task_done_events[task_id].wait()
|
||||
|
||||
# Check that task was auto-failed
|
||||
task_status = await client_session.send_request(
|
||||
ClientRequest(GetTaskRequest(params=GetTaskRequestParams(taskId=task_id))),
|
||||
ClientRequest(GetTaskRequest(params=GetTaskRequestParams(task_id=task_id))),
|
||||
GetTaskResult,
|
||||
)
|
||||
|
||||
assert task_status.status == "failed"
|
||||
assert task_status.statusMessage == "Something went wrong!"
|
||||
assert task_status.status_message == "Something went wrong!"
|
||||
|
||||
tg.cancel_scope.cancel()
|
||||
|
||||
@@ -69,8 +69,8 @@ async def test_run_task_basic_flow() -> None:
|
||||
Tool(
|
||||
name="simple_task",
|
||||
description="A simple task",
|
||||
inputSchema={"type": "object", "properties": {"input": {"type": "string"}}},
|
||||
execution=ToolExecution(taskSupport=TASK_REQUIRED),
|
||||
input_schema={"type": "object", "properties": {"input": {"type": "string"}}},
|
||||
execution=ToolExecution(task_support=TASK_REQUIRED),
|
||||
)
|
||||
]
|
||||
|
||||
@@ -119,7 +119,7 @@ async def test_run_task_basic_flow() -> None:
|
||||
)
|
||||
|
||||
# Should get CreateTaskResult
|
||||
task_id = result.task.taskId
|
||||
task_id = result.task.task_id
|
||||
assert result.task.status == "working"
|
||||
|
||||
# Wait for work to complete
|
||||
@@ -157,8 +157,8 @@ async def test_run_task_auto_fails_on_exception() -> None:
|
||||
Tool(
|
||||
name="failing_task",
|
||||
description="A task that fails",
|
||||
inputSchema={"type": "object"},
|
||||
execution=ToolExecution(taskSupport=TASK_REQUIRED),
|
||||
input_schema={"type": "object"},
|
||||
execution=ToolExecution(task_support=TASK_REQUIRED),
|
||||
)
|
||||
]
|
||||
|
||||
@@ -188,7 +188,7 @@ async def test_run_task_auto_fails_on_exception() -> None:
|
||||
await client_session.initialize()
|
||||
|
||||
result = await client_session.experimental.call_tool_as_task("failing_task", {})
|
||||
task_id = result.task.taskId
|
||||
task_id = result.task.task_id
|
||||
|
||||
# Wait for work to fail
|
||||
with anyio.fail_after(5):
|
||||
@@ -201,7 +201,7 @@ async def test_run_task_auto_fails_on_exception() -> None:
|
||||
if task_status.status == "failed": # pragma: no branch
|
||||
break
|
||||
|
||||
assert "Something went wrong" in (task_status.statusMessage or "")
|
||||
assert "Something went wrong" in (task_status.status_message or "")
|
||||
|
||||
async with anyio.create_task_group() as tg:
|
||||
tg.start_soon(run_server)
|
||||
@@ -363,8 +363,8 @@ async def test_run_task_with_model_immediate_response() -> None:
|
||||
Tool(
|
||||
name="task_with_immediate",
|
||||
description="A task with immediate response",
|
||||
inputSchema={"type": "object"},
|
||||
execution=ToolExecution(taskSupport=TASK_REQUIRED),
|
||||
input_schema={"type": "object"},
|
||||
execution=ToolExecution(task_support=TASK_REQUIRED),
|
||||
)
|
||||
]
|
||||
|
||||
@@ -422,8 +422,8 @@ async def test_run_task_doesnt_complete_if_already_terminal() -> None:
|
||||
Tool(
|
||||
name="manual_complete_task",
|
||||
description="A task that manually completes",
|
||||
inputSchema={"type": "object"},
|
||||
execution=ToolExecution(taskSupport=TASK_REQUIRED),
|
||||
input_schema={"type": "object"},
|
||||
execution=ToolExecution(task_support=TASK_REQUIRED),
|
||||
)
|
||||
]
|
||||
|
||||
@@ -457,7 +457,7 @@ async def test_run_task_doesnt_complete_if_already_terminal() -> None:
|
||||
await client_session.initialize()
|
||||
|
||||
result = await client_session.experimental.call_tool_as_task("manual_complete_task", {})
|
||||
task_id = result.task.taskId
|
||||
task_id = result.task.task_id
|
||||
|
||||
with anyio.fail_after(5):
|
||||
await work_completed.wait()
|
||||
@@ -488,8 +488,8 @@ async def test_run_task_doesnt_fail_if_already_terminal() -> None:
|
||||
Tool(
|
||||
name="manual_cancel_task",
|
||||
description="A task that manually cancels then raises",
|
||||
inputSchema={"type": "object"},
|
||||
execution=ToolExecution(taskSupport=TASK_REQUIRED),
|
||||
input_schema={"type": "object"},
|
||||
execution=ToolExecution(task_support=TASK_REQUIRED),
|
||||
)
|
||||
]
|
||||
|
||||
@@ -522,7 +522,7 @@ async def test_run_task_doesnt_fail_if_already_terminal() -> None:
|
||||
await client_session.initialize()
|
||||
|
||||
result = await client_session.experimental.call_tool_as_task("manual_cancel_task", {})
|
||||
task_id = result.task.taskId
|
||||
task_id = result.task.task_id
|
||||
|
||||
with anyio.fail_after(5):
|
||||
await work_completed.wait()
|
||||
@@ -535,7 +535,7 @@ async def test_run_task_doesnt_fail_if_already_terminal() -> None:
|
||||
break
|
||||
|
||||
# Task should still be failed (from manual fail, not auto-fail from exception)
|
||||
assert status.statusMessage == "Manually failed" # Not "This error should not change status"
|
||||
assert status.status_message == "Manually failed" # Not "This error should not change status"
|
||||
|
||||
async with anyio.create_task_group() as tg:
|
||||
tg.start_soon(run_server)
|
||||
|
||||
@@ -64,20 +64,20 @@ async def test_list_tasks_handler() -> None:
|
||||
now = datetime.now(timezone.utc)
|
||||
test_tasks = [
|
||||
Task(
|
||||
taskId="task-1",
|
||||
task_id="task-1",
|
||||
status="working",
|
||||
createdAt=now,
|
||||
lastUpdatedAt=now,
|
||||
created_at=now,
|
||||
last_updated_at=now,
|
||||
ttl=60000,
|
||||
pollInterval=1000,
|
||||
poll_interval=1000,
|
||||
),
|
||||
Task(
|
||||
taskId="task-2",
|
||||
task_id="task-2",
|
||||
status="completed",
|
||||
createdAt=now,
|
||||
lastUpdatedAt=now,
|
||||
created_at=now,
|
||||
last_updated_at=now,
|
||||
ttl=60000,
|
||||
pollInterval=1000,
|
||||
poll_interval=1000,
|
||||
),
|
||||
]
|
||||
|
||||
@@ -92,8 +92,8 @@ async def test_list_tasks_handler() -> None:
|
||||
assert isinstance(result, ServerResult)
|
||||
assert isinstance(result.root, ListTasksResult)
|
||||
assert len(result.root.tasks) == 2
|
||||
assert result.root.tasks[0].taskId == "task-1"
|
||||
assert result.root.tasks[1].taskId == "task-2"
|
||||
assert result.root.tasks[0].task_id == "task-1"
|
||||
assert result.root.tasks[1].task_id == "task-2"
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@@ -105,24 +105,24 @@ async def test_get_task_handler() -> None:
|
||||
async def handle_get_task(request: GetTaskRequest) -> GetTaskResult:
|
||||
now = datetime.now(timezone.utc)
|
||||
return GetTaskResult(
|
||||
taskId=request.params.taskId,
|
||||
task_id=request.params.task_id,
|
||||
status="working",
|
||||
createdAt=now,
|
||||
lastUpdatedAt=now,
|
||||
created_at=now,
|
||||
last_updated_at=now,
|
||||
ttl=60000,
|
||||
pollInterval=1000,
|
||||
poll_interval=1000,
|
||||
)
|
||||
|
||||
handler = server.request_handlers[GetTaskRequest]
|
||||
request = GetTaskRequest(
|
||||
method="tasks/get",
|
||||
params=GetTaskRequestParams(taskId="test-task-123"),
|
||||
params=GetTaskRequestParams(task_id="test-task-123"),
|
||||
)
|
||||
result = await handler(request)
|
||||
|
||||
assert isinstance(result, ServerResult)
|
||||
assert isinstance(result.root, GetTaskResult)
|
||||
assert result.root.taskId == "test-task-123"
|
||||
assert result.root.task_id == "test-task-123"
|
||||
assert result.root.status == "working"
|
||||
|
||||
|
||||
@@ -138,7 +138,7 @@ async def test_get_task_result_handler() -> None:
|
||||
handler = server.request_handlers[GetTaskPayloadRequest]
|
||||
request = GetTaskPayloadRequest(
|
||||
method="tasks/result",
|
||||
params=GetTaskPayloadRequestParams(taskId="test-task-123"),
|
||||
params=GetTaskPayloadRequestParams(task_id="test-task-123"),
|
||||
)
|
||||
result = await handler(request)
|
||||
|
||||
@@ -155,23 +155,23 @@ async def test_cancel_task_handler() -> None:
|
||||
async def handle_cancel_task(request: CancelTaskRequest) -> CancelTaskResult:
|
||||
now = datetime.now(timezone.utc)
|
||||
return CancelTaskResult(
|
||||
taskId=request.params.taskId,
|
||||
task_id=request.params.task_id,
|
||||
status="cancelled",
|
||||
createdAt=now,
|
||||
lastUpdatedAt=now,
|
||||
created_at=now,
|
||||
last_updated_at=now,
|
||||
ttl=60000,
|
||||
)
|
||||
|
||||
handler = server.request_handlers[CancelTaskRequest]
|
||||
request = CancelTaskRequest(
|
||||
method="tasks/cancel",
|
||||
params=CancelTaskRequestParams(taskId="test-task-123"),
|
||||
params=CancelTaskRequestParams(task_id="test-task-123"),
|
||||
)
|
||||
result = await handler(request)
|
||||
|
||||
assert isinstance(result, ServerResult)
|
||||
assert isinstance(result.root, CancelTaskResult)
|
||||
assert result.root.taskId == "test-task-123"
|
||||
assert result.root.task_id == "test-task-123"
|
||||
assert result.root.status == "cancelled"
|
||||
|
||||
|
||||
@@ -232,20 +232,20 @@ async def test_tool_with_task_execution_metadata() -> None:
|
||||
Tool(
|
||||
name="quick_tool",
|
||||
description="Fast tool",
|
||||
inputSchema={"type": "object", "properties": {}},
|
||||
execution=ToolExecution(taskSupport=TASK_FORBIDDEN),
|
||||
input_schema={"type": "object", "properties": {}},
|
||||
execution=ToolExecution(task_support=TASK_FORBIDDEN),
|
||||
),
|
||||
Tool(
|
||||
name="long_tool",
|
||||
description="Long running tool",
|
||||
inputSchema={"type": "object", "properties": {}},
|
||||
execution=ToolExecution(taskSupport=TASK_REQUIRED),
|
||||
input_schema={"type": "object", "properties": {}},
|
||||
execution=ToolExecution(task_support=TASK_REQUIRED),
|
||||
),
|
||||
Tool(
|
||||
name="flexible_tool",
|
||||
description="Can be either",
|
||||
inputSchema={"type": "object", "properties": {}},
|
||||
execution=ToolExecution(taskSupport=TASK_OPTIONAL),
|
||||
input_schema={"type": "object", "properties": {}},
|
||||
execution=ToolExecution(task_support=TASK_OPTIONAL),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -258,11 +258,11 @@ async def test_tool_with_task_execution_metadata() -> None:
|
||||
tools = result.root.tools
|
||||
|
||||
assert tools[0].execution is not None
|
||||
assert tools[0].execution.taskSupport == TASK_FORBIDDEN
|
||||
assert tools[0].execution.task_support == TASK_FORBIDDEN
|
||||
assert tools[1].execution is not None
|
||||
assert tools[1].execution.taskSupport == TASK_REQUIRED
|
||||
assert tools[1].execution.task_support == TASK_REQUIRED
|
||||
assert tools[2].execution is not None
|
||||
assert tools[2].execution.taskSupport == TASK_OPTIONAL
|
||||
assert tools[2].execution.task_support == TASK_OPTIONAL
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@@ -277,8 +277,8 @@ async def test_task_metadata_in_call_tool_request() -> None:
|
||||
Tool(
|
||||
name="long_task",
|
||||
description="A long running task",
|
||||
inputSchema={"type": "object", "properties": {}},
|
||||
execution=ToolExecution(taskSupport="optional"),
|
||||
input_schema={"type": "object", "properties": {}},
|
||||
execution=ToolExecution(task_support="optional"),
|
||||
)
|
||||
]
|
||||
|
||||
@@ -361,7 +361,7 @@ async def test_task_metadata_is_task_property() -> None:
|
||||
Tool(
|
||||
name="test_tool",
|
||||
description="Test tool",
|
||||
inputSchema={"type": "object", "properties": {}},
|
||||
input_schema={"type": "object", "properties": {}},
|
||||
)
|
||||
]
|
||||
|
||||
@@ -513,33 +513,35 @@ async def test_default_task_handlers_via_enable_tasks() -> None:
|
||||
ListTasksResult,
|
||||
)
|
||||
assert len(list_result.tasks) == 1
|
||||
assert list_result.tasks[0].taskId == task.taskId
|
||||
assert list_result.tasks[0].task_id == task.task_id
|
||||
|
||||
# Test get_task (default handler - found)
|
||||
get_result = await client_session.send_request(
|
||||
ClientRequest(GetTaskRequest(params=GetTaskRequestParams(taskId=task.taskId))),
|
||||
ClientRequest(GetTaskRequest(params=GetTaskRequestParams(task_id=task.task_id))),
|
||||
GetTaskResult,
|
||||
)
|
||||
assert get_result.taskId == task.taskId
|
||||
assert get_result.task_id == task.task_id
|
||||
assert get_result.status == "working"
|
||||
|
||||
# Test get_task (default handler - not found path)
|
||||
with pytest.raises(McpError, match="not found"):
|
||||
await client_session.send_request(
|
||||
ClientRequest(GetTaskRequest(params=GetTaskRequestParams(taskId="nonexistent-task"))),
|
||||
ClientRequest(GetTaskRequest(params=GetTaskRequestParams(task_id="nonexistent-task"))),
|
||||
GetTaskResult,
|
||||
)
|
||||
|
||||
# Create a completed task to test get_task_result
|
||||
completed_task = await store.create_task(TaskMetadata(ttl=60000))
|
||||
await store.store_result(
|
||||
completed_task.taskId, CallToolResult(content=[TextContent(type="text", text="Test result")])
|
||||
completed_task.task_id, CallToolResult(content=[TextContent(type="text", text="Test result")])
|
||||
)
|
||||
await store.update_task(completed_task.taskId, status="completed")
|
||||
await store.update_task(completed_task.task_id, status="completed")
|
||||
|
||||
# Test get_task_result (default handler)
|
||||
payload_result = await client_session.send_request(
|
||||
ClientRequest(GetTaskPayloadRequest(params=GetTaskPayloadRequestParams(taskId=completed_task.taskId))),
|
||||
ClientRequest(
|
||||
GetTaskPayloadRequest(params=GetTaskPayloadRequestParams(task_id=completed_task.task_id))
|
||||
),
|
||||
GetTaskPayloadResult,
|
||||
)
|
||||
# The result should have the related-task metadata
|
||||
@@ -548,10 +550,10 @@ async def test_default_task_handlers_via_enable_tasks() -> None:
|
||||
|
||||
# Test cancel_task (default handler)
|
||||
cancel_result = await client_session.send_request(
|
||||
ClientRequest(CancelTaskRequest(params=CancelTaskRequestParams(taskId=task.taskId))),
|
||||
ClientRequest(CancelTaskRequest(params=CancelTaskRequestParams(task_id=task.task_id))),
|
||||
CancelTaskResult,
|
||||
)
|
||||
assert cancel_result.taskId == task.taskId
|
||||
assert cancel_result.task_id == task.task_id
|
||||
assert cancel_result.status == "cancelled"
|
||||
|
||||
tg.cancel_scope.cancel()
|
||||
@@ -576,7 +578,7 @@ async def test_build_elicit_form_request() -> None:
|
||||
# Test without task_id
|
||||
request = server_session._build_elicit_form_request(
|
||||
message="Test message",
|
||||
requestedSchema={"type": "object", "properties": {"answer": {"type": "string"}}},
|
||||
requested_schema={"type": "object", "properties": {"answer": {"type": "string"}}},
|
||||
)
|
||||
assert request.method == "elicitation/create"
|
||||
assert request.params is not None
|
||||
@@ -585,7 +587,7 @@ async def test_build_elicit_form_request() -> None:
|
||||
# Test with related_task_id (adds related-task metadata)
|
||||
request_with_task = server_session._build_elicit_form_request(
|
||||
message="Task message",
|
||||
requestedSchema={"type": "object"},
|
||||
requested_schema={"type": "object"},
|
||||
related_task_id="test-task-123",
|
||||
)
|
||||
assert request_with_task.method == "elicitation/create"
|
||||
|
||||
@@ -45,7 +45,7 @@ async def test_server_task_context_properties() -> None:
|
||||
)
|
||||
|
||||
assert ctx.task_id == "test-123"
|
||||
assert ctx.task.taskId == "test-123"
|
||||
assert ctx.task.task_id == "test-123"
|
||||
assert ctx.is_cancelled is False
|
||||
|
||||
store.cleanup()
|
||||
@@ -181,7 +181,7 @@ async def test_elicit_raises_when_client_lacks_capability() -> None:
|
||||
)
|
||||
|
||||
with pytest.raises(McpError) as exc_info:
|
||||
await ctx.elicit(message="Test?", requestedSchema={"type": "object"})
|
||||
await ctx.elicit(message="Test?", requested_schema={"type": "object"})
|
||||
|
||||
assert "elicitation capability" in exc_info.value.error.message
|
||||
mock_session.check_client_capability.assert_called_once()
|
||||
@@ -232,7 +232,7 @@ async def test_elicit_raises_without_handler() -> None:
|
||||
)
|
||||
|
||||
with pytest.raises(RuntimeError, match="handler is required"):
|
||||
await ctx.elicit(message="Test?", requestedSchema={"type": "object"})
|
||||
await ctx.elicit(message="Test?", requested_schema={"type": "object"})
|
||||
|
||||
store.cleanup()
|
||||
|
||||
@@ -320,22 +320,22 @@ async def test_elicit_queues_request_and_waits_for_response() -> None:
|
||||
nonlocal elicit_result
|
||||
elicit_result = await ctx.elicit(
|
||||
message="Test?",
|
||||
requestedSchema={"type": "object"},
|
||||
requested_schema={"type": "object"},
|
||||
)
|
||||
|
||||
async with anyio.create_task_group() as tg:
|
||||
tg.start_soon(run_elicit)
|
||||
|
||||
# Wait for request to be queued
|
||||
await queue.wait_for_message(task.taskId)
|
||||
await queue.wait_for_message(task.task_id)
|
||||
|
||||
# Verify task is in input_required status
|
||||
updated_task = await store.get_task(task.taskId)
|
||||
updated_task = await store.get_task(task.task_id)
|
||||
assert updated_task is not None
|
||||
assert updated_task.status == "input_required"
|
||||
|
||||
# Dequeue and simulate response
|
||||
msg = await queue.dequeue(task.taskId)
|
||||
msg = await queue.dequeue(task.task_id)
|
||||
assert msg is not None
|
||||
assert msg.resolver is not None
|
||||
|
||||
@@ -348,7 +348,7 @@ async def test_elicit_queues_request_and_waits_for_response() -> None:
|
||||
assert elicit_result.content == {"name": "Alice"}
|
||||
|
||||
# Verify task is back to working
|
||||
final_task = await store.get_task(task.taskId)
|
||||
final_task = await store.get_task(task.task_id)
|
||||
assert final_task is not None
|
||||
assert final_task.status == "working"
|
||||
|
||||
@@ -396,15 +396,15 @@ async def test_elicit_url_queues_request_and_waits_for_response() -> None:
|
||||
tg.start_soon(run_elicit_url)
|
||||
|
||||
# Wait for request to be queued
|
||||
await queue.wait_for_message(task.taskId)
|
||||
await queue.wait_for_message(task.task_id)
|
||||
|
||||
# Verify task is in input_required status
|
||||
updated_task = await store.get_task(task.taskId)
|
||||
updated_task = await store.get_task(task.task_id)
|
||||
assert updated_task is not None
|
||||
assert updated_task.status == "input_required"
|
||||
|
||||
# Dequeue and simulate response
|
||||
msg = await queue.dequeue(task.taskId)
|
||||
msg = await queue.dequeue(task.task_id)
|
||||
assert msg is not None
|
||||
assert msg.resolver is not None
|
||||
|
||||
@@ -416,7 +416,7 @@ async def test_elicit_url_queues_request_and_waits_for_response() -> None:
|
||||
assert elicit_result.action == "accept"
|
||||
|
||||
# Verify task is back to working
|
||||
final_task = await store.get_task(task.taskId)
|
||||
final_task = await store.get_task(task.task_id)
|
||||
assert final_task is not None
|
||||
assert final_task.status == "working"
|
||||
|
||||
@@ -463,15 +463,15 @@ async def test_create_message_queues_request_and_waits_for_response() -> None:
|
||||
tg.start_soon(run_sampling)
|
||||
|
||||
# Wait for request to be queued
|
||||
await queue.wait_for_message(task.taskId)
|
||||
await queue.wait_for_message(task.task_id)
|
||||
|
||||
# Verify task is in input_required status
|
||||
updated_task = await store.get_task(task.taskId)
|
||||
updated_task = await store.get_task(task.task_id)
|
||||
assert updated_task is not None
|
||||
assert updated_task.status == "input_required"
|
||||
|
||||
# Dequeue and simulate response
|
||||
msg = await queue.dequeue(task.taskId)
|
||||
msg = await queue.dequeue(task.task_id)
|
||||
assert msg is not None
|
||||
assert msg.resolver is not None
|
||||
|
||||
@@ -491,7 +491,7 @@ async def test_create_message_queues_request_and_waits_for_response() -> None:
|
||||
assert sampling_result.model == "test-model"
|
||||
|
||||
# Verify task is back to working
|
||||
final_task = await store.get_task(task.taskId)
|
||||
final_task = await store.get_task(task.task_id)
|
||||
assert final_task is not None
|
||||
assert final_task.status == "working"
|
||||
|
||||
@@ -534,7 +534,7 @@ async def test_elicit_restores_status_on_cancellation() -> None:
|
||||
try:
|
||||
await ctx.elicit(
|
||||
message="Test?",
|
||||
requestedSchema={"type": "object"},
|
||||
requested_schema={"type": "object"},
|
||||
)
|
||||
except anyio.get_cancelled_exc_class():
|
||||
cancelled_error_raised = True
|
||||
@@ -543,15 +543,15 @@ async def test_elicit_restores_status_on_cancellation() -> None:
|
||||
tg.start_soon(do_elicit)
|
||||
|
||||
# Wait for request to be queued
|
||||
await queue.wait_for_message(task.taskId)
|
||||
await queue.wait_for_message(task.task_id)
|
||||
|
||||
# Verify task is in input_required status
|
||||
updated_task = await store.get_task(task.taskId)
|
||||
updated_task = await store.get_task(task.task_id)
|
||||
assert updated_task is not None
|
||||
assert updated_task.status == "input_required"
|
||||
|
||||
# Get the queued message and set cancellation exception on its resolver
|
||||
msg = await queue.dequeue(task.taskId)
|
||||
msg = await queue.dequeue(task.task_id)
|
||||
assert msg is not None
|
||||
assert msg.resolver is not None
|
||||
|
||||
@@ -559,7 +559,7 @@ async def test_elicit_restores_status_on_cancellation() -> None:
|
||||
msg.resolver.set_exception(asyncio.CancelledError())
|
||||
|
||||
# Verify task is back to working after cancellation
|
||||
final_task = await store.get_task(task.taskId)
|
||||
final_task = await store.get_task(task.task_id)
|
||||
assert final_task is not None
|
||||
assert final_task.status == "working"
|
||||
assert cancelled_error_raised
|
||||
@@ -612,15 +612,15 @@ async def test_create_message_restores_status_on_cancellation() -> None:
|
||||
tg.start_soon(do_sampling)
|
||||
|
||||
# Wait for request to be queued
|
||||
await queue.wait_for_message(task.taskId)
|
||||
await queue.wait_for_message(task.task_id)
|
||||
|
||||
# Verify task is in input_required status
|
||||
updated_task = await store.get_task(task.taskId)
|
||||
updated_task = await store.get_task(task.task_id)
|
||||
assert updated_task is not None
|
||||
assert updated_task.status == "input_required"
|
||||
|
||||
# Get the queued message and set cancellation exception on its resolver
|
||||
msg = await queue.dequeue(task.taskId)
|
||||
msg = await queue.dequeue(task.task_id)
|
||||
assert msg is not None
|
||||
assert msg.resolver is not None
|
||||
|
||||
@@ -628,7 +628,7 @@ async def test_create_message_restores_status_on_cancellation() -> None:
|
||||
msg.resolver.set_exception(asyncio.CancelledError())
|
||||
|
||||
# Verify task is back to working after cancellation
|
||||
final_task = await store.get_task(task.taskId)
|
||||
final_task = await store.get_task(task.task_id)
|
||||
assert final_task is not None
|
||||
assert final_task.status == "working"
|
||||
assert cancelled_error_raised
|
||||
@@ -646,7 +646,7 @@ async def test_elicit_as_task_raises_without_handler() -> None:
|
||||
# Create mock session with proper client capabilities
|
||||
mock_session = Mock()
|
||||
mock_session.client_params = InitializeRequestParams(
|
||||
protocolVersion="2025-01-01",
|
||||
protocol_version="2025-01-01",
|
||||
capabilities=ClientCapabilities(
|
||||
tasks=ClientTasksCapability(
|
||||
requests=ClientTasksRequestsCapability(
|
||||
@@ -654,7 +654,7 @@ async def test_elicit_as_task_raises_without_handler() -> None:
|
||||
)
|
||||
)
|
||||
),
|
||||
clientInfo=Implementation(name="test", version="1.0"),
|
||||
client_info=Implementation(name="test", version="1.0"),
|
||||
)
|
||||
|
||||
ctx = ServerTaskContext(
|
||||
@@ -666,7 +666,7 @@ async def test_elicit_as_task_raises_without_handler() -> None:
|
||||
)
|
||||
|
||||
with pytest.raises(RuntimeError, match="handler is required for elicit_as_task"):
|
||||
await ctx.elicit_as_task(message="Test?", requestedSchema={"type": "object"})
|
||||
await ctx.elicit_as_task(message="Test?", requested_schema={"type": "object"})
|
||||
|
||||
store.cleanup()
|
||||
|
||||
@@ -681,15 +681,15 @@ async def test_create_message_as_task_raises_without_handler() -> None:
|
||||
# Create mock session with proper client capabilities
|
||||
mock_session = Mock()
|
||||
mock_session.client_params = InitializeRequestParams(
|
||||
protocolVersion="2025-01-01",
|
||||
protocol_version="2025-01-01",
|
||||
capabilities=ClientCapabilities(
|
||||
tasks=ClientTasksCapability(
|
||||
requests=ClientTasksRequestsCapability(
|
||||
sampling=TasksSamplingCapability(createMessage=TasksCreateMessageCapability())
|
||||
sampling=TasksSamplingCapability(create_message=TasksCreateMessageCapability())
|
||||
)
|
||||
)
|
||||
),
|
||||
clientInfo=Implementation(name="test", version="1.0"),
|
||||
client_info=Implementation(name="test", version="1.0"),
|
||||
)
|
||||
|
||||
ctx = ServerTaskContext(
|
||||
|
||||
@@ -24,13 +24,13 @@ async def test_create_and_get(store: InMemoryTaskStore) -> None:
|
||||
"""Test InMemoryTaskStore create and get operations."""
|
||||
task = await store.create_task(metadata=TaskMetadata(ttl=60000))
|
||||
|
||||
assert task.taskId is not None
|
||||
assert task.task_id is not None
|
||||
assert task.status == "working"
|
||||
assert task.ttl == 60000
|
||||
|
||||
retrieved = await store.get_task(task.taskId)
|
||||
retrieved = await store.get_task(task.task_id)
|
||||
assert retrieved is not None
|
||||
assert retrieved.taskId == task.taskId
|
||||
assert retrieved.task_id == task.task_id
|
||||
assert retrieved.status == "working"
|
||||
|
||||
|
||||
@@ -42,12 +42,12 @@ async def test_create_with_custom_id(store: InMemoryTaskStore) -> None:
|
||||
task_id="my-custom-id",
|
||||
)
|
||||
|
||||
assert task.taskId == "my-custom-id"
|
||||
assert task.task_id == "my-custom-id"
|
||||
assert task.status == "working"
|
||||
|
||||
retrieved = await store.get_task("my-custom-id")
|
||||
assert retrieved is not None
|
||||
assert retrieved.taskId == "my-custom-id"
|
||||
assert retrieved.task_id == "my-custom-id"
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@@ -71,15 +71,15 @@ async def test_update_status(store: InMemoryTaskStore) -> None:
|
||||
"""Test InMemoryTaskStore status updates."""
|
||||
task = await store.create_task(metadata=TaskMetadata(ttl=60000))
|
||||
|
||||
updated = await store.update_task(task.taskId, status="completed", status_message="All done!")
|
||||
updated = await store.update_task(task.task_id, status="completed", status_message="All done!")
|
||||
|
||||
assert updated.status == "completed"
|
||||
assert updated.statusMessage == "All done!"
|
||||
assert updated.status_message == "All done!"
|
||||
|
||||
retrieved = await store.get_task(task.taskId)
|
||||
retrieved = await store.get_task(task.task_id)
|
||||
assert retrieved is not None
|
||||
assert retrieved.status == "completed"
|
||||
assert retrieved.statusMessage == "All done!"
|
||||
assert retrieved.status_message == "All done!"
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@@ -96,10 +96,10 @@ async def test_store_and_get_result(store: InMemoryTaskStore) -> None:
|
||||
|
||||
# Store result
|
||||
result = CallToolResult(content=[TextContent(type="text", text="Result data")])
|
||||
await store.store_result(task.taskId, result)
|
||||
await store.store_result(task.task_id, result)
|
||||
|
||||
# Retrieve result
|
||||
retrieved_result = await store.get_result(task.taskId)
|
||||
retrieved_result = await store.get_result(task.task_id)
|
||||
assert retrieved_result == result
|
||||
|
||||
|
||||
@@ -114,7 +114,7 @@ async def test_get_result_nonexistent_returns_none(store: InMemoryTaskStore) ->
|
||||
async def test_get_result_no_result_returns_none(store: InMemoryTaskStore) -> None:
|
||||
"""Test that getting result when none stored returns None."""
|
||||
task = await store.create_task(metadata=TaskMetadata(ttl=60000))
|
||||
result = await store.get_result(task.taskId)
|
||||
result = await store.get_result(task.task_id)
|
||||
assert result is None
|
||||
|
||||
|
||||
@@ -172,14 +172,14 @@ async def test_delete_task(store: InMemoryTaskStore) -> None:
|
||||
"""Test InMemoryTaskStore delete operation."""
|
||||
task = await store.create_task(metadata=TaskMetadata(ttl=60000))
|
||||
|
||||
deleted = await store.delete_task(task.taskId)
|
||||
deleted = await store.delete_task(task.task_id)
|
||||
assert deleted is True
|
||||
|
||||
retrieved = await store.get_task(task.taskId)
|
||||
retrieved = await store.get_task(task.task_id)
|
||||
assert retrieved is None
|
||||
|
||||
# Delete non-existent
|
||||
deleted = await store.delete_task(task.taskId)
|
||||
deleted = await store.delete_task(task.task_id)
|
||||
assert deleted is False
|
||||
|
||||
|
||||
@@ -210,7 +210,7 @@ async def test_create_task_with_null_ttl(store: InMemoryTaskStore) -> None:
|
||||
assert task.ttl is None
|
||||
|
||||
# Task should persist (not expire)
|
||||
retrieved = await store.get_task(task.taskId)
|
||||
retrieved = await store.get_task(task.task_id)
|
||||
assert retrieved is not None
|
||||
|
||||
|
||||
@@ -221,19 +221,19 @@ async def test_task_expiration_cleanup(store: InMemoryTaskStore) -> None:
|
||||
task = await store.create_task(metadata=TaskMetadata(ttl=1)) # 1ms TTL
|
||||
|
||||
# Manually force the expiry to be in the past
|
||||
stored = store._tasks.get(task.taskId)
|
||||
stored = store._tasks.get(task.task_id)
|
||||
assert stored is not None
|
||||
stored.expires_at = datetime.now(timezone.utc) - timedelta(seconds=10)
|
||||
|
||||
# Task should still exist in internal dict but be expired
|
||||
assert task.taskId in store._tasks
|
||||
assert task.task_id in store._tasks
|
||||
|
||||
# Any access operation should clean up expired tasks
|
||||
# list_tasks triggers cleanup
|
||||
tasks, _ = await store.list_tasks()
|
||||
|
||||
# Expired task should be cleaned up
|
||||
assert task.taskId not in store._tasks
|
||||
assert task.task_id not in store._tasks
|
||||
assert len(tasks) == 0
|
||||
|
||||
|
||||
@@ -244,17 +244,17 @@ async def test_task_with_null_ttl_never_expires(store: InMemoryTaskStore) -> Non
|
||||
task = await store.create_task(metadata=TaskMetadata(ttl=None))
|
||||
|
||||
# Verify internal storage has no expiry
|
||||
stored = store._tasks.get(task.taskId)
|
||||
stored = store._tasks.get(task.task_id)
|
||||
assert stored is not None
|
||||
assert stored.expires_at is None
|
||||
|
||||
# Access operations should NOT remove this task
|
||||
await store.list_tasks()
|
||||
await store.get_task(task.taskId)
|
||||
await store.get_task(task.task_id)
|
||||
|
||||
# Task should still exist
|
||||
assert task.taskId in store._tasks
|
||||
retrieved = await store.get_task(task.taskId)
|
||||
assert task.task_id in store._tasks
|
||||
retrieved = await store.get_task(task.task_id)
|
||||
assert retrieved is not None
|
||||
|
||||
|
||||
@@ -265,13 +265,13 @@ async def test_terminal_task_ttl_reset(store: InMemoryTaskStore) -> None:
|
||||
task = await store.create_task(metadata=TaskMetadata(ttl=60000)) # 60s
|
||||
|
||||
# Get the initial expiry
|
||||
stored = store._tasks.get(task.taskId)
|
||||
stored = store._tasks.get(task.task_id)
|
||||
assert stored is not None
|
||||
initial_expiry = stored.expires_at
|
||||
assert initial_expiry is not None
|
||||
|
||||
# Update to terminal state (completed)
|
||||
await store.update_task(task.taskId, status="completed")
|
||||
await store.update_task(task.task_id, status="completed")
|
||||
|
||||
# Expiry should be reset to a new time (from now + TTL)
|
||||
new_expiry = stored.expires_at
|
||||
@@ -291,16 +291,16 @@ async def test_terminal_status_transition_rejected(store: InMemoryTaskStore) ->
|
||||
task = await store.create_task(metadata=TaskMetadata(ttl=60000))
|
||||
|
||||
# Move to terminal state
|
||||
await store.update_task(task.taskId, status=terminal_status)
|
||||
await store.update_task(task.task_id, status=terminal_status)
|
||||
|
||||
# Attempting to transition to any other status should raise
|
||||
with pytest.raises(ValueError, match="Cannot transition from terminal status"):
|
||||
await store.update_task(task.taskId, status="working")
|
||||
await store.update_task(task.task_id, status="working")
|
||||
|
||||
# Also test transitioning to another terminal state
|
||||
other_terminal = "failed" if terminal_status != "failed" else "completed"
|
||||
with pytest.raises(ValueError, match="Cannot transition from terminal status"):
|
||||
await store.update_task(task.taskId, status=other_terminal)
|
||||
await store.update_task(task.task_id, status=other_terminal)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@@ -310,15 +310,15 @@ async def test_terminal_status_allows_same_status(store: InMemoryTaskStore) -> N
|
||||
This is not a transition, so it should be allowed (no-op).
|
||||
"""
|
||||
task = await store.create_task(metadata=TaskMetadata(ttl=60000))
|
||||
await store.update_task(task.taskId, status="completed")
|
||||
await store.update_task(task.task_id, status="completed")
|
||||
|
||||
# Setting the same status should not raise
|
||||
updated = await store.update_task(task.taskId, status="completed")
|
||||
updated = await store.update_task(task.task_id, status="completed")
|
||||
assert updated.status == "completed"
|
||||
|
||||
# Updating just the message should also work
|
||||
updated = await store.update_task(task.taskId, status_message="Updated message")
|
||||
assert updated.statusMessage == "Updated message"
|
||||
updated = await store.update_task(task.task_id, status_message="Updated message")
|
||||
assert updated.status_message == "Updated message"
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@@ -334,13 +334,13 @@ async def test_cancel_task_succeeds_for_working_task(store: InMemoryTaskStore) -
|
||||
task = await store.create_task(metadata=TaskMetadata(ttl=60000))
|
||||
assert task.status == "working"
|
||||
|
||||
result = await cancel_task(store, task.taskId)
|
||||
result = await cancel_task(store, task.task_id)
|
||||
|
||||
assert result.taskId == task.taskId
|
||||
assert result.task_id == task.task_id
|
||||
assert result.status == "cancelled"
|
||||
|
||||
# Verify store is updated
|
||||
retrieved = await store.get_task(task.taskId)
|
||||
retrieved = await store.get_task(task.task_id)
|
||||
assert retrieved is not None
|
||||
assert retrieved.status == "cancelled"
|
||||
|
||||
@@ -359,10 +359,10 @@ async def test_cancel_task_rejects_nonexistent_task(store: InMemoryTaskStore) ->
|
||||
async def test_cancel_task_rejects_completed_task(store: InMemoryTaskStore) -> None:
|
||||
"""Test cancel_task raises McpError with INVALID_PARAMS for completed task."""
|
||||
task = await store.create_task(metadata=TaskMetadata(ttl=60000))
|
||||
await store.update_task(task.taskId, status="completed")
|
||||
await store.update_task(task.task_id, status="completed")
|
||||
|
||||
with pytest.raises(McpError) as exc_info:
|
||||
await cancel_task(store, task.taskId)
|
||||
await cancel_task(store, task.task_id)
|
||||
|
||||
assert exc_info.value.error.code == INVALID_PARAMS
|
||||
assert "terminal state 'completed'" in exc_info.value.error.message
|
||||
@@ -372,10 +372,10 @@ async def test_cancel_task_rejects_completed_task(store: InMemoryTaskStore) -> N
|
||||
async def test_cancel_task_rejects_failed_task(store: InMemoryTaskStore) -> None:
|
||||
"""Test cancel_task raises McpError with INVALID_PARAMS for failed task."""
|
||||
task = await store.create_task(metadata=TaskMetadata(ttl=60000))
|
||||
await store.update_task(task.taskId, status="failed")
|
||||
await store.update_task(task.task_id, status="failed")
|
||||
|
||||
with pytest.raises(McpError) as exc_info:
|
||||
await cancel_task(store, task.taskId)
|
||||
await cancel_task(store, task.task_id)
|
||||
|
||||
assert exc_info.value.error.code == INVALID_PARAMS
|
||||
assert "terminal state 'failed'" in exc_info.value.error.message
|
||||
@@ -385,10 +385,10 @@ async def test_cancel_task_rejects_failed_task(store: InMemoryTaskStore) -> None
|
||||
async def test_cancel_task_rejects_already_cancelled_task(store: InMemoryTaskStore) -> None:
|
||||
"""Test cancel_task raises McpError with INVALID_PARAMS for already cancelled task."""
|
||||
task = await store.create_task(metadata=TaskMetadata(ttl=60000))
|
||||
await store.update_task(task.taskId, status="cancelled")
|
||||
await store.update_task(task.task_id, status="cancelled")
|
||||
|
||||
with pytest.raises(McpError) as exc_info:
|
||||
await cancel_task(store, task.taskId)
|
||||
await cancel_task(store, task.task_id)
|
||||
|
||||
assert exc_info.value.error.code == INVALID_PARAMS
|
||||
assert "terminal state 'cancelled'" in exc_info.value.error.message
|
||||
@@ -398,9 +398,9 @@ async def test_cancel_task_rejects_already_cancelled_task(store: InMemoryTaskSto
|
||||
async def test_cancel_task_succeeds_for_input_required_task(store: InMemoryTaskStore) -> None:
|
||||
"""Test cancel_task helper succeeds for a task in input_required status."""
|
||||
task = await store.create_task(metadata=TaskMetadata(ttl=60000))
|
||||
await store.update_task(task.taskId, status="input_required")
|
||||
await store.update_task(task.task_id, status="input_required")
|
||||
|
||||
result = await cancel_task(store, task.taskId)
|
||||
result = await cancel_task(store, task.task_id)
|
||||
|
||||
assert result.taskId == task.taskId
|
||||
assert result.task_id == task.task_id
|
||||
assert result.status == "cancelled"
|
||||
|
||||
@@ -53,13 +53,13 @@ async def test_handle_returns_result_for_completed_task(
|
||||
"""Test that handle() returns the stored result for a completed task."""
|
||||
task = await store.create_task(TaskMetadata(ttl=60000), task_id="test-task")
|
||||
result = CallToolResult(content=[TextContent(type="text", text="Done!")])
|
||||
await store.store_result(task.taskId, result)
|
||||
await store.update_task(task.taskId, status="completed")
|
||||
await store.store_result(task.task_id, result)
|
||||
await store.update_task(task.task_id, status="completed")
|
||||
|
||||
mock_session = Mock()
|
||||
mock_session.send_message = AsyncMock()
|
||||
|
||||
request = GetTaskPayloadRequest(params=GetTaskPayloadRequestParams(taskId=task.taskId))
|
||||
request = GetTaskPayloadRequest(params=GetTaskPayloadRequestParams(task_id=task.task_id))
|
||||
response = await handler.handle(request, mock_session, "req-1")
|
||||
|
||||
assert response is not None
|
||||
@@ -73,7 +73,7 @@ async def test_handle_raises_for_nonexistent_task(
|
||||
) -> None:
|
||||
"""Test that handle() raises McpError for nonexistent task."""
|
||||
mock_session = Mock()
|
||||
request = GetTaskPayloadRequest(params=GetTaskPayloadRequestParams(taskId="nonexistent"))
|
||||
request = GetTaskPayloadRequest(params=GetTaskPayloadRequestParams(task_id="nonexistent"))
|
||||
|
||||
with pytest.raises(McpError) as exc_info:
|
||||
await handler.handle(request, mock_session, "req-1")
|
||||
@@ -87,12 +87,12 @@ async def test_handle_returns_empty_result_when_no_result_stored(
|
||||
) -> None:
|
||||
"""Test that handle() returns minimal result when task completed without stored result."""
|
||||
task = await store.create_task(TaskMetadata(ttl=60000), task_id="test-task")
|
||||
await store.update_task(task.taskId, status="completed")
|
||||
await store.update_task(task.task_id, status="completed")
|
||||
|
||||
mock_session = Mock()
|
||||
mock_session.send_message = AsyncMock()
|
||||
|
||||
request = GetTaskPayloadRequest(params=GetTaskPayloadRequestParams(taskId=task.taskId))
|
||||
request = GetTaskPayloadRequest(params=GetTaskPayloadRequestParams(task_id=task.task_id))
|
||||
response = await handler.handle(request, mock_session, "req-1")
|
||||
|
||||
assert response is not None
|
||||
@@ -116,8 +116,8 @@ async def test_handle_delivers_queued_messages(
|
||||
params={},
|
||||
),
|
||||
)
|
||||
await queue.enqueue(task.taskId, queued_msg)
|
||||
await store.update_task(task.taskId, status="completed")
|
||||
await queue.enqueue(task.task_id, queued_msg)
|
||||
await store.update_task(task.task_id, status="completed")
|
||||
|
||||
sent_messages: list[SessionMessage] = []
|
||||
|
||||
@@ -127,7 +127,7 @@ async def test_handle_delivers_queued_messages(
|
||||
mock_session = Mock()
|
||||
mock_session.send_message = track_send
|
||||
|
||||
request = GetTaskPayloadRequest(params=GetTaskPayloadRequestParams(taskId=task.taskId))
|
||||
request = GetTaskPayloadRequest(params=GetTaskPayloadRequestParams(task_id=task.task_id))
|
||||
await handler.handle(request, mock_session, "req-1")
|
||||
|
||||
assert len(sent_messages) == 1
|
||||
@@ -143,7 +143,7 @@ async def test_handle_waits_for_task_completion(
|
||||
mock_session = Mock()
|
||||
mock_session.send_message = AsyncMock()
|
||||
|
||||
request = GetTaskPayloadRequest(params=GetTaskPayloadRequestParams(taskId=task.taskId))
|
||||
request = GetTaskPayloadRequest(params=GetTaskPayloadRequestParams(task_id=task.task_id))
|
||||
result_holder: list[GetTaskPayloadResult | None] = [None]
|
||||
|
||||
async def run_handle() -> None:
|
||||
@@ -153,11 +153,11 @@ async def test_handle_waits_for_task_completion(
|
||||
tg.start_soon(run_handle)
|
||||
|
||||
# Wait for handler to start waiting (event gets created when wait starts)
|
||||
while task.taskId not in store._update_events:
|
||||
while task.task_id not in store._update_events:
|
||||
await anyio.sleep(0)
|
||||
|
||||
await store.store_result(task.taskId, CallToolResult(content=[TextContent(type="text", text="Done")]))
|
||||
await store.update_task(task.taskId, status="completed")
|
||||
await store.store_result(task.task_id, CallToolResult(content=[TextContent(type="text", text="Done")]))
|
||||
await store.update_task(task.task_id, status="completed")
|
||||
|
||||
assert result_holder[0] is not None
|
||||
|
||||
@@ -248,12 +248,12 @@ async def test_deliver_registers_resolver_for_request_messages(
|
||||
resolver=resolver,
|
||||
original_request_id="inner-req-1",
|
||||
)
|
||||
await queue.enqueue(task.taskId, queued_msg)
|
||||
await queue.enqueue(task.task_id, queued_msg)
|
||||
|
||||
mock_session = Mock()
|
||||
mock_session.send_message = AsyncMock()
|
||||
|
||||
await handler._deliver_queued_messages(task.taskId, mock_session, "outer-req-1")
|
||||
await handler._deliver_queued_messages(task.task_id, mock_session, "outer-req-1")
|
||||
|
||||
assert "inner-req-1" in handler._pending_requests
|
||||
assert handler._pending_requests["inner-req-1"] is resolver
|
||||
@@ -278,12 +278,12 @@ async def test_deliver_skips_resolver_registration_when_no_original_id(
|
||||
resolver=resolver,
|
||||
original_request_id=None, # No original request ID
|
||||
)
|
||||
await queue.enqueue(task.taskId, queued_msg)
|
||||
await queue.enqueue(task.task_id, queued_msg)
|
||||
|
||||
mock_session = Mock()
|
||||
mock_session.send_message = AsyncMock()
|
||||
|
||||
await handler._deliver_queued_messages(task.taskId, mock_session, "outer-req-1")
|
||||
await handler._deliver_queued_messages(task.task_id, mock_session, "outer-req-1")
|
||||
|
||||
# Resolver should NOT be registered since original_request_id is None
|
||||
assert len(handler._pending_requests) == 0
|
||||
@@ -307,10 +307,10 @@ async def test_wait_for_task_update_handles_store_exception(
|
||||
# Queue a message to unblock the race via the queue path
|
||||
async def enqueue_later() -> None:
|
||||
# Wait for queue to start waiting (event gets created when wait starts)
|
||||
while task.taskId not in queue._events:
|
||||
while task.task_id not in queue._events:
|
||||
await anyio.sleep(0)
|
||||
await queue.enqueue(
|
||||
task.taskId,
|
||||
task.task_id,
|
||||
QueuedMessage(
|
||||
type="notification",
|
||||
message=JSONRPCRequest(
|
||||
@@ -325,7 +325,7 @@ async def test_wait_for_task_update_handles_store_exception(
|
||||
async with anyio.create_task_group() as tg:
|
||||
tg.start_soon(enqueue_later)
|
||||
# This should complete via the queue path even though store raises
|
||||
await handler._wait_for_task_update(task.taskId)
|
||||
await handler._wait_for_task_update(task.task_id)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@@ -344,11 +344,11 @@ async def test_wait_for_task_update_handles_queue_exception(
|
||||
# Update the store to unblock the race via the store path
|
||||
async def update_later() -> None:
|
||||
# Wait for store to start waiting (event gets created when wait starts)
|
||||
while task.taskId not in store._update_events:
|
||||
while task.task_id not in store._update_events:
|
||||
await anyio.sleep(0)
|
||||
await store.update_task(task.taskId, status="completed")
|
||||
await store.update_task(task.task_id, status="completed")
|
||||
|
||||
async with anyio.create_task_group() as tg:
|
||||
tg.start_soon(update_later)
|
||||
# This should complete via the store path even though queue raises
|
||||
await handler._wait_for_task_update(task.taskId)
|
||||
await handler._wait_for_task_update(task.task_id)
|
||||
|
||||
@@ -82,7 +82,7 @@ class TestCheckTasksCapability:
|
||||
"""When sampling.createMessage is required but client doesn't have it."""
|
||||
required = ClientTasksCapability(
|
||||
requests=ClientTasksRequestsCapability(
|
||||
sampling=TasksSamplingCapability(createMessage=TasksCreateMessageCapability())
|
||||
sampling=TasksSamplingCapability(create_message=TasksCreateMessageCapability())
|
||||
)
|
||||
)
|
||||
client = ClientTasksCapability(
|
||||
@@ -96,12 +96,12 @@ class TestCheckTasksCapability:
|
||||
"""When sampling.createMessage is required and client has it."""
|
||||
required = ClientTasksCapability(
|
||||
requests=ClientTasksRequestsCapability(
|
||||
sampling=TasksSamplingCapability(createMessage=TasksCreateMessageCapability())
|
||||
sampling=TasksSamplingCapability(create_message=TasksCreateMessageCapability())
|
||||
)
|
||||
)
|
||||
client = ClientTasksCapability(
|
||||
requests=ClientTasksRequestsCapability(
|
||||
sampling=TasksSamplingCapability(createMessage=TasksCreateMessageCapability())
|
||||
sampling=TasksSamplingCapability(create_message=TasksCreateMessageCapability())
|
||||
)
|
||||
)
|
||||
assert check_tasks_capability(required, client) is True
|
||||
@@ -111,13 +111,13 @@ class TestCheckTasksCapability:
|
||||
required = ClientTasksCapability(
|
||||
requests=ClientTasksRequestsCapability(
|
||||
elicitation=TasksElicitationCapability(create=TasksCreateElicitationCapability()),
|
||||
sampling=TasksSamplingCapability(createMessage=TasksCreateMessageCapability()),
|
||||
sampling=TasksSamplingCapability(create_message=TasksCreateMessageCapability()),
|
||||
)
|
||||
)
|
||||
client = ClientTasksCapability(
|
||||
requests=ClientTasksRequestsCapability(
|
||||
elicitation=TasksElicitationCapability(create=TasksCreateElicitationCapability()),
|
||||
sampling=TasksSamplingCapability(createMessage=TasksCreateMessageCapability()),
|
||||
sampling=TasksSamplingCapability(create_message=TasksCreateMessageCapability()),
|
||||
)
|
||||
)
|
||||
assert check_tasks_capability(required, client) is True
|
||||
@@ -145,7 +145,7 @@ class TestCheckTasksCapability:
|
||||
)
|
||||
client = ClientTasksCapability(
|
||||
requests=ClientTasksRequestsCapability(
|
||||
sampling=TasksSamplingCapability(createMessage=TasksCreateMessageCapability())
|
||||
sampling=TasksSamplingCapability(create_message=TasksCreateMessageCapability())
|
||||
)
|
||||
)
|
||||
assert check_tasks_capability(required, client) is True
|
||||
@@ -220,7 +220,7 @@ class TestHasTaskAugmentedSampling:
|
||||
caps = ClientCapabilities(
|
||||
tasks=ClientTasksCapability(
|
||||
requests=ClientTasksRequestsCapability(
|
||||
sampling=TasksSamplingCapability(createMessage=TasksCreateMessageCapability())
|
||||
sampling=TasksSamplingCapability(create_message=TasksCreateMessageCapability())
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -276,7 +276,7 @@ class TestRequireTaskAugmentedSampling:
|
||||
caps = ClientCapabilities(
|
||||
tasks=ClientTasksCapability(
|
||||
requests=ClientTasksRequestsCapability(
|
||||
sampling=TasksSamplingCapability(createMessage=TasksCreateMessageCapability())
|
||||
sampling=TasksSamplingCapability(create_message=TasksCreateMessageCapability())
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -61,13 +61,13 @@ def create_client_task_handlers(
|
||||
"""Handle task-augmented elicitation by creating a client-side task."""
|
||||
elicit_received.set()
|
||||
task = await client_task_store.create_task(task_metadata)
|
||||
task_complete_events[task.taskId] = Event()
|
||||
task_complete_events[task.task_id] = Event()
|
||||
|
||||
async def complete_task() -> None:
|
||||
# Store result before updating status to avoid race condition
|
||||
await client_task_store.store_result(task.taskId, elicit_response)
|
||||
await client_task_store.update_task(task.taskId, status="completed")
|
||||
task_complete_events[task.taskId].set()
|
||||
await client_task_store.store_result(task.task_id, elicit_response)
|
||||
await client_task_store.update_task(task.task_id, status="completed")
|
||||
task_complete_events[task.task_id].set()
|
||||
|
||||
context.session._task_group.start_soon(complete_task) # pyright: ignore[reportPrivateUsage]
|
||||
return CreateTaskResult(task=task)
|
||||
@@ -77,16 +77,16 @@ def create_client_task_handlers(
|
||||
params: Any,
|
||||
) -> GetTaskResult:
|
||||
"""Handle tasks/get from server."""
|
||||
task = await client_task_store.get_task(params.taskId)
|
||||
assert task is not None, f"Task not found: {params.taskId}"
|
||||
task = await client_task_store.get_task(params.task_id)
|
||||
assert task is not None, f"Task not found: {params.task_id}"
|
||||
return GetTaskResult(
|
||||
taskId=task.taskId,
|
||||
task_id=task.task_id,
|
||||
status=task.status,
|
||||
statusMessage=task.statusMessage,
|
||||
createdAt=task.createdAt,
|
||||
lastUpdatedAt=task.lastUpdatedAt,
|
||||
status_message=task.status_message,
|
||||
created_at=task.created_at,
|
||||
last_updated_at=task.last_updated_at,
|
||||
ttl=task.ttl,
|
||||
pollInterval=100,
|
||||
poll_interval=100,
|
||||
)
|
||||
|
||||
async def handle_get_task_result(
|
||||
@@ -94,11 +94,11 @@ def create_client_task_handlers(
|
||||
params: Any,
|
||||
) -> GetTaskPayloadResult | ErrorData:
|
||||
"""Handle tasks/result from server."""
|
||||
event = task_complete_events.get(params.taskId)
|
||||
assert event is not None, f"No completion event for task: {params.taskId}"
|
||||
event = task_complete_events.get(params.task_id)
|
||||
assert event is not None, f"No completion event for task: {params.task_id}"
|
||||
await event.wait()
|
||||
result = await client_task_store.get_result(params.taskId)
|
||||
assert result is not None, f"Result not found for task: {params.taskId}"
|
||||
result = await client_task_store.get_result(params.task_id)
|
||||
assert result is not None, f"Result not found for task: {params.task_id}"
|
||||
return GetTaskPayloadResult.model_validate(result.model_dump(by_alias=True))
|
||||
|
||||
return ExperimentalTaskHandlers(
|
||||
@@ -129,13 +129,13 @@ def create_sampling_task_handlers(
|
||||
"""Handle task-augmented sampling by creating a client-side task."""
|
||||
sampling_received.set()
|
||||
task = await client_task_store.create_task(task_metadata)
|
||||
task_complete_events[task.taskId] = Event()
|
||||
task_complete_events[task.task_id] = Event()
|
||||
|
||||
async def complete_task() -> None:
|
||||
# Store result before updating status to avoid race condition
|
||||
await client_task_store.store_result(task.taskId, sampling_response)
|
||||
await client_task_store.update_task(task.taskId, status="completed")
|
||||
task_complete_events[task.taskId].set()
|
||||
await client_task_store.store_result(task.task_id, sampling_response)
|
||||
await client_task_store.update_task(task.task_id, status="completed")
|
||||
task_complete_events[task.task_id].set()
|
||||
|
||||
context.session._task_group.start_soon(complete_task) # pyright: ignore[reportPrivateUsage]
|
||||
return CreateTaskResult(task=task)
|
||||
@@ -145,16 +145,16 @@ def create_sampling_task_handlers(
|
||||
params: Any,
|
||||
) -> GetTaskResult:
|
||||
"""Handle tasks/get from server."""
|
||||
task = await client_task_store.get_task(params.taskId)
|
||||
assert task is not None, f"Task not found: {params.taskId}"
|
||||
task = await client_task_store.get_task(params.task_id)
|
||||
assert task is not None, f"Task not found: {params.task_id}"
|
||||
return GetTaskResult(
|
||||
taskId=task.taskId,
|
||||
task_id=task.task_id,
|
||||
status=task.status,
|
||||
statusMessage=task.statusMessage,
|
||||
createdAt=task.createdAt,
|
||||
lastUpdatedAt=task.lastUpdatedAt,
|
||||
status_message=task.status_message,
|
||||
created_at=task.created_at,
|
||||
last_updated_at=task.last_updated_at,
|
||||
ttl=task.ttl,
|
||||
pollInterval=100,
|
||||
poll_interval=100,
|
||||
)
|
||||
|
||||
async def handle_get_task_result(
|
||||
@@ -162,11 +162,11 @@ def create_sampling_task_handlers(
|
||||
params: Any,
|
||||
) -> GetTaskPayloadResult | ErrorData:
|
||||
"""Handle tasks/result from server."""
|
||||
event = task_complete_events.get(params.taskId)
|
||||
assert event is not None, f"No completion event for task: {params.taskId}"
|
||||
event = task_complete_events.get(params.task_id)
|
||||
assert event is not None, f"No completion event for task: {params.task_id}"
|
||||
await event.wait()
|
||||
result = await client_task_store.get_result(params.taskId)
|
||||
assert result is not None, f"Result not found for task: {params.taskId}"
|
||||
result = await client_task_store.get_result(params.task_id)
|
||||
assert result is not None, f"Result not found for task: {params.task_id}"
|
||||
return GetTaskPayloadResult.model_validate(result.model_dump(by_alias=True))
|
||||
|
||||
return ExperimentalTaskHandlers(
|
||||
@@ -193,7 +193,7 @@ async def test_scenario1_normal_tool_normal_elicitation() -> None:
|
||||
Tool(
|
||||
name="confirm_action",
|
||||
description="Confirm an action",
|
||||
inputSchema={"type": "object"},
|
||||
input_schema={"type": "object"},
|
||||
)
|
||||
]
|
||||
|
||||
@@ -204,7 +204,7 @@ async def test_scenario1_normal_tool_normal_elicitation() -> None:
|
||||
# Normal elicitation - expects immediate response
|
||||
result = await ctx.session.elicit(
|
||||
message="Please confirm the action",
|
||||
requestedSchema={"type": "object", "properties": {"confirm": {"type": "boolean"}}},
|
||||
requested_schema={"type": "object", "properties": {"confirm": {"type": "boolean"}}},
|
||||
)
|
||||
|
||||
confirmed = result.content.get("confirm", False) if result.content else False
|
||||
@@ -278,7 +278,7 @@ async def test_scenario2_normal_tool_task_augmented_elicitation() -> None:
|
||||
Tool(
|
||||
name="confirm_action",
|
||||
description="Confirm an action",
|
||||
inputSchema={"type": "object"},
|
||||
input_schema={"type": "object"},
|
||||
)
|
||||
]
|
||||
|
||||
@@ -289,7 +289,7 @@ async def test_scenario2_normal_tool_task_augmented_elicitation() -> None:
|
||||
# Task-augmented elicitation - server polls client
|
||||
result = await ctx.session.experimental.elicit_as_task(
|
||||
message="Please confirm the action",
|
||||
requestedSchema={"type": "object", "properties": {"confirm": {"type": "boolean"}}},
|
||||
requested_schema={"type": "object", "properties": {"confirm": {"type": "boolean"}}},
|
||||
ttl=60000,
|
||||
)
|
||||
|
||||
@@ -358,8 +358,8 @@ async def test_scenario3_task_augmented_tool_normal_elicitation() -> None:
|
||||
Tool(
|
||||
name="confirm_action",
|
||||
description="Confirm an action",
|
||||
inputSchema={"type": "object"},
|
||||
execution=ToolExecution(taskSupport=TASK_REQUIRED),
|
||||
input_schema={"type": "object"},
|
||||
execution=ToolExecution(task_support=TASK_REQUIRED),
|
||||
)
|
||||
]
|
||||
|
||||
@@ -372,7 +372,7 @@ async def test_scenario3_task_augmented_tool_normal_elicitation() -> None:
|
||||
# Normal elicitation within task - queued and delivered via tasks/result
|
||||
result = await task.elicit(
|
||||
message="Please confirm the action",
|
||||
requestedSchema={"type": "object", "properties": {"confirm": {"type": "boolean"}}},
|
||||
requested_schema={"type": "object", "properties": {"confirm": {"type": "boolean"}}},
|
||||
)
|
||||
|
||||
confirmed = result.content.get("confirm", False) if result.content else False
|
||||
@@ -413,7 +413,7 @@ async def test_scenario3_task_augmented_tool_normal_elicitation() -> None:
|
||||
|
||||
# Call tool as task
|
||||
create_result = await client_session.experimental.call_tool_as_task("confirm_action", {})
|
||||
task_id = create_result.task.taskId
|
||||
task_id = create_result.task.task_id
|
||||
assert create_result.task.status == "working"
|
||||
|
||||
# Poll until input_required, then call tasks/result
|
||||
@@ -472,8 +472,8 @@ async def test_scenario4_task_augmented_tool_task_augmented_elicitation() -> Non
|
||||
Tool(
|
||||
name="confirm_action",
|
||||
description="Confirm an action",
|
||||
inputSchema={"type": "object"},
|
||||
execution=ToolExecution(taskSupport=TASK_REQUIRED),
|
||||
input_schema={"type": "object"},
|
||||
execution=ToolExecution(task_support=TASK_REQUIRED),
|
||||
)
|
||||
]
|
||||
|
||||
@@ -486,7 +486,7 @@ async def test_scenario4_task_augmented_tool_task_augmented_elicitation() -> Non
|
||||
# Task-augmented elicitation within task - server polls client
|
||||
result = await task.elicit_as_task(
|
||||
message="Please confirm the action",
|
||||
requestedSchema={"type": "object", "properties": {"confirm": {"type": "boolean"}}},
|
||||
requested_schema={"type": "object", "properties": {"confirm": {"type": "boolean"}}},
|
||||
ttl=60000,
|
||||
)
|
||||
|
||||
@@ -522,7 +522,7 @@ async def test_scenario4_task_augmented_tool_task_augmented_elicitation() -> Non
|
||||
|
||||
# Call tool as task
|
||||
create_result = await client_session.experimental.call_tool_as_task("confirm_action", {})
|
||||
task_id = create_result.task.taskId
|
||||
task_id = create_result.task.task_id
|
||||
assert create_result.task.status == "working"
|
||||
|
||||
# Poll until input_required or terminal, then call tasks/result
|
||||
@@ -572,7 +572,7 @@ async def test_scenario2_sampling_normal_tool_task_augmented_sampling() -> None:
|
||||
Tool(
|
||||
name="generate_text",
|
||||
description="Generate text using sampling",
|
||||
inputSchema={"type": "object"},
|
||||
input_schema={"type": "object"},
|
||||
)
|
||||
]
|
||||
|
||||
@@ -658,8 +658,8 @@ async def test_scenario4_sampling_task_augmented_tool_task_augmented_sampling()
|
||||
Tool(
|
||||
name="generate_text",
|
||||
description="Generate text using sampling",
|
||||
inputSchema={"type": "object"},
|
||||
execution=ToolExecution(taskSupport=TASK_REQUIRED),
|
||||
input_schema={"type": "object"},
|
||||
execution=ToolExecution(task_support=TASK_REQUIRED),
|
||||
)
|
||||
]
|
||||
|
||||
@@ -710,7 +710,7 @@ async def test_scenario4_sampling_task_augmented_tool_task_augmented_sampling()
|
||||
|
||||
# Call tool as task
|
||||
create_result = await client_session.experimental.call_tool_as_task("generate_text", {})
|
||||
task_id = create_result.task.taskId
|
||||
task_id = create_result.task.task_id
|
||||
assert create_result.task.status == "working"
|
||||
|
||||
# Poll until input_required or terminal
|
||||
|
||||
@@ -108,8 +108,8 @@ def test_validate_for_tool_with_execution_required() -> None:
|
||||
tool = Tool(
|
||||
name="test",
|
||||
description="test",
|
||||
inputSchema={"type": "object"},
|
||||
execution=ToolExecution(taskSupport=TASK_REQUIRED),
|
||||
input_schema={"type": "object"},
|
||||
execution=ToolExecution(task_support=TASK_REQUIRED),
|
||||
)
|
||||
error = exp.validate_for_tool(tool, raise_error=False)
|
||||
assert error is not None
|
||||
@@ -121,7 +121,7 @@ def test_validate_for_tool_without_execution() -> None:
|
||||
tool = Tool(
|
||||
name="test",
|
||||
description="test",
|
||||
inputSchema={"type": "object"},
|
||||
input_schema={"type": "object"},
|
||||
execution=None,
|
||||
)
|
||||
error = exp.validate_for_tool(tool, raise_error=False)
|
||||
@@ -134,8 +134,8 @@ def test_validate_for_tool_optional_with_task() -> None:
|
||||
tool = Tool(
|
||||
name="test",
|
||||
description="test",
|
||||
inputSchema={"type": "object"},
|
||||
execution=ToolExecution(taskSupport=TASK_OPTIONAL),
|
||||
input_schema={"type": "object"},
|
||||
execution=ToolExecution(task_support=TASK_OPTIONAL),
|
||||
)
|
||||
error = exp.validate_for_tool(tool, raise_error=False)
|
||||
assert error is None
|
||||
|
||||
@@ -346,10 +346,10 @@ class TestCreatingTask:
|
||||
|
||||
# CreateTaskResult can include model-immediate-response in _meta
|
||||
task = Task(
|
||||
taskId="test-123",
|
||||
task_id="test-123",
|
||||
status="working",
|
||||
createdAt=TEST_DATETIME,
|
||||
lastUpdatedAt=TEST_DATETIME,
|
||||
created_at=TEST_DATETIME,
|
||||
last_updated_at=TEST_DATETIME,
|
||||
ttl=60000,
|
||||
)
|
||||
immediate_msg = "Task started, processing your request..."
|
||||
|
||||
@@ -89,7 +89,7 @@ async def test_lifespan_cleanup_executed():
|
||||
async with ClientSession(read, write) as session:
|
||||
# Initialize the session
|
||||
result = await session.initialize()
|
||||
assert result.protocolVersion in ["2024-11-05", "2025-06-18", "2025-11-25"]
|
||||
assert result.protocol_version in ["2024-11-05", "2025-06-18", "2025-11-25"]
|
||||
|
||||
# Verify startup marker was created
|
||||
assert Path(startup_marker).exists(), "Server startup marker not created"
|
||||
|
||||
@@ -27,16 +27,16 @@ async def test_resource_templates():
|
||||
types.ListResourceTemplatesRequest(params=None)
|
||||
)
|
||||
assert isinstance(result.root, types.ListResourceTemplatesResult)
|
||||
templates = result.root.resourceTemplates
|
||||
templates = result.root.resource_templates
|
||||
|
||||
# Verify we get both templates back
|
||||
assert len(templates) == 2
|
||||
|
||||
# Verify template details
|
||||
greeting_template = next(t for t in templates if t.name == "get_greeting") # pragma: no cover
|
||||
assert greeting_template.uriTemplate == "greeting://{name}"
|
||||
assert greeting_template.uri_template == "greeting://{name}"
|
||||
assert greeting_template.description == "Get a personalized greeting"
|
||||
|
||||
profile_template = next(t for t in templates if t.name == "get_user_profile") # pragma: no cover
|
||||
assert profile_template.uriTemplate == "users://{user_id}/profile"
|
||||
assert profile_template.uri_template == "users://{user_id}/profile"
|
||||
assert profile_template.description == "Dynamic user data"
|
||||
|
||||
@@ -14,7 +14,7 @@ async def test_icons_and_website_url():
|
||||
# Create test icon
|
||||
test_icon = Icon(
|
||||
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==",
|
||||
mimeType="image/png",
|
||||
mime_type="image/png",
|
||||
sizes=["1x1"],
|
||||
)
|
||||
|
||||
@@ -51,7 +51,7 @@ async def test_icons_and_website_url():
|
||||
assert mcp.icons is not None
|
||||
assert len(mcp.icons) == 1
|
||||
assert mcp.icons[0].src == test_icon.src
|
||||
assert mcp.icons[0].mimeType == test_icon.mimeType
|
||||
assert mcp.icons[0].mime_type == test_icon.mime_type
|
||||
assert mcp.icons[0].sizes == test_icon.sizes
|
||||
|
||||
# Test tool includes icon
|
||||
@@ -86,7 +86,7 @@ async def test_icons_and_website_url():
|
||||
assert len(templates) == 1
|
||||
template = templates[0]
|
||||
assert template.name == "test_resource_template"
|
||||
assert template.uriTemplate == "test://weather/{city}"
|
||||
assert template.uri_template == "test://weather/{city}"
|
||||
assert template.icons is not None
|
||||
assert len(template.icons) == 1
|
||||
assert template.icons[0].src == test_icon.src
|
||||
@@ -96,9 +96,9 @@ async def test_multiple_icons():
|
||||
"""Test that multiple icons can be added to tools, resources, and prompts."""
|
||||
|
||||
# Create multiple test icons
|
||||
icon1 = Icon(src="data:image/png;base64,icon1", mimeType="image/png", sizes=["16x16"])
|
||||
icon2 = Icon(src="data:image/png;base64,icon2", mimeType="image/png", sizes=["32x32"])
|
||||
icon3 = Icon(src="data:image/png;base64,icon3", mimeType="image/png", sizes=["64x64"])
|
||||
icon1 = Icon(src="data:image/png;base64,icon1", mime_type="image/png", sizes=["16x16"])
|
||||
icon2 = Icon(src="data:image/png;base64,icon2", mime_type="image/png", sizes=["32x32"])
|
||||
icon3 = Icon(src="data:image/png;base64,icon3", mime_type="image/png", sizes=["64x64"])
|
||||
|
||||
mcp = FastMCP("MultiIconServer")
|
||||
|
||||
|
||||
@@ -85,10 +85,10 @@ async def test_resource_template_client_interaction():
|
||||
# List available resources
|
||||
resources = await session.list_resource_templates()
|
||||
assert isinstance(resources, ListResourceTemplatesResult)
|
||||
assert len(resources.resourceTemplates) == 2
|
||||
assert len(resources.resource_templates) == 2
|
||||
|
||||
# Verify resource templates are listed correctly
|
||||
templates = [r.uriTemplate for r in resources.resourceTemplates]
|
||||
templates = [r.uri_template for r in resources.resource_templates]
|
||||
assert "resource://users/{user_id}/posts/{post_id}" in templates
|
||||
assert "resource://users/{user_id}/profile" in templates
|
||||
|
||||
@@ -97,14 +97,14 @@ async def test_resource_template_client_interaction():
|
||||
contents = result.contents[0]
|
||||
assert isinstance(contents, TextResourceContents)
|
||||
assert contents.text == "Post 456 by user 123"
|
||||
assert contents.mimeType == "text/plain"
|
||||
assert contents.mime_type == "text/plain"
|
||||
|
||||
# Read another resource with valid parameters
|
||||
result = await session.read_resource(AnyUrl("resource://users/789/profile"))
|
||||
contents = result.contents[0]
|
||||
assert isinstance(contents, TextResourceContents)
|
||||
assert contents.text == "Profile for user 789"
|
||||
assert contents.mimeType == "text/plain"
|
||||
assert contents.mime_type == "text/plain"
|
||||
|
||||
# Verify invalid resource URIs raise appropriate errors
|
||||
with pytest.raises(Exception): # Specific exception type may vary
|
||||
|
||||
@@ -45,19 +45,19 @@ async def test_fastmcp_resource_mime_type():
|
||||
bytes_resource = mapping["test://image_bytes"]
|
||||
|
||||
# Verify mime types
|
||||
assert string_resource.mimeType == "image/png", "String resource mime type not respected"
|
||||
assert bytes_resource.mimeType == "image/png", "Bytes resource mime type not respected"
|
||||
assert string_resource.mime_type == "image/png", "String 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
|
||||
string_result = await client.read_resource(AnyUrl("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].mimeType == "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"))
|
||||
assert len(bytes_result.contents) == 1
|
||||
assert base64.b64decode(getattr(bytes_result.contents[0], "blob")) == image_bytes, "Bytes mismatch"
|
||||
assert bytes_result.contents[0].mimeType == "image/png", "Bytes content mime type not preserved"
|
||||
assert bytes_result.contents[0].mime_type == "image/png", "Bytes content mime type not preserved"
|
||||
|
||||
|
||||
async def test_lowlevel_resource_mime_type():
|
||||
@@ -70,11 +70,11 @@ async def test_lowlevel_resource_mime_type():
|
||||
|
||||
# Create test resources with specific mime types
|
||||
test_resources = [
|
||||
types.Resource(uri="test://image", name="test image", mimeType="image/png"),
|
||||
types.Resource(uri="test://image", name="test image", mime_type="image/png"),
|
||||
types.Resource(
|
||||
uri="test://image_bytes",
|
||||
name="test image bytes",
|
||||
mimeType="image/png",
|
||||
mime_type="image/png",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -103,16 +103,16 @@ async def test_lowlevel_resource_mime_type():
|
||||
bytes_resource = mapping["test://image_bytes"]
|
||||
|
||||
# Verify mime types
|
||||
assert string_resource.mimeType == "image/png", "String resource mime type not respected"
|
||||
assert bytes_resource.mimeType == "image/png", "Bytes resource mime type not respected"
|
||||
assert string_resource.mime_type == "image/png", "String 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
|
||||
string_result = await client.read_resource(AnyUrl("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].mimeType == "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"))
|
||||
assert len(bytes_result.contents) == 1
|
||||
assert base64.b64decode(getattr(bytes_result.contents[0], "blob")) == image_bytes, "Bytes mismatch"
|
||||
assert bytes_result.contents[0].mimeType == "image/png", "Bytes content mime type not preserved"
|
||||
assert bytes_result.contents[0].mime_type == "image/png", "Bytes content mime type not preserved"
|
||||
|
||||
@@ -125,7 +125,7 @@ def test_resource_contents_uri_json_roundtrip():
|
||||
contents = types.TextResourceContents(
|
||||
uri=uri_str,
|
||||
text="data",
|
||||
mimeType="text/plain",
|
||||
mime_type="text/plain",
|
||||
)
|
||||
json_data = contents.model_dump(mode="json")
|
||||
restored = types.TextResourceContents.model_validate(json_data)
|
||||
|
||||
@@ -26,7 +26,7 @@ async def test_mime_type_with_parameters():
|
||||
|
||||
resources = await mcp.list_resources()
|
||||
assert len(resources) == 1
|
||||
assert resources[0].mimeType == "text/html;profile=mcp-app"
|
||||
assert resources[0].mime_type == "text/html;profile=mcp-app"
|
||||
|
||||
|
||||
async def test_mime_type_with_parameters_and_space():
|
||||
@@ -39,7 +39,7 @@ async def test_mime_type_with_parameters_and_space():
|
||||
|
||||
resources = await mcp.list_resources()
|
||||
assert len(resources) == 1
|
||||
assert resources[0].mimeType == "application/json; charset=utf-8"
|
||||
assert resources[0].mime_type == "application/json; charset=utf-8"
|
||||
|
||||
|
||||
async def test_mime_type_with_multiple_parameters():
|
||||
@@ -52,7 +52,7 @@ async def test_mime_type_with_multiple_parameters():
|
||||
|
||||
resources = await mcp.list_resources()
|
||||
assert len(resources) == 1
|
||||
assert resources[0].mimeType == "text/plain; charset=utf-8; format=fixed"
|
||||
assert resources[0].mime_type == "text/plain; charset=utf-8; format=fixed"
|
||||
|
||||
|
||||
async def test_mime_type_preserved_in_read_resource():
|
||||
@@ -67,4 +67,4 @@ async def test_mime_type_preserved_in_read_resource():
|
||||
# Read the resource
|
||||
result = await client.read_resource(AnyUrl("ui://my-widget"))
|
||||
assert len(result.contents) == 1
|
||||
assert result.contents[0].mimeType == "text/html;profile=mcp-app"
|
||||
assert result.contents[0].mime_type == "text/html;profile=mcp-app"
|
||||
|
||||
@@ -17,7 +17,7 @@ async def test_progress_token_zero_first_call():
|
||||
|
||||
# Create request context with progress token 0
|
||||
mock_meta = MagicMock()
|
||||
mock_meta.progressToken = 0 # This is the key test case - token is 0
|
||||
mock_meta.progress_token = 0 # This is the key test case - token is 0
|
||||
|
||||
request_context = RequestContext(
|
||||
request_id="test-request",
|
||||
|
||||
@@ -59,9 +59,9 @@ async def test_request_id_match() -> None:
|
||||
id="init-1",
|
||||
method="initialize",
|
||||
params=InitializeRequestParams(
|
||||
protocolVersion=LATEST_PROTOCOL_VERSION,
|
||||
protocol_version=LATEST_PROTOCOL_VERSION,
|
||||
capabilities=ClientCapabilities(),
|
||||
clientInfo=Implementation(name="test-client", version="1.0.0"),
|
||||
client_info=Implementation(name="test-client", version="1.0.0"),
|
||||
).model_dump(by_alias=True, exclude_none=True),
|
||||
jsonrpc="2.0",
|
||||
)
|
||||
|
||||
@@ -42,12 +42,12 @@ async def test_notification_validation_error(tmp_path: Path):
|
||||
types.Tool(
|
||||
name="slow",
|
||||
description="A slow tool",
|
||||
inputSchema={"type": "object"},
|
||||
input_schema={"type": "object"},
|
||||
),
|
||||
types.Tool(
|
||||
name="fast",
|
||||
description="A fast tool",
|
||||
inputSchema={"type": "object"},
|
||||
input_schema={"type": "object"},
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ class TestRenderPrompt:
|
||||
resource=TextResourceContents(
|
||||
uri="file://file.txt",
|
||||
text="File contents",
|
||||
mimeType="text/plain",
|
||||
mime_type="text/plain",
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -109,7 +109,7 @@ class TestRenderPrompt:
|
||||
resource=TextResourceContents(
|
||||
uri="file://file.txt",
|
||||
text="File contents",
|
||||
mimeType="text/plain",
|
||||
mime_type="text/plain",
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -128,7 +128,7 @@ class TestRenderPrompt:
|
||||
resource=TextResourceContents(
|
||||
uri="file://file.txt",
|
||||
text="File contents",
|
||||
mimeType="text/plain",
|
||||
mime_type="text/plain",
|
||||
),
|
||||
)
|
||||
),
|
||||
@@ -144,7 +144,7 @@ class TestRenderPrompt:
|
||||
resource=TextResourceContents(
|
||||
uri="file://file.txt",
|
||||
text="File contents",
|
||||
mimeType="text/plain",
|
||||
mime_type="text/plain",
|
||||
),
|
||||
)
|
||||
),
|
||||
@@ -176,7 +176,7 @@ class TestRenderPrompt:
|
||||
resource=TextResourceContents(
|
||||
uri="file://file.txt",
|
||||
text="File contents",
|
||||
mimeType="text/plain",
|
||||
mime_type="text/plain",
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -290,7 +290,7 @@ async def test_elicitation_with_default_values():
|
||||
async def callback_schema_verify(context: RequestContext[ClientSession, None], params: ElicitRequestParams):
|
||||
# Verify the schema includes defaults
|
||||
assert isinstance(params, types.ElicitRequestFormParams), "Expected form mode elicitation"
|
||||
schema = params.requestedSchema
|
||||
schema = params.requested_schema
|
||||
props = schema["properties"]
|
||||
|
||||
assert props["name"]["default"] == "Guest"
|
||||
|
||||
@@ -850,7 +850,7 @@ def test_tool_call_result_annotated_is_structured_and_converted():
|
||||
name: str
|
||||
|
||||
def func_returning_annotated_tool_call_result() -> Annotated[CallToolResult, PersonClass]: # pragma: no cover
|
||||
return CallToolResult(content=[], structuredContent={"name": "Brandon"})
|
||||
return CallToolResult(content=[], structured_content={"name": "Brandon"})
|
||||
|
||||
meta = func_metadata(func_returning_annotated_tool_call_result)
|
||||
|
||||
@@ -870,7 +870,7 @@ def test_tool_call_result_annotated_is_structured_and_invalid():
|
||||
name: str
|
||||
|
||||
def func_returning_annotated_tool_call_result() -> Annotated[CallToolResult, PersonClass]: # pragma: no cover
|
||||
return CallToolResult(content=[], structuredContent={"person": "Brandon"})
|
||||
return CallToolResult(content=[], structured_content={"person": "Brandon"})
|
||||
|
||||
meta = func_metadata(func_returning_annotated_tool_call_result)
|
||||
|
||||
|
||||
@@ -258,7 +258,7 @@ async def test_basic_tools(server_transport: str, server_url: str) -> None:
|
||||
# Test initialization
|
||||
result = await session.initialize()
|
||||
assert isinstance(result, InitializeResult)
|
||||
assert result.serverInfo.name == "Tool Example"
|
||||
assert result.server_info.name == "Tool Example"
|
||||
assert result.capabilities.tools is not None
|
||||
|
||||
# Test sum tool
|
||||
@@ -295,7 +295,7 @@ async def test_basic_resources(server_transport: str, server_url: str) -> None:
|
||||
# Test initialization
|
||||
result = await session.initialize()
|
||||
assert isinstance(result, InitializeResult)
|
||||
assert result.serverInfo.name == "Resource Example"
|
||||
assert result.server_info.name == "Resource Example"
|
||||
assert result.capabilities.resources is not None
|
||||
|
||||
# Test document resource
|
||||
@@ -336,7 +336,7 @@ async def test_basic_prompts(server_transport: str, server_url: str) -> None:
|
||||
# Test initialization
|
||||
result = await session.initialize()
|
||||
assert isinstance(result, InitializeResult)
|
||||
assert result.serverInfo.name == "Prompt Example"
|
||||
assert result.server_info.name == "Prompt Example"
|
||||
assert result.capabilities.prompts is not None
|
||||
|
||||
# Test review_code prompt
|
||||
@@ -396,7 +396,7 @@ async def test_tool_progress(server_transport: str, server_url: str) -> None:
|
||||
# Test initialization
|
||||
result = await session.initialize()
|
||||
assert isinstance(result, InitializeResult)
|
||||
assert result.serverInfo.name == "Progress Example"
|
||||
assert result.server_info.name == "Progress Example"
|
||||
|
||||
# Test progress callback
|
||||
progress_updates = []
|
||||
@@ -449,7 +449,7 @@ async def test_sampling(server_transport: str, server_url: str) -> None:
|
||||
# Test initialization
|
||||
result = await session.initialize()
|
||||
assert isinstance(result, InitializeResult)
|
||||
assert result.serverInfo.name == "Sampling Example"
|
||||
assert result.server_info.name == "Sampling Example"
|
||||
assert result.capabilities.tools is not None
|
||||
|
||||
# Test sampling tool
|
||||
@@ -480,7 +480,7 @@ async def test_elicitation(server_transport: str, server_url: str) -> None:
|
||||
# Test initialization
|
||||
result = await session.initialize()
|
||||
assert isinstance(result, InitializeResult)
|
||||
assert result.serverInfo.name == "Elicitation Example"
|
||||
assert result.server_info.name == "Elicitation Example"
|
||||
|
||||
# Test booking with unavailable date (triggers elicitation)
|
||||
booking_result = await session.call_tool(
|
||||
@@ -537,7 +537,7 @@ async def test_notifications(server_transport: str, server_url: str) -> None:
|
||||
# Test initialization
|
||||
result = await session.initialize()
|
||||
assert isinstance(result, InitializeResult)
|
||||
assert result.serverInfo.name == "Notifications Example"
|
||||
assert result.server_info.name == "Notifications Example"
|
||||
|
||||
# Call tool that generates notifications
|
||||
tool_result = await session.call_tool("process_data", {"data": "test_data"})
|
||||
@@ -578,7 +578,7 @@ async def test_completion(server_transport: str, server_url: str) -> None:
|
||||
# Test initialization
|
||||
result = await session.initialize()
|
||||
assert isinstance(result, InitializeResult)
|
||||
assert result.serverInfo.name == "Example"
|
||||
assert result.server_info.name == "Example"
|
||||
assert result.capabilities.resources is not None
|
||||
assert result.capabilities.prompts is not None
|
||||
|
||||
@@ -635,7 +635,7 @@ async def test_fastmcp_quickstart(server_transport: str, server_url: str) -> Non
|
||||
# Test initialization
|
||||
result = await session.initialize()
|
||||
assert isinstance(result, InitializeResult)
|
||||
assert result.serverInfo.name == "Demo"
|
||||
assert result.server_info.name == "Demo"
|
||||
|
||||
# Test add tool
|
||||
tool_result = await session.call_tool("add", {"a": 10, "b": 20})
|
||||
@@ -673,7 +673,7 @@ async def test_structured_output(server_transport: str, server_url: str) -> None
|
||||
# Test initialization
|
||||
result = await session.initialize()
|
||||
assert isinstance(result, InitializeResult)
|
||||
assert result.serverInfo.name == "Structured Output Example"
|
||||
assert result.server_info.name == "Structured Output Example"
|
||||
|
||||
# Test get_weather tool
|
||||
weather_result = await session.call_tool("get_weather", {"city": "New York"})
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user