mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-23 05:54:19 +02:00
Adopt snake_case SDK type attribute access
This commit is contained in:
parent
fdac9f9f2e
commit
ed85d477f5
77 changed files with 410 additions and 406 deletions
|
|
@ -792,7 +792,7 @@ _LOG_PANEL_HTML = """\
|
|||
function renderEntry(entry) {
|
||||
var div = document.createElement("div");
|
||||
var isError = entry.direction === "response" && entry.body
|
||||
&& (entry.body.error || (entry.body.result && entry.body.result.isError));
|
||||
&& (entry.body.error || (entry.body.result && entry.body.result.is_error));
|
||||
div.className = "log-entry" + (isError ? " error" : "");
|
||||
var dirClass = isError ? "error" : entry.direction;
|
||||
var arrows = {request: "\u2192", response: "\u2190", bridge: "\u2191", notification: "\u2193"};
|
||||
|
|
|
|||
|
|
@ -191,7 +191,7 @@ async def _terminal_elicitation_handler(
|
|||
return ElicitResult(action="cancel")
|
||||
return ElicitResult(action="accept", content={})
|
||||
|
||||
schema = params.requestedSchema
|
||||
schema = params.requested_schema
|
||||
properties = schema.get("properties", {})
|
||||
required = set(schema.get("required", []))
|
||||
|
||||
|
|
@ -371,7 +371,7 @@ def format_tool_signature(tool: mcp_types.Tool) -> str:
|
|||
"""Build ``name(param: type, ...) -> return_type`` from a tool's JSON schemas."""
|
||||
|
||||
params: list[str] = []
|
||||
schema = tool.inputSchema
|
||||
schema = tool.input_schema
|
||||
properties = schema.get("properties", {})
|
||||
required = set(schema.get("required", []))
|
||||
|
||||
|
|
@ -386,8 +386,8 @@ def format_tool_signature(tool: mcp_types.Tool) -> str:
|
|||
|
||||
sig = f"{tool.name}({', '.join(params)})"
|
||||
|
||||
if tool.outputSchema:
|
||||
ret = _json_schema_type_to_str(tool.outputSchema)
|
||||
if tool.output_schema:
|
||||
ret = _json_schema_type_to_str(tool.output_schema)
|
||||
sig += f" -> {ret}"
|
||||
|
||||
return sig
|
||||
|
|
@ -441,10 +441,10 @@ def _format_call_result_text(result: CallToolResult) -> None:
|
|||
console.print(_sanitize_untrusted_text(block.text))
|
||||
elif isinstance(block, mcp_types.ImageContent):
|
||||
size = len(block.data) * 3 // 4 # rough decoded size
|
||||
console.print(f"[dim][Image: {block.mimeType}, ~{size} bytes][/dim]")
|
||||
console.print(f"[dim][Image: {block.mime_type}, ~{size} bytes][/dim]")
|
||||
elif isinstance(block, mcp_types.AudioContent):
|
||||
size = len(block.data) * 3 // 4
|
||||
console.print(f"[dim][Audio: {block.mimeType}, ~{size} bytes][/dim]")
|
||||
console.print(f"[dim][Audio: {block.mime_type}, ~{size} bytes][/dim]")
|
||||
else:
|
||||
console.print(_sanitize_untrusted_text(str(block)))
|
||||
|
||||
|
|
@ -454,9 +454,9 @@ def _content_block_to_dict(block: mcp_types.ContentBlock) -> dict[str, Any]:
|
|||
if isinstance(block, mcp_types.TextContent):
|
||||
return {"type": "text", "text": block.text}
|
||||
if isinstance(block, mcp_types.ImageContent):
|
||||
return {"type": "image", "mimeType": block.mimeType, "data": block.data}
|
||||
return {"type": "image", "mimeType": block.mime_type, "data": block.data}
|
||||
if isinstance(block, mcp_types.AudioContent):
|
||||
return {"type": "audio", "mimeType": block.mimeType, "data": block.data}
|
||||
return {"type": "audio", "mimeType": block.mime_type, "data": block.data}
|
||||
return {"type": "unknown", "value": str(block)}
|
||||
|
||||
|
||||
|
|
@ -477,8 +477,8 @@ def _tools_to_json(tools: list[mcp_types.Tool]) -> list[dict[str, Any]]:
|
|||
{
|
||||
"name": t.name,
|
||||
"description": t.description,
|
||||
"inputSchema": t.inputSchema,
|
||||
**({"outputSchema": t.outputSchema} if t.outputSchema else {}),
|
||||
"inputSchema": t.input_schema,
|
||||
**({"outputSchema": t.output_schema} if t.output_schema else {}),
|
||||
}
|
||||
for t in tools
|
||||
]
|
||||
|
|
@ -512,9 +512,9 @@ async def _handle_tool_call(
|
|||
sys.exit(1)
|
||||
|
||||
tool = tool_map[tool_name]
|
||||
parsed_args = parse_tool_arguments(arguments, input_json, tool.inputSchema)
|
||||
parsed_args = parse_tool_arguments(arguments, input_json, tool.input_schema)
|
||||
|
||||
required = set(tool.inputSchema.get("required", []))
|
||||
required = set(tool.input_schema.get("required", []))
|
||||
provided = set(parsed_args.keys())
|
||||
missing = required - provided
|
||||
if missing:
|
||||
|
|
@ -553,7 +553,7 @@ async def _handle_resource(
|
|||
data.append(
|
||||
{
|
||||
"uri": str(block.uri),
|
||||
"mimeType": block.mimeType,
|
||||
"mimeType": block.mime_type,
|
||||
"text": block.text,
|
||||
}
|
||||
)
|
||||
|
|
@ -561,7 +561,7 @@ async def _handle_resource(
|
|||
data.append(
|
||||
{
|
||||
"uri": str(block.uri),
|
||||
"mimeType": block.mimeType,
|
||||
"mimeType": block.mime_type,
|
||||
"blob": block.blob,
|
||||
}
|
||||
)
|
||||
|
|
@ -573,7 +573,7 @@ async def _handle_resource(
|
|||
console.print(_sanitize_untrusted_text(block.text))
|
||||
elif isinstance(block, mcp_types.BlobResourceContents):
|
||||
size = len(block.blob) * 3 // 4
|
||||
console.print(f"[dim][Blob: {block.mimeType}, ~{size} bytes][/dim]")
|
||||
console.print(f"[dim][Blob: {block.mime_type}, ~{size} bytes][/dim]")
|
||||
|
||||
|
||||
async def _handle_prompt(
|
||||
|
|
@ -626,7 +626,7 @@ async def _handle_prompt(
|
|||
elif isinstance(msg.content, mcp_types.ImageContent):
|
||||
size = len(msg.content.data) * 3 // 4
|
||||
console.print(
|
||||
f" [dim][Image: {msg.content.mimeType}, ~{size} bytes][/dim]"
|
||||
f" [dim][Image: {msg.content.mime_type}, ~{size} bytes][/dim]"
|
||||
)
|
||||
else:
|
||||
console.print(f" {_sanitize_untrusted_text(str(msg.content))}")
|
||||
|
|
@ -718,7 +718,7 @@ async def list_command(
|
|||
"uri": str(r.uri),
|
||||
"name": r.name,
|
||||
"description": r.description,
|
||||
"mimeType": r.mimeType,
|
||||
"mimeType": r.mime_type,
|
||||
}
|
||||
for r in res
|
||||
]
|
||||
|
|
@ -749,9 +749,9 @@ async def list_command(
|
|||
f" {_sanitize_untrusted_text(tool.description)}"
|
||||
)
|
||||
if input_schema:
|
||||
_print_schema("Input", tool.inputSchema)
|
||||
if output_schema and tool.outputSchema:
|
||||
_print_schema("Output", tool.outputSchema)
|
||||
_print_schema("Input", tool.input_schema)
|
||||
if output_schema and tool.output_schema:
|
||||
_print_schema("Output", tool.output_schema)
|
||||
console.print()
|
||||
|
||||
if resources:
|
||||
|
|
|
|||
|
|
@ -165,7 +165,7 @@ def _to_python_identifier(name: str) -> str:
|
|||
|
||||
def _tool_function_source(tool: mcp_types.Tool) -> str:
|
||||
"""Generate the source for a single ``@call_tool_app.command`` function."""
|
||||
schema = tool.inputSchema
|
||||
schema = tool.input_schema
|
||||
properties: dict[str, Any] = schema.get("properties", {})
|
||||
required = set(schema.get("required", []))
|
||||
|
||||
|
|
@ -361,10 +361,10 @@ def generate_cli_script(
|
|||
console.print(block.text)
|
||||
elif isinstance(block, mcp_types.ImageContent):
|
||||
size = len(block.data) * 3 // 4
|
||||
console.print(f"[dim][Image: {block.mimeType}, ~{size} bytes][/dim]")
|
||||
console.print(f"[dim][Image: {block.mime_type}, ~{size} bytes][/dim]")
|
||||
elif isinstance(block, mcp_types.AudioContent):
|
||||
size = len(block.data) * 3 // 4
|
||||
console.print(f"[dim][Audio: {block.mimeType}, ~{size} bytes][/dim]")
|
||||
console.print(f"[dim][Audio: {block.mime_type}, ~{size} bytes][/dim]")
|
||||
|
||||
|
||||
async def _call_tool(tool_name: str, arguments: dict) -> None:
|
||||
|
|
@ -401,8 +401,8 @@ def generate_cli_script(
|
|||
return
|
||||
for tool in tools:
|
||||
sig_parts = []
|
||||
props = tool.inputSchema.get("properties", {})
|
||||
required = set(tool.inputSchema.get("required", []))
|
||||
props = tool.input_schema.get("properties", {})
|
||||
required = set(tool.input_schema.get("required", []))
|
||||
for pname, pschema in props.items():
|
||||
ptype = pschema.get("type", "string")
|
||||
if pname in required:
|
||||
|
|
@ -443,7 +443,7 @@ def generate_cli_script(
|
|||
console.print(block.text)
|
||||
elif isinstance(block, mcp_types.BlobResourceContents):
|
||||
size = len(block.blob) * 3 // 4
|
||||
console.print(f"[dim][Blob: {block.mimeType}, ~{size} bytes][/dim]")
|
||||
console.print(f"[dim][Blob: {block.mime_type}, ~{size} bytes][/dim]")
|
||||
|
||||
|
||||
@app.command
|
||||
|
|
@ -487,7 +487,7 @@ def generate_cli_script(
|
|||
console.print(f" {msg.content.text}")
|
||||
elif isinstance(msg.content, mcp_types.ImageContent):
|
||||
size = len(msg.content.data) * 3 // 4
|
||||
console.print(f" [dim][Image: {msg.content.mimeType}, ~{size} bytes][/dim]")
|
||||
console.print(f" [dim][Image: {msg.content.mime_type}, ~{size} bytes][/dim]")
|
||||
else:
|
||||
console.print(f" {msg.content}")
|
||||
console.print()""")
|
||||
|
|
@ -566,7 +566,7 @@ def _schema_type_label(prop_schema: dict[str, Any]) -> str:
|
|||
|
||||
def _tool_skill_section(tool: mcp_types.Tool, cli_filename: str) -> str:
|
||||
"""Generate a SKILL.md section for a single tool."""
|
||||
schema = tool.inputSchema
|
||||
schema = tool.input_schema
|
||||
properties: dict[str, Any] = schema.get("properties", {})
|
||||
required = set(schema.get("required", []))
|
||||
|
||||
|
|
|
|||
|
|
@ -511,7 +511,7 @@ class Client(
|
|||
client = Client(server, auto_initialize=False)
|
||||
async with client:
|
||||
result = await client.initialize()
|
||||
print(f"Server: {result.serverInfo.name}")
|
||||
print(f"Server: {result.server_info.name}")
|
||||
print(f"Instructions: {result.instructions}")
|
||||
```
|
||||
"""
|
||||
|
|
@ -782,7 +782,7 @@ class Client(
|
|||
Updates Task object's cache and triggers events/callbacks.
|
||||
"""
|
||||
# Extract task ID from notification params
|
||||
task_id = notification.params.taskId
|
||||
task_id = notification.params.task_id
|
||||
if not task_id:
|
||||
return
|
||||
|
||||
|
|
|
|||
|
|
@ -45,10 +45,10 @@ def create_elicitation_callback(
|
|||
try:
|
||||
# requestedSchema only exists on ElicitRequestFormParams, not ElicitRequestURLParams
|
||||
if isinstance(params, ElicitRequestFormParams):
|
||||
if params.requestedSchema == {"type": "object", "properties": {}}:
|
||||
if params.requested_schema == {"type": "object", "properties": {}}:
|
||||
response_type = None
|
||||
else:
|
||||
response_type = json_schema_to_type(params.requestedSchema)
|
||||
response_type = json_schema_to_type(params.requested_schema)
|
||||
else:
|
||||
# URL-based elicitation doesn't have a schema
|
||||
response_type = None
|
||||
|
|
@ -65,7 +65,7 @@ def create_elicitation_callback(
|
|||
# (single "value" property). This lets handlers return T directly
|
||||
# for ctx.elicit("msg", str/int/float/bool).
|
||||
if isinstance(params, ElicitRequestFormParams) and set(
|
||||
params.requestedSchema.get("properties", {}).keys()
|
||||
params.requested_schema.get("properties", {}).keys()
|
||||
) == {"value"}:
|
||||
content = {"value": content}
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -89,16 +89,16 @@ class ClientPromptsMixin:
|
|||
for _ in range(max_pages):
|
||||
result = await self.list_prompts_mcp(cursor=cursor)
|
||||
all_prompts.extend(result.prompts)
|
||||
if not result.nextCursor:
|
||||
if not result.next_cursor:
|
||||
break
|
||||
if result.nextCursor in seen_cursors:
|
||||
if result.next_cursor in seen_cursors:
|
||||
logger.warning(
|
||||
f"[{self.name}] Server returned duplicate pagination cursor"
|
||||
f" {result.nextCursor!r} for list_prompts; stopping pagination"
|
||||
f" {result.next_cursor!r} for list_prompts; stopping pagination"
|
||||
)
|
||||
break
|
||||
seen_cursors.add(result.nextCursor)
|
||||
cursor = result.nextCursor
|
||||
seen_cursors.add(result.next_cursor)
|
||||
cursor = result.next_cursor
|
||||
else:
|
||||
raise RuntimeError(
|
||||
f"[{self.name}] Reached auto-pagination limit"
|
||||
|
|
@ -311,7 +311,7 @@ class ClientPromptsMixin:
|
|||
|
||||
if isinstance(raw_result, mcp_types.CreateTaskResult):
|
||||
# Task was accepted - extract task info from CreateTaskResult
|
||||
server_task_id = raw_result.task.taskId
|
||||
server_task_id = raw_result.task.task_id
|
||||
self._submitted_task_ids.add(server_task_id)
|
||||
|
||||
task_obj = PromptTask(
|
||||
|
|
|
|||
|
|
@ -88,16 +88,16 @@ class ClientResourcesMixin:
|
|||
for _ in range(max_pages):
|
||||
result = await self.list_resources_mcp(cursor=cursor)
|
||||
all_resources.extend(result.resources)
|
||||
if not result.nextCursor:
|
||||
if not result.next_cursor:
|
||||
break
|
||||
if result.nextCursor in seen_cursors:
|
||||
if result.next_cursor in seen_cursors:
|
||||
logger.warning(
|
||||
f"[{self.name}] Server returned duplicate pagination cursor"
|
||||
f" {result.nextCursor!r} for list_resources; stopping pagination"
|
||||
f" {result.next_cursor!r} for list_resources; stopping pagination"
|
||||
)
|
||||
break
|
||||
seen_cursors.add(result.nextCursor)
|
||||
cursor = result.nextCursor
|
||||
seen_cursors.add(result.next_cursor)
|
||||
cursor = result.next_cursor
|
||||
else:
|
||||
raise RuntimeError(
|
||||
f"[{self.name}] Reached auto-pagination limit"
|
||||
|
|
@ -164,18 +164,18 @@ class ClientResourcesMixin:
|
|||
|
||||
for _ in range(max_pages):
|
||||
result = await self.list_resource_templates_mcp(cursor=cursor)
|
||||
all_templates.extend(result.resourceTemplates)
|
||||
if not result.nextCursor:
|
||||
all_templates.extend(result.resource_templates)
|
||||
if not result.next_cursor:
|
||||
break
|
||||
if result.nextCursor in seen_cursors:
|
||||
if result.next_cursor in seen_cursors:
|
||||
logger.warning(
|
||||
f"[{self.name}] Server returned duplicate pagination cursor"
|
||||
f" {result.nextCursor!r} for list_resource_templates;"
|
||||
f" {result.next_cursor!r} for list_resource_templates;"
|
||||
" stopping pagination"
|
||||
)
|
||||
break
|
||||
seen_cursors.add(result.nextCursor)
|
||||
cursor = result.nextCursor
|
||||
seen_cursors.add(result.next_cursor)
|
||||
cursor = result.next_cursor
|
||||
else:
|
||||
raise RuntimeError(
|
||||
f"[{self.name}] Reached auto-pagination limit"
|
||||
|
|
@ -365,7 +365,7 @@ class ClientResourcesMixin:
|
|||
|
||||
if isinstance(raw_result, mcp_types.CreateTaskResult):
|
||||
# Task was accepted - extract task info from CreateTaskResult
|
||||
server_task_id = raw_result.task.taskId
|
||||
server_task_id = raw_result.task.task_id
|
||||
self._submitted_task_ids.add(server_task_id)
|
||||
|
||||
task_obj = ResourceTask(
|
||||
|
|
|
|||
|
|
@ -93,16 +93,16 @@ class ClientToolsMixin:
|
|||
for _ in range(max_pages):
|
||||
result = await self.list_tools_mcp(cursor=cursor)
|
||||
all_tools.extend(result.tools)
|
||||
if not result.nextCursor:
|
||||
if not result.next_cursor:
|
||||
break
|
||||
if result.nextCursor in seen_cursors:
|
||||
if result.next_cursor in seen_cursors:
|
||||
logger.warning(
|
||||
f"[{self.name}] Server returned duplicate pagination cursor"
|
||||
f" {result.nextCursor!r} for list_tools; stopping pagination"
|
||||
f" {result.next_cursor!r} for list_tools; stopping pagination"
|
||||
)
|
||||
break
|
||||
seen_cursors.add(result.nextCursor)
|
||||
cursor = result.nextCursor
|
||||
seen_cursors.add(result.next_cursor)
|
||||
cursor = result.next_cursor
|
||||
else:
|
||||
raise RuntimeError(
|
||||
f"[{self.name}] Reached auto-pagination limit"
|
||||
|
|
@ -170,7 +170,7 @@ class ClientToolsMixin:
|
|||
|
||||
# Reflect tool-level errors on the span so callers see ERROR
|
||||
# status even though the MCP protocol call itself succeeded.
|
||||
if result.isError and span.is_recording():
|
||||
if result.is_error and span.is_recording():
|
||||
span.set_attribute("error.type", "tool_error")
|
||||
description = ""
|
||||
if result.content and isinstance(
|
||||
|
|
@ -366,7 +366,7 @@ class ClientToolsMixin:
|
|||
|
||||
if isinstance(raw_result, mcp_types.CreateTaskResult):
|
||||
# Task was accepted - extract task info from CreateTaskResult
|
||||
server_task_id = raw_result.task.taskId
|
||||
server_task_id = raw_result.task.task_id
|
||||
self._submitted_task_ids.add(server_task_id)
|
||||
|
||||
task_obj = ToolTask(
|
||||
|
|
@ -418,13 +418,13 @@ async def _parse_call_tool_result(
|
|||
from fastmcp.client.client import CallToolResult
|
||||
|
||||
data = None
|
||||
if result.isError and raise_on_error:
|
||||
if result.is_error and raise_on_error:
|
||||
if result.content and isinstance(result.content[0], mcp_types.TextContent):
|
||||
msg = result.content[0].text
|
||||
else:
|
||||
msg = f"Tool '{name}' returned an error"
|
||||
raise ToolError(msg)
|
||||
elif result.structuredContent and not result.isError:
|
||||
elif result.structured_content and not result.is_error:
|
||||
try:
|
||||
raw_fastmcp_meta = (result.meta or {}).get("fastmcp")
|
||||
fastmcp_meta = (
|
||||
|
|
@ -441,15 +441,15 @@ async def _parse_call_tool_result(
|
|||
|
||||
if wrap_from_meta:
|
||||
# Meta tells us the result is wrapped — unwrap and validate.
|
||||
structured_content = result.structuredContent.get("result")
|
||||
structured_content = result.structured_content.get("result")
|
||||
elif name in tool_output_schemas:
|
||||
output_schema = tool_output_schemas.get(name)
|
||||
if output_schema and output_schema.get("x-fastmcp-wrap-result"):
|
||||
structured_content = result.structuredContent.get("result")
|
||||
structured_content = result.structured_content.get("result")
|
||||
else:
|
||||
structured_content = result.structuredContent
|
||||
structured_content = result.structured_content
|
||||
else:
|
||||
structured_content = result.structuredContent
|
||||
structured_content = result.structured_content
|
||||
|
||||
# Type-validate through the schema if available.
|
||||
output_schema = tool_output_schemas.get(name)
|
||||
|
|
@ -470,8 +470,8 @@ async def _parse_call_tool_result(
|
|||
|
||||
return CallToolResult(
|
||||
content=result.content,
|
||||
structured_content=result.structuredContent,
|
||||
structured_content=result.structured_content,
|
||||
meta=result.meta,
|
||||
data=data,
|
||||
is_error=result.isError,
|
||||
is_error=result.is_error,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -54,16 +54,16 @@ _ANTHROPIC_IMAGE_MEDIA_TYPES = frozenset(
|
|||
|
||||
def _image_content_to_anthropic_block(content: ImageContent) -> ImageBlockParam:
|
||||
"""Convert MCP ImageContent to Anthropic ImageBlockParam."""
|
||||
if content.mimeType not in _ANTHROPIC_IMAGE_MEDIA_TYPES:
|
||||
if content.mime_type not in _ANTHROPIC_IMAGE_MEDIA_TYPES:
|
||||
raise ValueError(
|
||||
f"Unsupported image MIME type for Anthropic: {content.mimeType!r}. "
|
||||
f"Unsupported image MIME type for Anthropic: {content.mime_type!r}. "
|
||||
f"Supported types: {', '.join(sorted(_ANTHROPIC_IMAGE_MEDIA_TYPES))}"
|
||||
)
|
||||
return ImageBlockParam(
|
||||
type="image",
|
||||
source=Base64ImageSourceParam(
|
||||
type="base64",
|
||||
media_type=content.mimeType, # type: ignore[arg-type] # ty:ignore[invalid-argument-type]
|
||||
media_type=content.mime_type, # type: ignore[arg-type] # ty:ignore[invalid-argument-type]
|
||||
data=content.data,
|
||||
),
|
||||
)
|
||||
|
|
@ -103,7 +103,9 @@ class AnthropicSamplingHandler:
|
|||
messages=messages,
|
||||
)
|
||||
|
||||
model: ModelParam = self._select_model_from_preferences(params.modelPreferences)
|
||||
model: ModelParam = self._select_model_from_preferences(
|
||||
params.model_preferences
|
||||
)
|
||||
|
||||
# Convert MCP tools to Anthropic format
|
||||
anthropic_tools: list[ToolParam] | None = None
|
||||
|
|
@ -113,8 +115,8 @@ class AnthropicSamplingHandler:
|
|||
# Convert tool_choice to Anthropic format
|
||||
# Returns None if mode is "none", signaling tools should be omitted
|
||||
anthropic_tool_choice: ToolChoiceParam | None = None
|
||||
if params.toolChoice:
|
||||
converted = self._convert_tool_choice_to_anthropic(params.toolChoice)
|
||||
if params.tool_choice:
|
||||
converted = self._convert_tool_choice_to_anthropic(params.tool_choice)
|
||||
if converted is None:
|
||||
# tool_choice="none" means don't use tools
|
||||
anthropic_tools = None
|
||||
|
|
@ -126,14 +128,14 @@ class AnthropicSamplingHandler:
|
|||
kwargs: dict[str, Any] = {
|
||||
"model": model,
|
||||
"messages": anthropic_messages,
|
||||
"max_tokens": params.maxTokens,
|
||||
"max_tokens": params.max_tokens,
|
||||
}
|
||||
if params.systemPrompt is not None:
|
||||
kwargs["system"] = params.systemPrompt
|
||||
if params.system_prompt is not None:
|
||||
kwargs["system"] = params.system_prompt
|
||||
if params.temperature is not None:
|
||||
kwargs["temperature"] = params.temperature
|
||||
if params.stopSequences is not None:
|
||||
kwargs["stop_sequences"] = params.stopSequences
|
||||
if params.stop_sequences is not None:
|
||||
kwargs["stop_sequences"] = params.stop_sequences
|
||||
if anthropic_tools is not None:
|
||||
kwargs["tools"] = anthropic_tools
|
||||
if anthropic_tool_choice is not None:
|
||||
|
|
@ -229,9 +231,9 @@ class AnthropicSamplingHandler:
|
|||
content_blocks.append(
|
||||
ToolResultBlockParam(
|
||||
type="tool_result",
|
||||
tool_use_id=item.toolUseId,
|
||||
tool_use_id=item.tool_use_id,
|
||||
content=result_content,
|
||||
is_error=item.isError if item.isError else False,
|
||||
is_error=item.is_error if item.is_error else False,
|
||||
)
|
||||
)
|
||||
else:
|
||||
|
|
@ -285,9 +287,11 @@ class AnthropicSamplingHandler:
|
|||
content=[
|
||||
ToolResultBlockParam(
|
||||
type="tool_result",
|
||||
tool_use_id=content.toolUseId,
|
||||
tool_use_id=content.tool_use_id,
|
||||
content=result_content_str,
|
||||
is_error=content.isError if content.isError else False,
|
||||
is_error=content.is_error
|
||||
if content.is_error
|
||||
else False,
|
||||
)
|
||||
],
|
||||
)
|
||||
|
|
@ -364,7 +368,7 @@ class AnthropicSamplingHandler:
|
|||
anthropic_tools: list[ToolParam] = []
|
||||
for tool in tools:
|
||||
# Build input_schema dict, ensuring required fields
|
||||
input_schema: dict[str, Any] = dict(tool.inputSchema)
|
||||
input_schema: dict[str, Any] = dict(tool.input_schema)
|
||||
if "type" not in input_schema:
|
||||
input_schema["type"] = "object"
|
||||
|
||||
|
|
|
|||
|
|
@ -100,10 +100,10 @@ class GoogleGenaiSamplingHandler:
|
|||
google_tools = [
|
||||
_convert_tool_to_google_genai(tool) for tool in params.tools
|
||||
]
|
||||
tool_config = _convert_tool_choice_to_google_genai(params.toolChoice)
|
||||
tool_config = _convert_tool_choice_to_google_genai(params.tool_choice)
|
||||
|
||||
# Select the model based on preferences
|
||||
selected_model = self._get_model(model_preferences=params.modelPreferences)
|
||||
selected_model = self._get_model(model_preferences=params.model_preferences)
|
||||
|
||||
# Configure thinking if a budget is specified
|
||||
thinking_config = (
|
||||
|
|
@ -117,10 +117,10 @@ class GoogleGenaiSamplingHandler:
|
|||
model=selected_model,
|
||||
contents=contents,
|
||||
config=GenerateContentConfig(
|
||||
system_instruction=params.systemPrompt,
|
||||
system_instruction=params.system_prompt,
|
||||
temperature=params.temperature,
|
||||
max_output_tokens=params.maxTokens,
|
||||
stop_sequences=params.stopSequences,
|
||||
max_output_tokens=params.max_tokens,
|
||||
stop_sequences=params.stop_sequences,
|
||||
thinking_config=thinking_config,
|
||||
tools=google_tools, # ty: ignore[invalid-argument-type]
|
||||
tool_config=tool_config,
|
||||
|
|
@ -150,7 +150,7 @@ def _convert_tool_to_google_genai(tool: MCPTool) -> GoogleTool:
|
|||
"""
|
||||
from fastmcp.utilities.json_schema import compress_schema
|
||||
|
||||
schema = compress_schema(tool.inputSchema, prune_titles=True)
|
||||
schema = compress_schema(tool.input_schema, prune_titles=True)
|
||||
return GoogleTool(
|
||||
function_declarations=[
|
||||
FunctionDeclaration(
|
||||
|
|
@ -207,7 +207,7 @@ def _sampling_content_to_google_genai_part(
|
|||
return Part(
|
||||
inline_data=Blob(
|
||||
data=base64.b64decode(content.data),
|
||||
mime_type=content.mimeType,
|
||||
mime_type=content.mime_type,
|
||||
)
|
||||
)
|
||||
|
||||
|
|
@ -215,7 +215,7 @@ def _sampling_content_to_google_genai_part(
|
|||
return Part(
|
||||
inline_data=Blob(
|
||||
data=base64.b64decode(content.data),
|
||||
mime_type=content.mimeType,
|
||||
mime_type=content.mime_type,
|
||||
)
|
||||
)
|
||||
|
||||
|
|
@ -249,7 +249,7 @@ def _sampling_content_to_google_genai_part(
|
|||
# Our IDs are formatted as "{function_name}_{uuid8}", so extract the name.
|
||||
# Note: This is a limitation of MCP's ToolResultContent which only carries
|
||||
# toolUseId, while Google's FunctionResponse requires the function name.
|
||||
tool_use_id = content.toolUseId
|
||||
tool_use_id = content.tool_use_id
|
||||
if "_" in tool_use_id:
|
||||
# Split and rejoin all but the last part (the UUID suffix)
|
||||
parts = tool_use_id.rsplit("_", 1)
|
||||
|
|
|
|||
|
|
@ -64,12 +64,12 @@ def _image_content_to_openai_part(
|
|||
content: ImageContent,
|
||||
) -> ChatCompletionContentPartImageParam:
|
||||
"""Convert MCP ImageContent to OpenAI image_url content part."""
|
||||
if content.mimeType not in _OPENAI_IMAGE_MEDIA_TYPES:
|
||||
if content.mime_type not in _OPENAI_IMAGE_MEDIA_TYPES:
|
||||
raise ValueError(
|
||||
f"Unsupported image MIME type for OpenAI: {content.mimeType!r}. "
|
||||
f"Unsupported image MIME type for OpenAI: {content.mime_type!r}. "
|
||||
f"Supported types: {', '.join(sorted(_OPENAI_IMAGE_MEDIA_TYPES))}"
|
||||
)
|
||||
data_url = f"data:{content.mimeType};base64,{content.data}"
|
||||
data_url = f"data:{content.mime_type};base64,{content.data}"
|
||||
return ChatCompletionContentPartImageParam(
|
||||
type="image_url",
|
||||
image_url={"url": data_url},
|
||||
|
|
@ -80,10 +80,10 @@ def _audio_content_to_openai_part(
|
|||
content: AudioContent,
|
||||
) -> ChatCompletionContentPartInputAudioParam:
|
||||
"""Convert MCP AudioContent to OpenAI input_audio content part."""
|
||||
audio_format = _OPENAI_AUDIO_FORMATS.get(content.mimeType)
|
||||
audio_format = _OPENAI_AUDIO_FORMATS.get(content.mime_type)
|
||||
if audio_format is None:
|
||||
raise ValueError(
|
||||
f"Unsupported audio MIME type for OpenAI: {content.mimeType!r}. "
|
||||
f"Unsupported audio MIME type for OpenAI: {content.mime_type!r}. "
|
||||
f"Supported types: {', '.join(sorted(_OPENAI_AUDIO_FORMATS))}"
|
||||
)
|
||||
return ChatCompletionContentPartInputAudioParam(
|
||||
|
|
@ -112,12 +112,12 @@ class OpenAISamplingHandler:
|
|||
) -> CreateMessageResult | CreateMessageResultWithTools:
|
||||
openai_messages: list[ChatCompletionMessageParam] = (
|
||||
self._convert_to_openai_messages(
|
||||
system_prompt=params.systemPrompt,
|
||||
system_prompt=params.system_prompt,
|
||||
messages=messages,
|
||||
)
|
||||
)
|
||||
|
||||
model: ChatModel = self._select_model_from_preferences(params.modelPreferences)
|
||||
model: ChatModel = self._select_model_from_preferences(params.model_preferences)
|
||||
|
||||
# Convert MCP tools to OpenAI format
|
||||
openai_tools: list[ChatCompletionToolParam] | None = None
|
||||
|
|
@ -126,8 +126,8 @@ class OpenAISamplingHandler:
|
|||
|
||||
# Convert tool_choice to OpenAI format
|
||||
openai_tool_choice: ChatCompletionToolChoiceOptionParam | None = None
|
||||
if params.toolChoice:
|
||||
openai_tool_choice = self._convert_tool_choice_to_openai(params.toolChoice)
|
||||
if params.tool_choice:
|
||||
openai_tool_choice = self._convert_tool_choice_to_openai(params.tool_choice)
|
||||
|
||||
# Build kwargs to avoid sentinel type compatibility issues across
|
||||
# openai SDK versions (NotGiven vs Omit)
|
||||
|
|
@ -135,12 +135,12 @@ class OpenAISamplingHandler:
|
|||
"model": model,
|
||||
"messages": openai_messages,
|
||||
}
|
||||
if params.maxTokens is not None:
|
||||
kwargs["max_completion_tokens"] = params.maxTokens
|
||||
if params.max_tokens is not None:
|
||||
kwargs["max_completion_tokens"] = params.max_tokens
|
||||
if params.temperature is not None:
|
||||
kwargs["temperature"] = params.temperature
|
||||
if params.stopSequences:
|
||||
kwargs["stop"] = params.stopSequences
|
||||
if params.stop_sequences:
|
||||
kwargs["stop"] = params.stop_sequences
|
||||
if openai_tools is not None:
|
||||
kwargs["tools"] = openai_tools
|
||||
if openai_tool_choice is not None:
|
||||
|
|
@ -240,7 +240,7 @@ class OpenAISamplingHandler:
|
|||
tool_messages.append(
|
||||
ChatCompletionToolMessageParam(
|
||||
role="tool",
|
||||
tool_call_id=item.toolUseId,
|
||||
tool_call_id=item.tool_use_id,
|
||||
content=content_text,
|
||||
)
|
||||
)
|
||||
|
|
@ -327,7 +327,7 @@ class OpenAISamplingHandler:
|
|||
openai_messages.append(
|
||||
ChatCompletionToolMessageParam(
|
||||
role="tool",
|
||||
tool_call_id=content.toolUseId,
|
||||
tool_call_id=content.tool_use_id,
|
||||
content="\n".join(result_texts),
|
||||
)
|
||||
)
|
||||
|
|
@ -417,7 +417,7 @@ class OpenAISamplingHandler:
|
|||
openai_tools: list[ChatCompletionToolParam] = []
|
||||
for tool in tools:
|
||||
# Build parameters dict, ensuring required fields
|
||||
parameters: dict[str, Any] = dict(tool.inputSchema)
|
||||
parameters: dict[str, Any] = dict(tool.input_schema)
|
||||
if "type" not in parameters:
|
||||
parameters["type"] = "object"
|
||||
|
||||
|
|
|
|||
|
|
@ -162,7 +162,7 @@ class Task(abc.ABC, Generic[TaskResultT]):
|
|||
>>> task = await client.call_tool("slow_operation", {}, task=True)
|
||||
>>>
|
||||
>>> def on_update(status: GetTaskResult):
|
||||
... print(f"Task {status.taskId} is now {status.status}")
|
||||
... print(f"Task {status.task_id} is now {status.status}")
|
||||
>>>
|
||||
>>> task.on_status_change(on_update)
|
||||
>>> result = await task # Callback fires when status changes
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ class CallToolRequestResult(CallToolResult):
|
|||
return cls(
|
||||
tool=tool,
|
||||
arguments=arguments,
|
||||
isError=result.isError,
|
||||
isError=result.is_error,
|
||||
content=result.content,
|
||||
)
|
||||
|
||||
|
|
@ -84,7 +84,7 @@ class BulkToolCaller(MCPMixin):
|
|||
|
||||
results.append(result)
|
||||
|
||||
if result.isError and not continue_on_error:
|
||||
if result.is_error and not continue_on_error:
|
||||
return results
|
||||
|
||||
return results
|
||||
|
|
@ -112,7 +112,7 @@ class BulkToolCaller(MCPMixin):
|
|||
|
||||
results.append(result)
|
||||
|
||||
if result.isError and not continue_on_error:
|
||||
if result.is_error and not continue_on_error:
|
||||
return results
|
||||
|
||||
return results
|
||||
|
|
@ -146,6 +146,6 @@ class BulkToolCaller(MCPMixin):
|
|||
return CallToolRequestResult(
|
||||
tool=tool,
|
||||
arguments=arguments,
|
||||
isError=result.isError,
|
||||
isError=result.is_error,
|
||||
content=result.content,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -340,10 +340,10 @@ class ResourceTemplate(FastMCPComponent):
|
|||
# Note: This creates a simple ResourceTemplate instance. For function-based templates,
|
||||
# the original function is lost, which is expected for remote templates.
|
||||
return cls(
|
||||
uri_template=mcp_template.uriTemplate,
|
||||
uri_template=mcp_template.uri_template,
|
||||
name=mcp_template.name,
|
||||
description=mcp_template.description,
|
||||
mime_type=mcp_template.mimeType or "text/plain",
|
||||
mime_type=mcp_template.mime_type or "text/plain",
|
||||
parameters={}, # Remote templates don't have local parameters
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -401,7 +401,7 @@ class Context:
|
|||
"""
|
||||
|
||||
progress_token = (
|
||||
self.request_context.meta.progressToken
|
||||
self.request_context.meta.progress_token
|
||||
if self.request_context and self.request_context.meta
|
||||
else None
|
||||
)
|
||||
|
|
@ -471,12 +471,12 @@ class Context:
|
|||
request = request_factory(cursor)
|
||||
result = await call_method(request)
|
||||
all_items.extend(extract_items(result))
|
||||
if not result.nextCursor:
|
||||
if not result.next_cursor:
|
||||
break
|
||||
if result.nextCursor in seen_cursors:
|
||||
if result.next_cursor in seen_cursors:
|
||||
break
|
||||
seen_cursors.add(result.nextCursor)
|
||||
cursor = result.nextCursor
|
||||
seen_cursors.add(result.next_cursor)
|
||||
cursor = result.next_cursor
|
||||
return all_items
|
||||
|
||||
async def list_resources(self) -> list[SDKResource]:
|
||||
|
|
|
|||
|
|
@ -152,9 +152,9 @@ class ProxyTool(Tool):
|
|||
name=mcp_tool.name,
|
||||
title=mcp_tool.title,
|
||||
description=mcp_tool.description,
|
||||
parameters=mcp_tool.inputSchema,
|
||||
parameters=mcp_tool.input_schema,
|
||||
annotations=mcp_tool.annotations,
|
||||
output_schema=mcp_tool.outputSchema,
|
||||
output_schema=mcp_tool.output_schema,
|
||||
icons=mcp_tool.icons,
|
||||
meta=mcp_tool.meta,
|
||||
tags=get_fastmcp_metadata(mcp_tool.meta).get("tags", []),
|
||||
|
|
@ -217,9 +217,9 @@ class ProxyTool(Tool):
|
|||
# Preserve backend's meta (includes task metadata for background tasks)
|
||||
return ToolResult(
|
||||
content=result.content,
|
||||
structured_content=result.structuredContent,
|
||||
structured_content=result.structured_content,
|
||||
meta=result.meta,
|
||||
is_error=result.isError,
|
||||
is_error=result.is_error,
|
||||
)
|
||||
|
||||
def get_span_attributes(self) -> dict[str, Any]:
|
||||
|
|
@ -277,7 +277,7 @@ class ProxyResource(Resource):
|
|||
name=mcp_resource.name,
|
||||
title=mcp_resource.title,
|
||||
description=mcp_resource.description,
|
||||
mime_type=mcp_resource.mimeType or "text/plain",
|
||||
mime_type=mcp_resource.mime_type or "text/plain",
|
||||
icons=mcp_resource.icons,
|
||||
meta=mcp_resource.meta,
|
||||
tags=get_fastmcp_metadata(mcp_resource.meta).get("tags", []),
|
||||
|
|
@ -312,7 +312,7 @@ class ProxyResource(Resource):
|
|||
contents.append(
|
||||
ResourceContent(
|
||||
content=item.text,
|
||||
mime_type=item.mimeType,
|
||||
mime_type=item.mime_type,
|
||||
meta=item.meta,
|
||||
)
|
||||
)
|
||||
|
|
@ -320,7 +320,7 @@ class ProxyResource(Resource):
|
|||
contents.append(
|
||||
ResourceContent(
|
||||
content=base64.b64decode(item.blob),
|
||||
mime_type=item.mimeType,
|
||||
mime_type=item.mime_type,
|
||||
meta=item.meta,
|
||||
)
|
||||
)
|
||||
|
|
@ -370,11 +370,11 @@ class ProxyTemplate(ResourceTemplate):
|
|||
|
||||
return cls(
|
||||
client_factory=client_factory,
|
||||
uri_template=mcp_template.uriTemplate,
|
||||
uri_template=mcp_template.uri_template,
|
||||
name=mcp_template.name,
|
||||
title=mcp_template.title,
|
||||
description=mcp_template.description,
|
||||
mime_type=mcp_template.mimeType or "text/plain",
|
||||
mime_type=mcp_template.mime_type or "text/plain",
|
||||
icons=mcp_template.icons,
|
||||
parameters={}, # Remote templates don't have local parameters
|
||||
meta=mcp_template.meta,
|
||||
|
|
@ -410,7 +410,7 @@ class ProxyTemplate(ResourceTemplate):
|
|||
contents.append(
|
||||
ResourceContent(
|
||||
content=item.text,
|
||||
mime_type=item.mimeType,
|
||||
mime_type=item.mime_type,
|
||||
meta=item.meta,
|
||||
)
|
||||
)
|
||||
|
|
@ -418,7 +418,7 @@ class ProxyTemplate(ResourceTemplate):
|
|||
contents.append(
|
||||
ResourceContent(
|
||||
content=base64.b64decode(item.blob),
|
||||
mime_type=item.mimeType,
|
||||
mime_type=item.mime_type,
|
||||
meta=item.meta,
|
||||
)
|
||||
)
|
||||
|
|
@ -435,7 +435,7 @@ class ProxyTemplate(ResourceTemplate):
|
|||
description=self.description,
|
||||
mime_type=result[
|
||||
0
|
||||
].mimeType, # Use first item's mimeType for backward compatibility
|
||||
].mime_type, # Use first item's mimeType for backward compatibility
|
||||
icons=self.icons,
|
||||
meta=self.meta,
|
||||
tags=get_fastmcp_metadata(self.meta).get("tags", []),
|
||||
|
|
@ -949,10 +949,10 @@ async def default_proxy_sampling_handler(
|
|||
ctx = get_context()
|
||||
result = await ctx.sample(
|
||||
list(messages),
|
||||
system_prompt=params.systemPrompt,
|
||||
system_prompt=params.system_prompt,
|
||||
temperature=params.temperature,
|
||||
max_tokens=params.maxTokens,
|
||||
model_preferences=params.modelPreferences,
|
||||
max_tokens=params.max_tokens,
|
||||
model_preferences=params.model_preferences,
|
||||
)
|
||||
content = mcp_types.TextContent(type="text", text=result.text or "")
|
||||
return mcp_types.CreateMessageResult(
|
||||
|
|
@ -973,7 +973,7 @@ async def default_proxy_elicitation_handler(
|
|||
ctx = get_context()
|
||||
# requestedSchema only exists on ElicitRequestFormParams, not ElicitRequestURLParams
|
||||
requested_schema = (
|
||||
params.requestedSchema
|
||||
params.requested_schema
|
||||
if isinstance(params, ElicitRequestFormParams)
|
||||
else {"type": "object", "properties": {}}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ class SampleStep:
|
|||
def is_tool_use(self) -> bool:
|
||||
"""True if the LLM is requesting tool execution."""
|
||||
if isinstance(self.response, CreateMessageResultWithTools):
|
||||
return self.response.stopReason == "toolUse"
|
||||
return self.response.stop_reason == "toolUse"
|
||||
return False
|
||||
|
||||
@property
|
||||
|
|
@ -575,7 +575,7 @@ async def sample_step_impl(
|
|||
# Check if this is a tool use response
|
||||
is_tool_use_response = (
|
||||
isinstance(response, CreateMessageResultWithTools)
|
||||
and response.stopReason == "toolUse"
|
||||
and response.stop_reason == "toolUse"
|
||||
)
|
||||
|
||||
# Always include the assistant response in history
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ class ToolResult(BaseModel):
|
|||
is_error: bool = Field(
|
||||
default=False,
|
||||
description="Whether this result represents a tool execution error. "
|
||||
"When True, it maps to CallToolResult.isError so the error is returned "
|
||||
"When True, it maps to CallToolResult.is_error so the error is returned "
|
||||
"to the client rather than raised.",
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ async def inspect_fastmcp_v2(mcp: FastMCP[Any]) -> FastMCPInfo:
|
|||
key=tool.key,
|
||||
name=tool.name or tool.key,
|
||||
description=tool.description,
|
||||
input_schema=mcp_tool.inputSchema if mcp_tool.inputSchema else {},
|
||||
input_schema=mcp_tool.input_schema if mcp_tool.input_schema else {},
|
||||
output_schema=tool.output_schema,
|
||||
annotations=tool.annotations.model_dump() if tool.annotations else None,
|
||||
tags=list(tool.tags) if tool.tags else None,
|
||||
|
|
@ -263,7 +263,7 @@ async def inspect_fastmcp_v1(mcp: FastMCP1x) -> FastMCPInfo:
|
|||
key=mcp_tool.name,
|
||||
name=mcp_tool.name,
|
||||
description=mcp_tool.description,
|
||||
input_schema=mcp_tool.inputSchema if mcp_tool.inputSchema else {},
|
||||
input_schema=mcp_tool.input_schema if mcp_tool.input_schema else {},
|
||||
output_schema=None, # v1 doesn't have output_schema
|
||||
annotations=None, # v1 doesn't have annotations
|
||||
tags=None, # v1 doesn't have tags
|
||||
|
|
@ -307,7 +307,7 @@ async def inspect_fastmcp_v1(mcp: FastMCP1x) -> FastMCPInfo:
|
|||
uri=str(mcp_resource.uri),
|
||||
name=mcp_resource.name,
|
||||
description=mcp_resource.description,
|
||||
mime_type=mcp_resource.mimeType,
|
||||
mime_type=mcp_resource.mime_type,
|
||||
annotations=None, # v1 doesn't have annotations
|
||||
tags=None, # v1 doesn't have tags
|
||||
title=None, # v1 doesn't have title
|
||||
|
|
@ -323,11 +323,11 @@ async def inspect_fastmcp_v1(mcp: FastMCP1x) -> FastMCPInfo:
|
|||
for mcp_template in mcp_templates:
|
||||
template_infos.append( # noqa: PERF401
|
||||
TemplateInfo(
|
||||
key=str(mcp_template.uriTemplate),
|
||||
uri_template=str(mcp_template.uriTemplate),
|
||||
key=str(mcp_template.uri_template),
|
||||
uri_template=str(mcp_template.uri_template),
|
||||
name=mcp_template.name,
|
||||
description=mcp_template.description,
|
||||
mime_type=mcp_template.mimeType,
|
||||
mime_type=mcp_template.mime_type,
|
||||
parameters=None, # v1 doesn't expose template parameters
|
||||
annotations=None, # v1 doesn't have annotations
|
||||
tags=None, # v1 doesn't have tags
|
||||
|
|
@ -348,14 +348,14 @@ async def inspect_fastmcp_v1(mcp: FastMCP1x) -> FastMCPInfo:
|
|||
}
|
||||
|
||||
# Extract server-level icons and website_url from serverInfo
|
||||
server_info = client.initialize_result.serverInfo
|
||||
server_info = client.initialize_result.server_info
|
||||
server_icons = (
|
||||
[icon.model_dump() for icon in server_info.icons]
|
||||
if hasattr(server_info, "icons") and server_info.icons
|
||||
else None
|
||||
)
|
||||
server_website_url = (
|
||||
server_info.websiteUrl if hasattr(server_info, "websiteUrl") else None
|
||||
server_info.website_url if hasattr(server_info, "websiteUrl") else None
|
||||
)
|
||||
|
||||
return FastMCPInfo(
|
||||
|
|
@ -443,7 +443,7 @@ async def format_mcp_info(mcp: FastMCP[Any] | FastMCP1x) -> bytes:
|
|||
templates_result = await client.list_resource_templates_mcp()
|
||||
|
||||
# Get server info from the initialize result
|
||||
server_info = client.initialize_result.serverInfo
|
||||
server_info = client.initialize_result.server_info
|
||||
|
||||
# Combine into MCP protocol structure with environment metadata
|
||||
result = {
|
||||
|
|
@ -456,7 +456,7 @@ async def format_mcp_info(mcp: FastMCP[Any] | FastMCP1x) -> bytes:
|
|||
"tools": tools_result.tools,
|
||||
"prompts": prompts_result.prompts,
|
||||
"resources": resources_result.resources,
|
||||
"resourceTemplates": templates_result.resourceTemplates,
|
||||
"resourceTemplates": templates_result.resource_templates,
|
||||
}
|
||||
|
||||
return pydantic_core.to_json(result, indent=2)
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ async def test_call_tool_mcp(fastmcp_server):
|
|||
# Check that we got the raw MCP CallToolResult object
|
||||
assert hasattr(result, "content")
|
||||
assert hasattr(result, "isError")
|
||||
assert result.isError is False
|
||||
assert result.is_error is False
|
||||
# The content is a list, so we'll check the first element
|
||||
# by properly accessing it
|
||||
content = result.content
|
||||
|
|
@ -356,7 +356,7 @@ async def test_initialize_called_once(fastmcp_server):
|
|||
async with client:
|
||||
# Verify that initialization succeeded by checking initialize_result
|
||||
assert client.initialize_result is not None
|
||||
assert client.initialize_result.serverInfo is not None
|
||||
assert client.initialize_result.server_info is not None
|
||||
|
||||
|
||||
async def test_initialize_result_connected(fastmcp_server):
|
||||
|
|
@ -372,8 +372,8 @@ async def test_initialize_result_connected(fastmcp_server):
|
|||
|
||||
# Verify the initialize result has expected properties
|
||||
assert hasattr(result, "serverInfo")
|
||||
assert result.serverInfo.name == "TestServer"
|
||||
assert result.serverInfo.version is not None
|
||||
assert result.server_info.name == "TestServer"
|
||||
assert result.server_info.version is not None
|
||||
|
||||
|
||||
async def test_initialize_result_disconnected(fastmcp_server):
|
||||
|
|
@ -401,8 +401,8 @@ async def test_server_info_custom_version():
|
|||
async with client:
|
||||
result = client.initialize_result
|
||||
assert result is not None
|
||||
assert result.serverInfo.name == "CustomVersionServer"
|
||||
assert result.serverInfo.version == "1.2.3"
|
||||
assert result.server_info.name == "CustomVersionServer"
|
||||
assert result.server_info.version == "1.2.3"
|
||||
|
||||
# Test without version (backward compatibility)
|
||||
server_without_version = FastMCP("DefaultVersionServer")
|
||||
|
|
@ -411,9 +411,9 @@ async def test_server_info_custom_version():
|
|||
async with client:
|
||||
result = client.initialize_result
|
||||
assert result is not None
|
||||
assert result.serverInfo.name == "DefaultVersionServer"
|
||||
assert result.server_info.name == "DefaultVersionServer"
|
||||
# Should fall back to FastMCP version
|
||||
assert result.serverInfo.version == fastmcp.__version__
|
||||
assert result.server_info.version == fastmcp.__version__
|
||||
|
||||
|
||||
class _DelayedConnectTransport(ClientTransport):
|
||||
|
|
@ -717,7 +717,7 @@ async def test_resource_template(fastmcp_server):
|
|||
|
||||
# Check that our template is available
|
||||
assert len(result) == 1
|
||||
assert "data://user/{user_id}" in result[0].uriTemplate
|
||||
assert "data://user/{user_id}" in result[0].uri_template
|
||||
|
||||
# Now use the template with a specific user_id
|
||||
uri = cast(AnyUrl, "data://user/123")
|
||||
|
|
@ -739,8 +739,8 @@ async def test_list_resource_templates_mcp(fastmcp_server):
|
|||
|
||||
# Check that we got the raw MCP ListResourceTemplatesResult object
|
||||
assert hasattr(result, "resourceTemplates")
|
||||
assert len(result.resourceTemplates) == 1
|
||||
assert "data://user/{user_id}" in result.resourceTemplates[0].uriTemplate
|
||||
assert len(result.resource_templates) == 1
|
||||
assert "data://user/{user_id}" in result.resource_templates[0].uri_template
|
||||
|
||||
|
||||
async def test_mcp_resource_generation(fastmcp_server):
|
||||
|
|
@ -772,7 +772,7 @@ async def test_mcp_template_generation(fastmcp_server):
|
|||
assert hasattr(template, "uriTemplate")
|
||||
assert hasattr(template, "name")
|
||||
assert hasattr(template, "description")
|
||||
assert "data://user/{user_id}" in template.uriTemplate
|
||||
assert "data://user/{user_id}" in template.uri_template
|
||||
|
||||
|
||||
async def test_template_access_via_client(fastmcp_server):
|
||||
|
|
@ -811,7 +811,7 @@ async def test_tagged_template_metadata(tagged_resources_server):
|
|||
template = templates[0]
|
||||
|
||||
# Verify template metadata is preserved
|
||||
assert "template://{id}" in template.uriTemplate
|
||||
assert "template://{id}" in template.uri_template
|
||||
assert template.description == "A tagged template"
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ class TestErrorHandling:
|
|||
|
||||
async with client:
|
||||
result = await client.call_tool_mcp("error_tool", {})
|
||||
assert result.isError
|
||||
assert result.is_error
|
||||
assert isinstance(result.content[0], TextContent)
|
||||
assert "test error" in result.content[0].text
|
||||
assert "abc" in result.content[0].text
|
||||
|
|
@ -43,7 +43,7 @@ class TestErrorHandling:
|
|||
|
||||
async with client:
|
||||
result = await client.call_tool_mcp("error_tool", {})
|
||||
assert result.isError
|
||||
assert result.is_error
|
||||
assert isinstance(result.content[0], TextContent)
|
||||
assert "test error" not in result.content[0].text
|
||||
assert "abc" not in result.content[0].text
|
||||
|
|
@ -57,7 +57,7 @@ class TestErrorHandling:
|
|||
|
||||
async with Client(transport=FastMCPTransport(mcp)) as client:
|
||||
result = await client.call_tool_mcp("validated_tool", {"x": "abc"})
|
||||
assert result.isError
|
||||
assert result.is_error
|
||||
# Pydantic validation error message should NOT be masked
|
||||
assert isinstance(result.content[0], TextContent)
|
||||
assert "Input should be a valid integer" in result.content[0].text
|
||||
|
|
@ -73,7 +73,7 @@ class TestErrorHandling:
|
|||
|
||||
async with client:
|
||||
result = await client.call_tool_mcp("custom_error_tool", {})
|
||||
assert result.isError
|
||||
assert result.is_error
|
||||
assert isinstance(result.content[0], TextContent)
|
||||
assert "test error" in result.content[0].text
|
||||
assert "abc" in result.content[0].text
|
||||
|
|
@ -283,7 +283,7 @@ class TestLogLevel:
|
|||
with caplog.at_level(logging.WARNING):
|
||||
result = await client.call_tool_mcp("custom_level_tool", {})
|
||||
|
||||
assert result.isError
|
||||
assert result.is_error
|
||||
assert isinstance(result.content[0], TextContent)
|
||||
assert "Missing required parameter" in result.content[0].text
|
||||
assert any(
|
||||
|
|
@ -307,7 +307,7 @@ class TestLogLevel:
|
|||
with caplog.at_level(logging.ERROR):
|
||||
result = await client.call_tool_mcp("regular_error_tool", {})
|
||||
|
||||
assert result.isError
|
||||
assert result.is_error
|
||||
assert isinstance(result.content[0], TextContent)
|
||||
assert "Something went wrong" in result.content[0].text
|
||||
assert any(
|
||||
|
|
@ -428,7 +428,7 @@ class TestLogLevel:
|
|||
)
|
||||
|
||||
assert len(results) == 1
|
||||
assert results[0].isError
|
||||
assert results[0].is_error
|
||||
assert "Expected sampling error" in results[0].content[0].text # type: ignore
|
||||
assert any(
|
||||
"Error calling sampling tool" in record.message
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ class TestInitialize:
|
|||
async with client:
|
||||
# Should be automatically initialized
|
||||
assert client.initialize_result is not None
|
||||
assert client.initialize_result.serverInfo.name == "TestServer"
|
||||
assert client.initialize_result.server_info.name == "TestServer"
|
||||
assert client.initialize_result.instructions is None
|
||||
|
||||
async def test_auto_initialize_explicit_true(self, fastmcp_server):
|
||||
|
|
@ -23,7 +23,7 @@ class TestInitialize:
|
|||
|
||||
async with client:
|
||||
assert client.initialize_result is not None
|
||||
assert client.initialize_result.serverInfo.name == "TestServer"
|
||||
assert client.initialize_result.server_info.name == "TestServer"
|
||||
|
||||
async def test_auto_initialize_false(self, fastmcp_server):
|
||||
"""Test that auto_initialize=False prevents automatic initialization."""
|
||||
|
|
@ -42,7 +42,7 @@ class TestInitialize:
|
|||
result = await client.initialize()
|
||||
|
||||
assert result is not None
|
||||
assert result.serverInfo.name == "TestServer"
|
||||
assert result.server_info.name == "TestServer"
|
||||
assert client.initialize_result is result
|
||||
|
||||
async def test_initialize_idempotent(self, fastmcp_server):
|
||||
|
|
@ -90,7 +90,7 @@ class TestInitialize:
|
|||
# Access via property
|
||||
result = client.initialize_result
|
||||
assert result is not None
|
||||
assert result.serverInfo.name == "TestServer"
|
||||
assert result.server_info.name == "TestServer"
|
||||
|
||||
# Call method - should return cached
|
||||
result2 = await client.initialize()
|
||||
|
|
|
|||
|
|
@ -252,7 +252,7 @@ def test_message_to_result_with_tools():
|
|||
|
||||
assert result.role == "assistant"
|
||||
assert result.model == "claude-3-5-sonnet-20241022"
|
||||
assert result.stopReason == "toolUse"
|
||||
assert result.stop_reason == "toolUse"
|
||||
content = result.content_as_list
|
||||
assert len(content) == 2
|
||||
assert content[0] == TextContent(type="text", text="I'll help you with that.")
|
||||
|
|
|
|||
|
|
@ -372,7 +372,7 @@ def test_response_to_result_with_tools_text_only():
|
|||
|
||||
assert result.role == "assistant"
|
||||
assert result.model == "gemini-2.0-flash"
|
||||
assert result.stopReason == "endTurn"
|
||||
assert result.stop_reason == "endTurn"
|
||||
assert isinstance(result.content, list)
|
||||
assert len(result.content) == 1
|
||||
assert result.content[0].type == "text"
|
||||
|
|
@ -394,7 +394,7 @@ def test_response_to_result_with_tools_function_call():
|
|||
|
||||
result = _response_to_result_with_tools(mock_response, model="gemini-2.0-flash")
|
||||
|
||||
assert result.stopReason == "toolUse"
|
||||
assert result.stop_reason == "toolUse"
|
||||
assert isinstance(result.content, list)
|
||||
assert len(result.content) == 1
|
||||
tool_use = result.content[0]
|
||||
|
|
@ -421,7 +421,7 @@ def test_response_to_result_with_tools_mixed_content():
|
|||
|
||||
result = _response_to_result_with_tools(mock_response, model="gemini-2.0-flash")
|
||||
|
||||
assert result.stopReason == "toolUse"
|
||||
assert result.stop_reason == "toolUse"
|
||||
assert isinstance(result.content, list)
|
||||
assert len(result.content) == 2
|
||||
text_content = result.content[0]
|
||||
|
|
@ -576,7 +576,7 @@ def test_normal_response_text_and_function_call():
|
|||
assert isinstance(result.content[1], ToolUseContent) # ty: ignore[not-subscriptable]
|
||||
assert result.content[1].name == "lookup" # ty: ignore[not-subscriptable]
|
||||
assert result.content[1].input == {"q": "test"} # ty: ignore[not-subscriptable]
|
||||
assert result.stopReason == "toolUse"
|
||||
assert result.stop_reason == "toolUse"
|
||||
|
||||
|
||||
def test_thought_with_function_call_keeps_function_call():
|
||||
|
|
@ -598,4 +598,4 @@ def test_thought_with_function_call_keeps_function_call():
|
|||
assert len(result.content) == 1 # ty: ignore[invalid-argument-type]
|
||||
assert isinstance(result.content[0], ToolUseContent) # ty: ignore[not-subscriptable]
|
||||
assert result.content[0].name == "get_weather" # ty: ignore[not-subscriptable]
|
||||
assert result.stopReason == "toolUse"
|
||||
assert result.stop_reason == "toolUse"
|
||||
|
|
|
|||
|
|
@ -206,7 +206,7 @@ async def test_notification_with_failed_task(task_notification_server):
|
|||
status = await task.status()
|
||||
assert status.status == "failed"
|
||||
assert (
|
||||
status.statusMessage is not None
|
||||
status.status_message is not None
|
||||
) # Error details in statusMessage per spec
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ async def test_tool_task_status_and_wait(tool_task_server):
|
|||
task = await client.call_tool("echo", {"message": "test"}, task=True)
|
||||
|
||||
status = await task.status()
|
||||
assert status.taskId == task.task_id
|
||||
assert status.task_id == task.task_id
|
||||
assert status.status in ["working", "completed"]
|
||||
|
||||
# Wait for completion
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ async def test_elicitation_handler_parameters():
|
|||
|
||||
assert captured_params["message"] == "Test message"
|
||||
assert "ScalarElicitationType" in str(captured_params["response_type"])
|
||||
assert captured_params["params"].requestedSchema == {
|
||||
assert captured_params["params"].requested_schema == {
|
||||
"properties": {"value": {"title": "Value", "type": "integer"}},
|
||||
"required": ["value"],
|
||||
"title": "ScalarElicitationType",
|
||||
|
|
@ -135,7 +135,7 @@ async def test_elicitation_response_title_and_description_on_scalar():
|
|||
return "no answer"
|
||||
|
||||
async def elicitation_handler(message, response_type, params, ctx):
|
||||
captured_schema.update(params.requestedSchema)
|
||||
captured_schema.update(params.requested_schema)
|
||||
return ElicitResult(action="accept", content={"value": True})
|
||||
|
||||
async with Client(mcp, elicitation_handler=elicitation_handler) as client:
|
||||
|
|
@ -164,7 +164,7 @@ async def test_elicitation_response_title_on_dict_shorthand():
|
|||
return "ok" if isinstance(result, AcceptedElicitation) else "none"
|
||||
|
||||
async def elicitation_handler(message, response_type, params, ctx):
|
||||
captured_schema.update(params.requestedSchema)
|
||||
captured_schema.update(params.requested_schema)
|
||||
return ElicitResult(action="accept", content={"value": "low"})
|
||||
|
||||
async with Client(mcp, elicitation_handler=elicitation_handler) as client:
|
||||
|
|
@ -188,7 +188,7 @@ async def test_elicitation_response_title_on_list_shorthand():
|
|||
return "ok" if isinstance(result, AcceptedElicitation) else "none"
|
||||
|
||||
async def elicitation_handler(message, response_type, params, ctx):
|
||||
captured_schema.update(params.requestedSchema)
|
||||
captured_schema.update(params.requested_schema)
|
||||
return ElicitResult(action="accept", content={"value": "red"})
|
||||
|
||||
async with Client(mcp, elicitation_handler=elicitation_handler) as client:
|
||||
|
|
@ -302,7 +302,7 @@ class TestScalarResponseTypes:
|
|||
message, response_type, params: ElicitRequestParams, ctx
|
||||
):
|
||||
assert isinstance(params, ElicitRequestFormParams)
|
||||
assert params.requestedSchema == {"type": "object", "properties": {}}
|
||||
assert params.requested_schema == {"type": "object", "properties": {}}
|
||||
assert response_type is None
|
||||
return ElicitResult(action="accept")
|
||||
|
||||
|
|
@ -612,7 +612,7 @@ async def test_structured_response_type(
|
|||
)
|
||||
|
||||
# Verify the schema has the dataclass fields (available in params)
|
||||
schema = params.requestedSchema
|
||||
schema = params.requested_schema
|
||||
assert schema["type"] == "object"
|
||||
assert "name" in schema["properties"]
|
||||
assert "age" in schema["properties"]
|
||||
|
|
|
|||
|
|
@ -170,7 +170,7 @@ async def test_dict_based_titled_single_select():
|
|||
|
||||
async def elicitation_handler(message, response_type, params, ctx):
|
||||
# Verify schema follows SEP-1330 pattern with type: "string"
|
||||
schema = params.requestedSchema
|
||||
schema = params.requested_schema
|
||||
assert schema["type"] == "object"
|
||||
assert "value" in schema["properties"]
|
||||
value_schema = schema["properties"]["value"]
|
||||
|
|
@ -205,7 +205,7 @@ async def test_list_list_multi_select_untitled():
|
|||
|
||||
async def elicitation_handler(message, response_type, params, ctx):
|
||||
# Verify schema has array with enum pattern
|
||||
schema = params.requestedSchema
|
||||
schema = params.requested_schema
|
||||
assert schema["type"] == "object"
|
||||
assert "value" in schema["properties"]
|
||||
value_schema = schema["properties"]["value"]
|
||||
|
|
@ -243,7 +243,7 @@ async def test_list_dict_multi_select_titled():
|
|||
|
||||
async def elicitation_handler(message, response_type, params, ctx):
|
||||
# Verify schema has array with SEP-1330 compliant items (anyOf pattern)
|
||||
schema = params.requestedSchema
|
||||
schema = params.requested_schema
|
||||
assert schema["type"] == "object"
|
||||
assert "value" in schema["properties"]
|
||||
value_schema = schema["properties"]["value"]
|
||||
|
|
@ -310,7 +310,7 @@ async def test_list_enum_multi_select_direct():
|
|||
|
||||
async def elicitation_handler(message, response_type, params, ctx):
|
||||
# Verify schema has array with enum pattern
|
||||
schema = params.requestedSchema
|
||||
schema = params.requested_schema
|
||||
assert schema["type"] == "object"
|
||||
assert "value" in schema["properties"]
|
||||
value_schema = schema["properties"]["value"]
|
||||
|
|
|
|||
|
|
@ -85,8 +85,8 @@ async def test_sampling_with_system_prompt(fastmcp_server: FastMCP):
|
|||
def sampling_handler(
|
||||
messages: list[SamplingMessage], params: SamplingParams, ctx: RequestContext
|
||||
) -> str:
|
||||
assert params.systemPrompt is not None
|
||||
return params.systemPrompt
|
||||
assert params.system_prompt is not None
|
||||
return params.system_prompt
|
||||
|
||||
async with Client(fastmcp_server, sampling_handler=sampling_handler) as client:
|
||||
result = await client.call_tool(
|
||||
|
|
|
|||
|
|
@ -218,7 +218,7 @@ class TestSamplingResultType:
|
|||
tool_result = msg.content
|
||||
break
|
||||
assert tool_result is not None
|
||||
assert tool_result.isError is True
|
||||
assert tool_result.is_error is True
|
||||
assert isinstance(tool_result.content[0], TextContent)
|
||||
error_text = tool_result.content[0].text
|
||||
assert "Validation error" in error_text
|
||||
|
|
|
|||
|
|
@ -201,7 +201,7 @@ class TestAutomaticToolLoop:
|
|||
tool_result = msg.content
|
||||
break
|
||||
assert tool_result is not None
|
||||
assert tool_result.isError is True
|
||||
assert tool_result.is_error is True
|
||||
# Content is list of TextContent objects
|
||||
assert isinstance(tool_result.content[0], TextContent)
|
||||
error_text = tool_result.content[0].text
|
||||
|
|
@ -275,7 +275,7 @@ class TestAutomaticToolLoop:
|
|||
tool_result = msg.content
|
||||
break
|
||||
assert tool_result is not None
|
||||
assert tool_result.isError is True
|
||||
assert tool_result.is_error is True
|
||||
# Content is list of TextContent objects
|
||||
assert isinstance(tool_result.content[0], TextContent)
|
||||
error_text = tool_result.content[0].text
|
||||
|
|
@ -682,8 +682,8 @@ class TestAutomaticToolLoop:
|
|||
tool_results = cast(list[ToolResultContent], tool_result_message.content)
|
||||
assert len(tool_results) == 2
|
||||
# One should be success, one should be error
|
||||
assert any(not r.isError for r in tool_results)
|
||||
assert any(r.isError for r in tool_results)
|
||||
assert any(not r.is_error for r in tool_results)
|
||||
assert any(r.is_error for r in tool_results)
|
||||
|
||||
async def test_concurrent_tool_result_order_preserved(self):
|
||||
"""Test that tool results maintain the same order as tool calls."""
|
||||
|
|
@ -761,9 +761,9 @@ class TestAutomaticToolLoop:
|
|||
tool_result_message = messages_received[1][-1]
|
||||
tool_results = cast(list[ToolResultContent], tool_result_message.content)
|
||||
assert len(tool_results) == 3
|
||||
assert tool_results[0].toolUseId == "call_1"
|
||||
assert tool_results[1].toolUseId == "call_2"
|
||||
assert tool_results[2].toolUseId == "call_3"
|
||||
assert tool_results[0].tool_use_id == "call_1"
|
||||
assert tool_results[1].tool_use_id == "call_2"
|
||||
assert tool_results[2].tool_use_id == "call_3"
|
||||
# Check values are correct
|
||||
result_texts = [cast(TextContent, r.content[0]).text for r in tool_results]
|
||||
assert result_texts == ["1", "2", "3"]
|
||||
|
|
|
|||
|
|
@ -555,7 +555,7 @@ async def test_import_conflict_resolution_templates():
|
|||
async with Client(main_app) as client:
|
||||
# The later imported server should win
|
||||
templates = await client.list_resource_templates()
|
||||
template_uris = [t.uriTemplate for t in templates]
|
||||
template_uris = [t.uri_template for t in templates]
|
||||
assert "users://{user_id}/profile" in template_uris
|
||||
assert (
|
||||
template_uris.count("users://{user_id}/profile") == 1
|
||||
|
|
|
|||
|
|
@ -64,8 +64,8 @@ class TestGithubMCPRemote:
|
|||
assert isinstance(tool, Tool)
|
||||
assert len(tool.name) > 0
|
||||
assert tool.description is not None and len(tool.description) > 0
|
||||
assert isinstance(tool.inputSchema, dict)
|
||||
assert len(tool.inputSchema) > 0
|
||||
assert isinstance(tool.input_schema, dict)
|
||||
assert len(tool.input_schema) > 0
|
||||
|
||||
async def test_list_resources(
|
||||
self, streamable_http_client: Client[StreamableHttpTransport]
|
||||
|
|
|
|||
|
|
@ -632,7 +632,7 @@ class TestMessage:
|
|||
msg = Message(img, role="user")
|
||||
assert isinstance(msg.content, ImageContent)
|
||||
assert msg.content.data == "base64data"
|
||||
assert msg.content.mimeType == "image/png"
|
||||
assert msg.content.mime_type == "image/png"
|
||||
|
||||
def test_message_passthrough_audio_content(self):
|
||||
"""Test Message passes through AudioContent without JSON serialization."""
|
||||
|
|
@ -642,7 +642,7 @@ class TestMessage:
|
|||
msg = Message(audio, role="user")
|
||||
assert isinstance(msg.content, AudioContent)
|
||||
assert msg.content.data == "base64audio"
|
||||
assert msg.content.mimeType == "audio/wav"
|
||||
assert msg.content.mime_type == "audio/wav"
|
||||
|
||||
def test_message_image_content_to_mcp_prompt_message(self):
|
||||
"""Test that ImageContent round-trips through to_mcp_prompt_message."""
|
||||
|
|
|
|||
|
|
@ -261,7 +261,7 @@ class TestResourceContentToMcp:
|
|||
|
||||
assert hasattr(mcp_content, "text")
|
||||
assert mcp_content.text == "hello world"
|
||||
assert mcp_content.mimeType == "text/html"
|
||||
assert mcp_content.mime_type == "text/html"
|
||||
assert mcp_content.meta == {"csp": "script-src 'self'"}
|
||||
|
||||
def test_binary_content_to_mcp(self):
|
||||
|
|
@ -275,18 +275,18 @@ class TestResourceContentToMcp:
|
|||
|
||||
assert hasattr(mcp_content, "blob")
|
||||
assert mcp_content.blob == "AAEC" # base64 of \x00\x01\x02
|
||||
assert mcp_content.mimeType == "application/octet-stream"
|
||||
assert mcp_content.mime_type == "application/octet-stream"
|
||||
assert mcp_content.meta == {"encoding": "raw"}
|
||||
|
||||
def test_default_mime_types(self):
|
||||
"""Test default mime types are applied correctly."""
|
||||
text_rc = ResourceContent(content="text")
|
||||
text_mcp = text_rc.to_mcp_resource_contents("resource://test")
|
||||
assert text_mcp.mimeType == "text/plain"
|
||||
assert text_mcp.mime_type == "text/plain"
|
||||
|
||||
binary_rc = ResourceContent(content=b"binary")
|
||||
binary_mcp = binary_rc.to_mcp_resource_contents("resource://test")
|
||||
assert binary_mcp.mimeType == "application/octet-stream"
|
||||
assert binary_mcp.mime_type == "application/octet-stream"
|
||||
|
||||
def test_none_meta(self):
|
||||
"""Test that None meta is handled correctly."""
|
||||
|
|
|
|||
|
|
@ -176,7 +176,7 @@ class TestResourceContent:
|
|||
mcp_content = content.to_mcp_resource_contents("resource://test")
|
||||
assert isinstance(mcp_content, mcp_types.TextResourceContents)
|
||||
assert mcp_content.text == "hello"
|
||||
assert mcp_content.mimeType == "text/plain"
|
||||
assert mcp_content.mime_type == "text/plain"
|
||||
assert str(mcp_content.uri) == "resource://test"
|
||||
assert mcp_content.meta == {"k": "v"}
|
||||
|
||||
|
|
@ -188,7 +188,7 @@ class TestResourceContent:
|
|||
mcp_content = content.to_mcp_resource_contents("resource://binary")
|
||||
assert isinstance(mcp_content, mcp_types.BlobResourceContents)
|
||||
assert mcp_content.blob == "AAEC" # base64 of \x00\x01\x02
|
||||
assert mcp_content.mimeType == "application/octet-stream"
|
||||
assert mcp_content.mime_type == "application/octet-stream"
|
||||
|
||||
|
||||
class TestResourceResult:
|
||||
|
|
|
|||
|
|
@ -165,7 +165,7 @@ class TestResourceDecorator:
|
|||
|
||||
async with Client(mcp) as client:
|
||||
templates = await client.list_resource_templates()
|
||||
assert any(t.uriTemplate == "users://{user_id}/profile" for t in templates)
|
||||
assert any(t.uri_template == "users://{user_id}/profile" for t in templates)
|
||||
|
||||
result = await client.read_resource("users://123/profile")
|
||||
assert "123" in str(result)
|
||||
|
|
|
|||
|
|
@ -521,7 +521,7 @@ class TestAuthMiddleware:
|
|||
result = await mcp._list_resource_templates_mcp(
|
||||
mcp_types.ListResourceTemplatesRequest()
|
||||
)
|
||||
assert [template.uriTemplate for template in result.resourceTemplates] == [
|
||||
assert [template.uri_template for template in result.resource_templates] == [
|
||||
"resource://allowed/{item}"
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ class TestDereferenceRefsMiddleware:
|
|||
async with Client(mcp) as client:
|
||||
tools = await client.list_tools()
|
||||
|
||||
schema = tools[0].inputSchema
|
||||
schema = tools[0].input_schema
|
||||
# $defs should be removed — everything inlined
|
||||
assert "$defs" not in schema
|
||||
# The Color enum should be inlined into the request property
|
||||
|
|
@ -49,7 +49,7 @@ class TestDereferenceRefsMiddleware:
|
|||
async with Client(mcp) as client:
|
||||
tools = await client.list_tools()
|
||||
|
||||
schema = tools[0].inputSchema
|
||||
schema = tools[0].input_schema
|
||||
# $defs should still be present
|
||||
assert "$defs" in schema
|
||||
|
||||
|
|
@ -64,7 +64,7 @@ class TestDereferenceRefsMiddleware:
|
|||
async with Client(mcp) as client:
|
||||
tools = await client.list_tools()
|
||||
|
||||
schema = tools[0].inputSchema
|
||||
schema = tools[0].input_schema
|
||||
assert "$defs" not in schema
|
||||
|
||||
async def test_does_not_mutate_original_tool(self):
|
||||
|
|
@ -100,9 +100,9 @@ class TestDereferenceRefsMiddleware:
|
|||
|
||||
tool = tools[0]
|
||||
# Both input and output schemas should be dereferenced
|
||||
assert "$defs" not in tool.inputSchema
|
||||
if tool.outputSchema is not None:
|
||||
assert "$defs" not in tool.outputSchema
|
||||
assert "$defs" not in tool.input_schema
|
||||
if tool.output_schema is not None:
|
||||
assert "$defs" not in tool.output_schema
|
||||
|
||||
async def test_resource_templates_dereferenced(self):
|
||||
"""Middleware dereferences resource template schemas."""
|
||||
|
|
@ -130,7 +130,7 @@ class TestDereferenceRefsMiddleware:
|
|||
async with Client(mcp) as client:
|
||||
tools = await client.list_tools()
|
||||
|
||||
schema = tools[0].inputSchema
|
||||
schema = tools[0].input_schema
|
||||
# Simple schema should not have $defs regardless
|
||||
assert "$defs" not in schema
|
||||
assert schema["properties"]["a"]["type"] == "integer"
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ class InitializationMiddleware(Middleware):
|
|||
if hasattr(context.message, "params") and hasattr(
|
||||
context.message.params, "clientInfo"
|
||||
):
|
||||
self.client_info = context.message.params.clientInfo
|
||||
self.client_info = context.message.params.client_info
|
||||
|
||||
# Store in instance for cross-request access
|
||||
# (session state is not available during on_initialize)
|
||||
|
|
@ -96,7 +96,7 @@ class ClientDetectionMiddleware(Middleware):
|
|||
if tool.annotations is None:
|
||||
tool.annotations = mt.ToolAnnotations()
|
||||
# Mark as read-only for test clients
|
||||
tool.annotations.readOnlyHint = True
|
||||
tool.annotations.read_only_hint = True
|
||||
self.tools_modified = True
|
||||
|
||||
return tools
|
||||
|
|
@ -173,7 +173,7 @@ async def test_client_detection_middleware():
|
|||
# Check that the tool has the modified annotation
|
||||
tool = tools[0]
|
||||
assert tool.annotations is not None
|
||||
assert tool.annotations.readOnlyHint is True
|
||||
assert tool.annotations.read_only_hint is True
|
||||
|
||||
|
||||
async def test_multiple_middleware_initialization():
|
||||
|
|
@ -293,8 +293,8 @@ async def test_middleware_can_access_initialize_result():
|
|||
assert isinstance(middleware.initialize_result, mt.InitializeResult)
|
||||
|
||||
# Verify the result contains expected server info
|
||||
assert middleware.initialize_result.serverInfo.name == "TestServer"
|
||||
assert middleware.initialize_result.protocolVersion is not None
|
||||
assert middleware.initialize_result.server_info.name == "TestServer"
|
||||
assert middleware.initialize_result.protocol_version is not None
|
||||
assert middleware.initialize_result.capabilities is not None
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -391,7 +391,7 @@ class TestMiddlewareHooks:
|
|||
templates = await client.list_resource_templates()
|
||||
|
||||
assert len(templates) == 1
|
||||
assert str(templates[0].uriTemplate) == "resource://public/{x}"
|
||||
assert str(templates[0].uri_template) == "resource://public/{x}"
|
||||
|
||||
async def test_list_prompts_filtering_middleware(self):
|
||||
"""Test that middleware can filter prompts."""
|
||||
|
|
|
|||
|
|
@ -216,8 +216,8 @@ class TestToolInjectionMiddleware:
|
|||
|
||||
multiply_tool = next(t for t in tools if t.name == "multiply")
|
||||
assert multiply_tool.description == "Multiply two numbers."
|
||||
assert "a" in multiply_tool.inputSchema["properties"]
|
||||
assert "b" in multiply_tool.inputSchema["properties"]
|
||||
assert "a" in multiply_tool.input_schema["properties"]
|
||||
assert "b" in multiply_tool.input_schema["properties"]
|
||||
|
||||
async def test_injected_tool_does_not_conflict_with_base_tool(
|
||||
self, base_server: FastMCP
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@ class TestToolReturnTypes:
|
|||
content = result.content[0]
|
||||
assert isinstance(content, ImageContent)
|
||||
assert content.type == "image"
|
||||
assert content.mimeType == "image/png"
|
||||
assert content.mime_type == "image/png"
|
||||
decoded = base64.b64decode(content.data)
|
||||
assert decoded == b"fake png data"
|
||||
|
||||
|
|
@ -142,7 +142,7 @@ class TestToolReturnTypes:
|
|||
content = result.content[0]
|
||||
assert isinstance(content, AudioContent)
|
||||
assert content.type == "audio"
|
||||
assert content.mimeType == "audio/wav"
|
||||
assert content.mime_type == "audio/wav"
|
||||
decoded = base64.b64decode(content.data)
|
||||
assert decoded == b"fake wav data"
|
||||
|
||||
|
|
@ -162,7 +162,7 @@ class TestToolReturnTypes:
|
|||
assert isinstance(content, EmbeddedResource)
|
||||
assert content.type == "resource"
|
||||
resource = content.resource
|
||||
assert resource.mimeType == "application/octet-stream"
|
||||
assert resource.mime_type == "application/octet-stream"
|
||||
assert hasattr(resource, "blob")
|
||||
blob_data = getattr(resource, "blob")
|
||||
decoded = base64.b64decode(blob_data)
|
||||
|
|
@ -179,12 +179,12 @@ class TestToolReturnTypes:
|
|||
assert isinstance(content1, TextContent)
|
||||
assert content1.text == "Hello"
|
||||
assert isinstance(content2, ImageContent)
|
||||
assert content2.mimeType == "application/octet-stream"
|
||||
assert content2.mime_type == "application/octet-stream"
|
||||
assert content2.data == "abc"
|
||||
assert isinstance(content3, EmbeddedResource)
|
||||
assert content3.type == "resource"
|
||||
resource = content3.resource
|
||||
assert resource.mimeType == "application/octet-stream"
|
||||
assert resource.mime_type == "application/octet-stream"
|
||||
assert hasattr(resource, "blob")
|
||||
blob_data = getattr(resource, "blob")
|
||||
decoded = base64.b64decode(blob_data)
|
||||
|
|
@ -208,7 +208,7 @@ class TestToolReturnTypes:
|
|||
assert content1.text == "text message"
|
||||
content2 = result.content[1]
|
||||
assert isinstance(content2, ImageContent)
|
||||
assert content2.mimeType == "image/png"
|
||||
assert content2.mime_type == "image/png"
|
||||
assert base64.b64decode(content2.data) == b"test image data"
|
||||
content3 = result.content[2]
|
||||
assert isinstance(content3, TextContent)
|
||||
|
|
@ -235,7 +235,7 @@ class TestToolReturnTypes:
|
|||
assert content1.text == "text message"
|
||||
content2 = result.content[1]
|
||||
assert isinstance(content2, AudioContent)
|
||||
assert content2.mimeType == "audio/wav"
|
||||
assert content2.mime_type == "audio/wav"
|
||||
assert base64.b64decode(content2.data) == b"test audio data"
|
||||
content3 = result.content[2]
|
||||
assert isinstance(content3, TextContent)
|
||||
|
|
@ -264,7 +264,7 @@ class TestToolReturnTypes:
|
|||
assert isinstance(content2, EmbeddedResource)
|
||||
assert content2.type == "resource"
|
||||
resource = content2.resource
|
||||
assert resource.mimeType == "application/octet-stream"
|
||||
assert resource.mime_type == "application/octet-stream"
|
||||
assert hasattr(resource, "blob")
|
||||
blob_data = getattr(resource, "blob")
|
||||
assert base64.b64decode(blob_data) == b"test file data"
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ class TestToolParameters:
|
|||
assert result.structured_content is None
|
||||
assert isinstance(result.content, list)
|
||||
assert isinstance(result.content[0], ImageContent)
|
||||
assert result.content[0].mimeType == "image/png"
|
||||
assert result.content[0].mime_type == "image/png"
|
||||
assert result.content[0].data == base64.b64encode(b"fake png data").decode()
|
||||
|
||||
async def test_tool_with_invalid_input(self):
|
||||
|
|
|
|||
|
|
@ -430,7 +430,7 @@ class TestOpenAPIComprehensive:
|
|||
collision_tool = next(
|
||||
tool for tool in tools if tool.name == "collision_test"
|
||||
)
|
||||
schema = collision_tool.inputSchema
|
||||
schema = collision_tool.input_schema
|
||||
properties = schema["properties"]
|
||||
|
||||
# Should have unique parameter names for colliding parameters
|
||||
|
|
@ -461,7 +461,7 @@ class TestOpenAPIComprehensive:
|
|||
search_tool = next(
|
||||
tool for tool in tools if tool.name == "search_users"
|
||||
)
|
||||
schema = search_tool.inputSchema
|
||||
schema = search_tool.input_schema
|
||||
properties = schema["properties"]
|
||||
|
||||
# Should have flattened deepObject parameters
|
||||
|
|
@ -681,7 +681,7 @@ class TestOpenAPIComprehensive:
|
|||
|
||||
# Find create_user tool which uses schema refs
|
||||
create_tool = next(tool for tool in tools if tool.name == "create_user")
|
||||
schema = create_tool.inputSchema
|
||||
schema = create_tool.input_schema
|
||||
properties = schema["properties"]
|
||||
|
||||
# Should have resolved User schema properties
|
||||
|
|
@ -702,7 +702,7 @@ class TestOpenAPIComprehensive:
|
|||
|
||||
# Check list_users tool - has optional query parameters
|
||||
list_tool = next(tool for tool in tools if tool.name == "list_users")
|
||||
schema = list_tool.inputSchema
|
||||
schema = list_tool.input_schema
|
||||
# Query parameters should be optional
|
||||
# (may not appear in required list)
|
||||
# This test just ensures the schema is well-formed
|
||||
|
|
@ -712,7 +712,7 @@ class TestOpenAPIComprehensive:
|
|||
search_tool = next(
|
||||
tool for tool in tools if tool.name == "search_users"
|
||||
)
|
||||
search_schema = search_tool.inputSchema
|
||||
search_schema = search_tool.input_schema
|
||||
# Should have some required parameters
|
||||
assert len(search_schema["properties"]) > 0
|
||||
|
||||
|
|
|
|||
|
|
@ -206,7 +206,7 @@ class TestDeepObjectStyle:
|
|||
assert surveys_tool is not None
|
||||
|
||||
# Check that deepObject parameters are included in schema
|
||||
params = surveys_tool.inputSchema
|
||||
params = surveys_tool.input_schema
|
||||
properties = params["properties"]
|
||||
|
||||
# Should have the deepObject parameters
|
||||
|
|
@ -236,7 +236,7 @@ class TestDeepObjectStyle:
|
|||
)
|
||||
|
||||
# Check that explode=true parameters are properly structured
|
||||
params = surveys_tool.inputSchema
|
||||
params = surveys_tool.input_schema
|
||||
properties = params["properties"]
|
||||
|
||||
# Target parameter with explode=true should allow individual property access
|
||||
|
|
@ -261,7 +261,7 @@ class TestDeepObjectStyle:
|
|||
)
|
||||
|
||||
# Check that explode=false parameters are handled
|
||||
params = surveys_tool.inputSchema
|
||||
params = surveys_tool.input_schema
|
||||
properties = params["properties"]
|
||||
|
||||
# Compact parameter with explode=false should still be present and valid
|
||||
|
|
@ -292,7 +292,7 @@ class TestDeepObjectStyle:
|
|||
assert prefs_tool is not None
|
||||
|
||||
# Check that nested object structure is preserved
|
||||
params = prefs_tool.inputSchema
|
||||
params = prefs_tool.input_schema
|
||||
properties = params["properties"]
|
||||
|
||||
# Should have path parameter
|
||||
|
|
@ -338,9 +338,9 @@ class TestDeepObjectStyle:
|
|||
|
||||
# All tools should have valid schemas
|
||||
for tool in tools:
|
||||
assert tool.inputSchema is not None
|
||||
assert tool.inputSchema["type"] == "object"
|
||||
assert "properties" in tool.inputSchema
|
||||
assert tool.input_schema is not None
|
||||
assert tool.input_schema["type"] == "object"
|
||||
assert "properties" in tool.input_schema
|
||||
|
||||
# Should have some properties
|
||||
assert len(tool.inputSchema["properties"]) > 0
|
||||
assert len(tool.input_schema["properties"]) > 0
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ class TestEndToEndFunctionality:
|
|||
assert tool.description
|
||||
|
||||
# Check schema structure
|
||||
schema = tool.inputSchema
|
||||
schema = tool.input_schema
|
||||
assert schema["type"] == "object"
|
||||
|
||||
properties = schema.get("properties", {})
|
||||
|
|
@ -141,7 +141,7 @@ class TestEndToEndFunctionality:
|
|||
assert len(tools) == 1
|
||||
|
||||
tool = tools[0]
|
||||
schema = tool.inputSchema
|
||||
schema = tool.input_schema
|
||||
|
||||
# Both should have collision-resolved parameters
|
||||
properties = schema.get("properties", {})
|
||||
|
|
|
|||
|
|
@ -163,7 +163,7 @@ class TestParameterHandling:
|
|||
assert search_tool is not None
|
||||
|
||||
# Check that parameters are included in the tool's input schema
|
||||
params = search_tool.inputSchema
|
||||
params = search_tool.input_schema
|
||||
assert params["type"] == "object"
|
||||
|
||||
properties = params["properties"]
|
||||
|
|
@ -211,7 +211,7 @@ class TestParameterHandling:
|
|||
assert user_post_tool is not None
|
||||
|
||||
# Check that path parameters are included
|
||||
params = user_post_tool.inputSchema
|
||||
params = user_post_tool.input_schema
|
||||
properties = params["properties"]
|
||||
|
||||
# Check that path parameters are present
|
||||
|
|
@ -316,7 +316,7 @@ class TestRequestBodyHandling:
|
|||
assert create_tool is not None
|
||||
|
||||
# Check that request body properties are included
|
||||
params = create_tool.inputSchema
|
||||
params = create_tool.input_schema
|
||||
properties = params["properties"]
|
||||
|
||||
# Check that request body properties are present
|
||||
|
|
@ -637,7 +637,7 @@ class TestResourceTemplateMimeType:
|
|||
async with Client(mcp) as mcp_client:
|
||||
templates = await mcp_client.list_resource_templates()
|
||||
assert len(templates) == 1
|
||||
assert templates[0].mimeType == "text/plain"
|
||||
assert templates[0].mime_type == "text/plain"
|
||||
|
||||
async def test_resource_template_html_mime_type(self, html_spec):
|
||||
"""Resource template should reflect text/html from OpenAPI spec."""
|
||||
|
|
@ -651,7 +651,7 @@ class TestResourceTemplateMimeType:
|
|||
async with Client(mcp) as mcp_client:
|
||||
templates = await mcp_client.list_resource_templates()
|
||||
assert len(templates) == 1
|
||||
assert templates[0].mimeType == "text/html"
|
||||
assert templates[0].mime_type == "text/html"
|
||||
|
||||
async def test_resource_template_defaults_json_mime_type(self):
|
||||
"""Resource template defaults to application/json for JSON responses."""
|
||||
|
|
@ -702,7 +702,7 @@ class TestResourceTemplateMimeType:
|
|||
async with Client(mcp) as mcp_client:
|
||||
templates = await mcp_client.list_resource_templates()
|
||||
assert len(templates) == 1
|
||||
assert templates[0].mimeType == "application/json"
|
||||
assert templates[0].mime_type == "application/json"
|
||||
|
||||
|
||||
class TestResourceMimeType:
|
||||
|
|
@ -741,7 +741,7 @@ class TestResourceMimeType:
|
|||
async with Client(mcp) as mcp_client:
|
||||
resources = await mcp_client.list_resources()
|
||||
assert len(resources) == 1
|
||||
assert resources[0].mimeType == "text/plain"
|
||||
assert resources[0].mime_type == "text/plain"
|
||||
|
||||
async def test_resource_mime_type_without_schema(self):
|
||||
"""Resource with media type but no schema still infers MIME type."""
|
||||
|
|
@ -774,7 +774,7 @@ class TestResourceMimeType:
|
|||
async with Client(mcp) as mcp_client:
|
||||
resources = await mcp_client.list_resources()
|
||||
assert len(resources) == 1
|
||||
assert resources[0].mimeType == "text/plain"
|
||||
assert resources[0].mime_type == "text/plain"
|
||||
|
||||
|
||||
class TestValidateOutput:
|
||||
|
|
@ -981,10 +981,10 @@ class TestValidateOutput:
|
|||
tools = await mcp_client.list_tools()
|
||||
get_user = next(t for t in tools if t.name == "get_user")
|
||||
# With validate_output=False, the outputSchema should be permissive
|
||||
assert get_user.outputSchema is not None
|
||||
assert get_user.outputSchema.get("additionalProperties") is True
|
||||
assert get_user.output_schema is not None
|
||||
assert get_user.output_schema.get("additionalProperties") is True
|
||||
# Should NOT have specific properties from the original schema
|
||||
assert "properties" not in get_user.outputSchema
|
||||
assert "properties" not in get_user.output_schema
|
||||
|
||||
|
||||
class TestRedactHeaders:
|
||||
|
|
|
|||
|
|
@ -146,7 +146,7 @@ class TestParameterCollisions:
|
|||
assert update_tool is not None
|
||||
|
||||
# Check that both path and body 'id' parameters are included
|
||||
params = update_tool.inputSchema
|
||||
params = update_tool.input_schema
|
||||
properties = params["properties"]
|
||||
|
||||
# Should have both path ID and body ID (with potential suffixing)
|
||||
|
|
@ -188,7 +188,7 @@ class TestParameterCollisions:
|
|||
assert search_tool is not None
|
||||
|
||||
# Check that both query and header 'query' parameters are handled
|
||||
params = search_tool.inputSchema
|
||||
params = search_tool.input_schema
|
||||
properties = params["properties"]
|
||||
|
||||
# Should handle the collision somehow (suffixing or other mechanism)
|
||||
|
|
@ -220,6 +220,6 @@ class TestParameterCollisions:
|
|||
|
||||
# Tools should have valid schemas
|
||||
for tool in tools:
|
||||
assert tool.inputSchema is not None
|
||||
assert tool.inputSchema["type"] == "object"
|
||||
assert "properties" in tool.inputSchema
|
||||
assert tool.input_schema is not None
|
||||
assert tool.input_schema["type"] == "object"
|
||||
assert "properties" in tool.input_schema
|
||||
|
|
|
|||
|
|
@ -348,7 +348,7 @@ class TestOpenAPIProviderBasicFunctionality:
|
|||
},
|
||||
"required": ["name", "active"],
|
||||
}
|
||||
assert tool.inputSchema == expected_input_schema
|
||||
assert tool.input_schema == expected_input_schema
|
||||
|
||||
expected_output_schema = {
|
||||
"type": "object",
|
||||
|
|
@ -360,4 +360,4 @@ class TestOpenAPIProviderBasicFunctionality:
|
|||
"required": ["id", "name", "active"],
|
||||
"title": "User",
|
||||
}
|
||||
assert tool.outputSchema == expected_output_schema
|
||||
assert tool.output_schema == expected_output_schema
|
||||
|
|
|
|||
|
|
@ -152,10 +152,10 @@ class TestProxyClient:
|
|||
content=TextContent(type="text", text="Hello, world!"),
|
||||
)
|
||||
]
|
||||
assert params.systemPrompt == "You love FastMCP"
|
||||
assert params.system_prompt == "You love FastMCP"
|
||||
assert params.temperature == 0.5
|
||||
assert params.maxTokens == 100
|
||||
assert params.modelPreferences == ModelPreferences(
|
||||
assert params.max_tokens == 100
|
||||
assert params.model_preferences == ModelPreferences(
|
||||
hints=[ModelHint(name="gpt-4o")]
|
||||
)
|
||||
return ""
|
||||
|
|
@ -189,7 +189,7 @@ class TestProxyClient:
|
|||
assert message == "What is your name?"
|
||||
assert "Person" in str(response_type)
|
||||
assert isinstance(params, ElicitRequestFormParams)
|
||||
assert params.requestedSchema == {
|
||||
assert params.requested_schema == {
|
||||
"title": "Person",
|
||||
"type": "object",
|
||||
"properties": {"name": {"title": "Name", "type": "string"}},
|
||||
|
|
@ -388,7 +388,7 @@ class TestProxyClient:
|
|||
):
|
||||
# Verify the schema is correct - acknowledge should have default=False, not be nullable
|
||||
assert isinstance(params, ElicitRequestFormParams)
|
||||
schema = params.requestedSchema
|
||||
schema = params.requested_schema
|
||||
assert schema["properties"]["acknowledge"]["type"] == "boolean"
|
||||
assert schema["properties"]["acknowledge"]["default"] is False
|
||||
|
||||
|
|
|
|||
|
|
@ -479,7 +479,7 @@ class TestTools:
|
|||
async with Client(proxy_server) as client:
|
||||
tools = await client.list_tools()
|
||||
greet_tool = next(t for t in tools if t.name == "greet")
|
||||
assert "extra" in greet_tool.inputSchema["properties"]
|
||||
assert "extra" in greet_tool.input_schema["properties"]
|
||||
|
||||
|
||||
class TestResources:
|
||||
|
|
@ -546,18 +546,18 @@ class TestResources:
|
|||
assert isinstance(original, TextResourceContents)
|
||||
assert isinstance(proxied, TextResourceContents)
|
||||
assert original.text == proxied.text, f"Content {i} text mismatch"
|
||||
assert original.mimeType == proxied.mimeType, (
|
||||
assert original.mime_type == proxied.mime_type, (
|
||||
f"Content {i} mimeType mismatch"
|
||||
)
|
||||
assert original.meta == proxied.meta, f"Content {i} meta mismatch"
|
||||
|
||||
# Verify the contents are what we expect
|
||||
assert original_result[0].text == "First item"
|
||||
assert original_result[0].mimeType == "text/plain"
|
||||
assert original_result[0].mime_type == "text/plain"
|
||||
assert original_result[1].text == '{"key": "value"}'
|
||||
assert original_result[1].mimeType == "application/json"
|
||||
assert original_result[1].mime_type == "application/json"
|
||||
assert original_result[2].text == "# Markdown\nContent"
|
||||
assert original_result[2].mimeType == "text/markdown"
|
||||
assert original_result[2].mime_type == "text/markdown"
|
||||
|
||||
async def test_read_resource_returns_none_if_not_found(self, proxy_server):
|
||||
with pytest.raises(
|
||||
|
|
@ -661,15 +661,15 @@ class TestResourceTemplates:
|
|||
assert isinstance(original, TextResourceContents)
|
||||
assert isinstance(proxied, TextResourceContents)
|
||||
assert original.text == proxied.text, f"Content {i} text mismatch"
|
||||
assert original.mimeType == proxied.mimeType, (
|
||||
assert original.mime_type == proxied.mime_type, (
|
||||
f"Content {i} mimeType mismatch"
|
||||
)
|
||||
|
||||
# Verify the contents are what we expect
|
||||
assert original_result[0].text == "Item test123 - First"
|
||||
assert original_result[0].mimeType == "text/plain"
|
||||
assert original_result[0].mime_type == "text/plain"
|
||||
assert original_result[1].text == '{"id": "test123", "status": "active"}'
|
||||
assert original_result[1].mimeType == "application/json"
|
||||
assert original_result[1].mime_type == "application/json"
|
||||
|
||||
async def test_proxy_can_overwrite_proxied_resource_template(self, proxy_server):
|
||||
"""
|
||||
|
|
@ -706,7 +706,7 @@ class TestResourceTemplates:
|
|||
async with Client(proxy_server) as client:
|
||||
templates = await client.list_resource_templates()
|
||||
user_template = next(
|
||||
t for t in templates if t.uriTemplate == "data://user/{user_id}"
|
||||
t for t in templates if t.uri_template == "data://user/{user_id}"
|
||||
)
|
||||
assert user_template.name == "overwritten_get_user"
|
||||
|
||||
|
|
@ -888,7 +888,7 @@ class TestPrompts:
|
|||
# Verify the image content is preserved as ImageContent, not JSON text
|
||||
assert isinstance(proxy_result.messages[1].content, mcp_types.ImageContent)
|
||||
assert proxy_result.messages[1].content.data == "iVBORw0KGgoAAAANSUhEUg=="
|
||||
assert proxy_result.messages[1].content.mimeType == "image/png"
|
||||
assert proxy_result.messages[1].content.mime_type == "image/png"
|
||||
|
||||
|
||||
async def test_proxy_handles_multiple_concurrent_tasks_correctly(
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ class TestSamplingToolSDKConversion:
|
|||
|
||||
assert sdk_tool.name == "search"
|
||||
assert sdk_tool.description == "Search the web."
|
||||
assert "query" in sdk_tool.inputSchema.get("properties", {})
|
||||
assert "query" in sdk_tool.input_schema.get("properties", {})
|
||||
|
||||
|
||||
class TestSamplingToolFromCallableTool:
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ async def test_progress_status_message_in_background_task():
|
|||
|
||||
# Verify statusMessage field is accessible and contains progress info
|
||||
# Should not raise AttributeError
|
||||
msg = status.statusMessage
|
||||
msg = status.status_message
|
||||
assert msg is None or msg.startswith("Step")
|
||||
|
||||
# Wait for completion
|
||||
|
|
|
|||
|
|
@ -185,7 +185,7 @@ class TestResourceTaskMetaDirectServerCall:
|
|||
# Read inner resource as background task
|
||||
result = await server.read_resource("data://inner", task_meta=TaskMeta())
|
||||
# Should get CreateTaskResult since we provided task_meta
|
||||
return f"Created task: {result.task.taskId}"
|
||||
return f"Created task: {result.task.task_id}"
|
||||
|
||||
async with Client(server) as client:
|
||||
result = await client.call_tool("outer_tool", {})
|
||||
|
|
@ -221,7 +221,7 @@ class TestResourceTaskMetaDirectServerCall:
|
|||
@server.tool
|
||||
async def outer_tool() -> str:
|
||||
result = await server.read_resource("item://99", task_meta=TaskMeta())
|
||||
return f"Created task: {result.task.taskId}"
|
||||
return f"Created task: {result.task.task_id}"
|
||||
|
||||
async with Client(server) as client:
|
||||
result = await client.call_tool("outer_tool", {})
|
||||
|
|
|
|||
|
|
@ -265,7 +265,7 @@ class TestPromptModeEnforcement:
|
|||
|
||||
|
||||
class TestToolExecutionMetadata:
|
||||
"""Test that ToolExecution.taskSupport is set correctly in tool metadata."""
|
||||
"""Test that ToolExecution.task_support is set correctly in tool metadata."""
|
||||
|
||||
async def test_optional_tool_exposes_task_support(self):
|
||||
"""Tools with task enabled should expose taskSupport in metadata."""
|
||||
|
|
@ -280,7 +280,7 @@ class TestToolExecutionMetadata:
|
|||
tool = next(t for t in tools if t.name == "my_tool")
|
||||
assert isinstance(tool, MCPTool)
|
||||
assert isinstance(tool.execution, ToolExecution)
|
||||
assert tool.execution.taskSupport == "optional"
|
||||
assert tool.execution.task_support == "optional"
|
||||
|
||||
async def test_required_tool_exposes_task_support(self):
|
||||
"""Tools with mode=required should expose taskSupport='required'."""
|
||||
|
|
@ -295,7 +295,7 @@ class TestToolExecutionMetadata:
|
|||
tool = next(t for t in tools if t.name == "my_tool")
|
||||
assert isinstance(tool, MCPTool)
|
||||
assert isinstance(tool.execution, ToolExecution)
|
||||
assert tool.execution.taskSupport == "required"
|
||||
assert tool.execution.task_support == "required"
|
||||
|
||||
async def test_forbidden_tool_has_no_execution(self):
|
||||
"""Tools with mode=forbidden should not expose execution metadata."""
|
||||
|
|
|
|||
|
|
@ -263,7 +263,7 @@ class TestTaskMetaDirectServerCall:
|
|||
"inner_tool", {"x": x}, task_meta=TaskMeta()
|
||||
)
|
||||
# Should get CreateTaskResult since we're in server context
|
||||
return f"Created task: {result.task.taskId}"
|
||||
return f"Created task: {result.task.task_id}"
|
||||
|
||||
async with Client(server) as client:
|
||||
# Call outer_tool which internally calls inner_tool with task_meta
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ async def test_tasks_get_includes_related_task_metadata(metadata_server: FastMCP
|
|||
|
||||
# GetTaskResult is returned from response with metadata
|
||||
# Verify the protocol included related-task metadata by checking the response worked
|
||||
assert status.taskId == task_id
|
||||
assert status.task_id == task_id
|
||||
assert status.status in ["working", "completed"]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ async def test_tasks_get_endpoint_returns_status(endpoint_server):
|
|||
|
||||
# Check status immediately - should be submitted or working
|
||||
status = await task.status()
|
||||
assert status.taskId == task.task_id
|
||||
assert status.task_id == task.task_id
|
||||
assert status.status in ["working", "completed"]
|
||||
|
||||
# Wait for completion
|
||||
|
|
@ -62,8 +62,8 @@ async def test_tasks_get_endpoint_includes_poll_interval(endpoint_server):
|
|||
task = await client.call_tool("quick_tool", {"value": 42}, task=True)
|
||||
|
||||
status = await task.status()
|
||||
assert status.pollInterval is not None
|
||||
assert isinstance(status.pollInterval, int)
|
||||
assert status.poll_interval is not None
|
||||
assert isinstance(status.poll_interval, int)
|
||||
|
||||
|
||||
async def test_tasks_result_endpoint_returns_result_when_completed(endpoint_server):
|
||||
|
|
|
|||
|
|
@ -553,7 +553,7 @@ class TestMountedTaskMetadata:
|
|||
"""Test task metadata exposure for mounted tools."""
|
||||
|
||||
async def test_mounted_tool_list_preserves_task_support_metadata(self):
|
||||
"""Mounted tools should preserve execution.taskSupport in tools/list."""
|
||||
"""Mounted tools should preserve execution.task_support in tools/list."""
|
||||
child = FastMCP("child")
|
||||
|
||||
@child.tool(task=True)
|
||||
|
|
@ -574,11 +574,11 @@ class TestMountedTaskMetadata:
|
|||
|
||||
assert child_mcp_tool.execution is not None
|
||||
assert parent_mcp_tool.execution is not None
|
||||
assert child_mcp_tool.execution.taskSupport == "optional"
|
||||
assert parent_mcp_tool.execution.taskSupport == "optional"
|
||||
assert child_mcp_tool.execution.task_support == "optional"
|
||||
assert parent_mcp_tool.execution.task_support == "optional"
|
||||
|
||||
async def test_proxy_tool_preserves_execution_metadata(self):
|
||||
"""ProxyTool.from_mcp_tool should propagate execution.taskSupport (#3569)."""
|
||||
"""ProxyTool.from_mcp_tool should propagate execution.task_support (#3569)."""
|
||||
mcp_tool = MCPTool(
|
||||
name="remote_task_tool",
|
||||
description="A remote tool that supports tasks",
|
||||
|
|
@ -590,7 +590,7 @@ class TestMountedTaskMetadata:
|
|||
result = proxy.to_mcp_tool(name=proxy.name)
|
||||
|
||||
assert result.execution is not None
|
||||
assert result.execution.taskSupport == "optional"
|
||||
assert result.execution.task_support == "optional"
|
||||
|
||||
|
||||
class TestMountedTaskConfigModes:
|
||||
|
|
@ -928,7 +928,7 @@ class TestMountedTasksWithTaskMetaParameter:
|
|||
result = await parent.call_tool(
|
||||
"child_add", {"a": 2, "b": 3}, task_meta=TaskMeta(ttl=300)
|
||||
)
|
||||
return f"task:{result.task.taskId}"
|
||||
return f"task:{result.task.task_id}"
|
||||
|
||||
async with Client(parent) as client:
|
||||
result = await client.call_tool("outer", {})
|
||||
|
|
@ -952,7 +952,7 @@ class TestMountedTasksWithTaskMetaParameter:
|
|||
result = await parent.read_resource(
|
||||
"data://child/info", task_meta=TaskMeta(ttl=300)
|
||||
)
|
||||
return f"task:{result.task.taskId}"
|
||||
return f"task:{result.task.task_id}"
|
||||
|
||||
async with Client(parent) as client:
|
||||
result = await client.call_tool("outer", {})
|
||||
|
|
@ -976,7 +976,7 @@ class TestMountedTasksWithTaskMetaParameter:
|
|||
result = await parent.read_resource(
|
||||
"item://child/42", task_meta=TaskMeta(ttl=300)
|
||||
)
|
||||
return f"task:{result.task.taskId}"
|
||||
return f"task:{result.task.task_id}"
|
||||
|
||||
async with Client(parent) as client:
|
||||
result = await client.call_tool("outer", {})
|
||||
|
|
@ -1003,7 +1003,7 @@ class TestMountedTasksWithTaskMetaParameter:
|
|||
result = await parent.call_tool(
|
||||
"c_gc_compute", {"n": 7}, task_meta=TaskMeta(ttl=300)
|
||||
)
|
||||
return f"task:{result.task.taskId}"
|
||||
return f"task:{result.task.task_id}"
|
||||
|
||||
async with Client(parent) as client:
|
||||
result = await client.call_tool("outer", {})
|
||||
|
|
@ -1030,7 +1030,7 @@ class TestMountedTasksWithTaskMetaParameter:
|
|||
result = await parent.read_resource(
|
||||
"doc://c/gc/readme", task_meta=TaskMeta(ttl=300)
|
||||
)
|
||||
return f"task:{result.task.taskId}"
|
||||
return f"task:{result.task.task_id}"
|
||||
|
||||
async with Client(parent) as client:
|
||||
result = await client.call_tool("outer", {})
|
||||
|
|
@ -1054,7 +1054,7 @@ class TestMountedTasksWithTaskMetaParameter:
|
|||
result = await parent.render_prompt(
|
||||
"child_greet", {"name": "World"}, task_meta=TaskMeta(ttl=300)
|
||||
)
|
||||
return f"task:{result.task.taskId}"
|
||||
return f"task:{result.task.task_id}"
|
||||
|
||||
async with Client(parent) as client:
|
||||
result = await client.call_tool("outer", {})
|
||||
|
|
@ -1081,7 +1081,7 @@ class TestMountedTasksWithTaskMetaParameter:
|
|||
result = await parent.render_prompt(
|
||||
"c_gc_describe", {"topic": "FastMCP"}, task_meta=TaskMeta(ttl=300)
|
||||
)
|
||||
return f"task:{result.task.taskId}"
|
||||
return f"task:{result.task.task_id}"
|
||||
|
||||
async with Client(parent) as client:
|
||||
result = await client.call_tool("outer", {})
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ async def test_task_notification_sent_after_submission(task_enabled_server):
|
|||
|
||||
# Verify we can query the task
|
||||
status = await task.status()
|
||||
assert status.taskId == task.task_id
|
||||
assert status.task_id == task.task_id
|
||||
|
||||
|
||||
async def test_failed_task_stores_error(task_enabled_server):
|
||||
|
|
|
|||
|
|
@ -407,7 +407,7 @@ async def media_server(tmp_path):
|
|||
lambda r: (
|
||||
len(r.content) == 1
|
||||
and r.content[0].type == "image"
|
||||
and r.content[0].mimeType == "image/png"
|
||||
and r.content[0].mime_type == "image/png"
|
||||
),
|
||||
),
|
||||
(
|
||||
|
|
@ -630,7 +630,7 @@ async def mcp_content_server(tmp_path):
|
|||
lambda r: (
|
||||
len(r.content) == 1
|
||||
and r.content[0].type == "image"
|
||||
and r.content[0].mimeType == "image/png"
|
||||
and r.content[0].mime_type == "image/png"
|
||||
),
|
||||
),
|
||||
(
|
||||
|
|
|
|||
|
|
@ -139,10 +139,10 @@ async def test_dependencies_excluded_from_schema(mcp: FastMCP):
|
|||
result = await mcp._list_tools_mcp(mcp_types.ListToolsRequest())
|
||||
tool = next(t for t in result.tools if t.name == "my_tool")
|
||||
|
||||
assert "name" in tool.inputSchema["properties"]
|
||||
assert "age" in tool.inputSchema["properties"]
|
||||
assert "config" not in tool.inputSchema["properties"]
|
||||
assert len(tool.inputSchema["properties"]) == 2
|
||||
assert "name" in tool.input_schema["properties"]
|
||||
assert "age" in tool.input_schema["properties"]
|
||||
assert "config" not in tool.input_schema["properties"]
|
||||
assert len(tool.input_schema["properties"]) == 2
|
||||
|
||||
|
||||
async def test_current_context_dependency(mcp: FastMCP):
|
||||
|
|
@ -471,8 +471,8 @@ async def test_connection_dependency_excluded_from_tool_schema(mcp: FastMCP):
|
|||
result = await mcp._list_tools_mcp(mcp_types.ListToolsRequest())
|
||||
tool = next(t for t in result.tools if t.name == "with_connection")
|
||||
|
||||
assert "name" in tool.inputSchema["properties"]
|
||||
assert "connection" not in tool.inputSchema["properties"]
|
||||
assert "name" in tool.input_schema["properties"]
|
||||
assert "connection" not in tool.input_schema["properties"]
|
||||
|
||||
|
||||
async def test_sync_tool_context_manager_stays_open(mcp: FastMCP):
|
||||
|
|
@ -592,7 +592,7 @@ async def test_external_user_cannot_override_dependency(mcp: FastMCP):
|
|||
# Verify dependency is NOT in the schema
|
||||
result = await mcp._list_tools_mcp(mcp_types.ListToolsRequest())
|
||||
tool = next(t for t in result.tools if t.name == "check_permission")
|
||||
assert "admin" not in tool.inputSchema["properties"]
|
||||
assert "admin" not in tool.input_schema["properties"]
|
||||
|
||||
# Normal call - dependency is resolved
|
||||
result = await mcp.call_tool("check_permission", {"action": "read"})
|
||||
|
|
@ -979,8 +979,8 @@ class TestAuthDependencies:
|
|||
result = await mcp._list_tools_mcp(mcp_types.ListToolsRequest())
|
||||
tool = next(t for t in result.tools if t.name == "tool_with_token")
|
||||
|
||||
assert "name" in tool.inputSchema["properties"]
|
||||
assert "token" not in tool.inputSchema["properties"]
|
||||
assert "name" in tool.input_schema["properties"]
|
||||
assert "token" not in tool.input_schema["properties"]
|
||||
|
||||
async def test_token_claim_excluded_from_tool_schema(self, mcp: FastMCP):
|
||||
"""Test that TokenClaim dependency is excluded from tool schema."""
|
||||
|
|
@ -998,8 +998,8 @@ class TestAuthDependencies:
|
|||
result = await mcp._list_tools_mcp(mcp_types.ListToolsRequest())
|
||||
tool = next(t for t in result.tools if t.name == "tool_with_claim")
|
||||
|
||||
assert "name" in tool.inputSchema["properties"]
|
||||
assert "user_id" not in tool.inputSchema["properties"]
|
||||
assert "name" in tool.input_schema["properties"]
|
||||
assert "user_id" not in tool.input_schema["properties"]
|
||||
|
||||
def test_current_access_token_exported_from_all(self):
|
||||
"""Test that CurrentAccessToken is exported from __all__."""
|
||||
|
|
@ -1143,8 +1143,8 @@ class TestSharedDependencies:
|
|||
result = await mcp._list_tools_mcp(mcp_types.ListToolsRequest())
|
||||
tool = next(t for t in result.tools if t.name == "my_tool")
|
||||
|
||||
assert "name" in tool.inputSchema["properties"]
|
||||
assert "db" not in tool.inputSchema["properties"]
|
||||
assert "name" in tool.input_schema["properties"]
|
||||
assert "db" not in tool.input_schema["properties"]
|
||||
|
||||
async def test_shared_in_resource(self, mcp: FastMCP):
|
||||
"""Shared dependencies work in resource functions."""
|
||||
|
|
|
|||
|
|
@ -172,9 +172,9 @@ class TestTransformContextAnnotations:
|
|||
# Both ctx params should be excluded from schema
|
||||
result = await mcp._list_tools_mcp(mcp_types.ListToolsRequest())
|
||||
tool = next(t for t in result.tools if t.name == "tool_with_multiple_ctx")
|
||||
assert "name" in tool.inputSchema["properties"]
|
||||
assert "ctx1" not in tool.inputSchema["properties"]
|
||||
assert "ctx2" not in tool.inputSchema["properties"]
|
||||
assert "name" in tool.input_schema["properties"]
|
||||
assert "ctx1" not in tool.input_schema["properties"]
|
||||
assert "ctx2" not in tool.input_schema["properties"]
|
||||
|
||||
async def test_context_in_class_method(self, mcp: FastMCP):
|
||||
"""Test Context transformation works with bound methods."""
|
||||
|
|
|
|||
|
|
@ -36,8 +36,8 @@ class TestServerIcons:
|
|||
|
||||
# Verify that icons and website_url are passed to the underlying server
|
||||
async with Client(mcp) as client:
|
||||
server_info = client.initialize_result.serverInfo
|
||||
assert server_info.websiteUrl == "https://example.com"
|
||||
server_info = client.initialize_result.server_info
|
||||
assert server_info.website_url == "https://example.com"
|
||||
assert server_info.icons == icons
|
||||
|
||||
async def test_server_without_icons_and_website_url(self):
|
||||
|
|
@ -45,8 +45,8 @@ class TestServerIcons:
|
|||
mcp = FastMCP(name="TestServer")
|
||||
|
||||
async with Client(mcp) as client:
|
||||
server_info = client.initialize_result.serverInfo
|
||||
assert server_info.websiteUrl is None
|
||||
server_info = client.initialize_result.server_info
|
||||
assert server_info.website_url is None
|
||||
assert server_info.icons is None
|
||||
|
||||
|
||||
|
|
@ -290,7 +290,7 @@ class TestIconTypes:
|
|||
mcp = FastMCP("TestServer", icons=icons)
|
||||
|
||||
async with Client(mcp) as client:
|
||||
server_info = client.initialize_result.serverInfo
|
||||
server_info = client.initialize_result.server_info
|
||||
assert len(server_info.icons) == 3
|
||||
assert server_info.icons == icons
|
||||
|
||||
|
|
@ -319,9 +319,9 @@ class TestIconTypes:
|
|||
mcp = FastMCP("TestServer", icons=icons)
|
||||
|
||||
async with Client(mcp) as client:
|
||||
server_info = client.initialize_result.serverInfo
|
||||
server_info = client.initialize_result.server_info
|
||||
assert server_info.icons[0].src == "https://example.com/icon.png"
|
||||
assert server_info.icons[0].mimeType is None
|
||||
assert server_info.icons[0].mime_type is None
|
||||
assert server_info.icons[0].sizes is None
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -166,17 +166,17 @@ class TestServerPagination:
|
|||
# First page
|
||||
result = await client.list_tools_mcp()
|
||||
assert len(result.tools) == 10
|
||||
assert result.nextCursor is not None
|
||||
assert result.next_cursor is not None
|
||||
|
||||
# Second page
|
||||
result2 = await client.list_tools_mcp(cursor=result.nextCursor)
|
||||
result2 = await client.list_tools_mcp(cursor=result.next_cursor)
|
||||
assert len(result2.tools) == 10
|
||||
assert result2.nextCursor is not None
|
||||
assert result2.next_cursor is not None
|
||||
|
||||
# Third (last) page
|
||||
result3 = await client.list_tools_mcp(cursor=result2.nextCursor)
|
||||
result3 = await client.list_tools_mcp(cursor=result2.next_cursor)
|
||||
assert len(result3.tools) == 5
|
||||
assert result3.nextCursor is None
|
||||
assert result3.next_cursor is None
|
||||
|
||||
async def test_invalid_cursor_returns_error(self) -> None:
|
||||
"""Server should return MCP error for invalid cursor."""
|
||||
|
|
@ -204,7 +204,7 @@ class TestServerPagination:
|
|||
async with Client(server) as client:
|
||||
result = await client.list_tools_mcp()
|
||||
assert len(result.tools) == 25
|
||||
assert result.nextCursor is None
|
||||
assert result.next_cursor is None
|
||||
|
||||
async def test_pagination_exact_page_boundary(self) -> None:
|
||||
"""Test pagination at exact page boundaries."""
|
||||
|
|
@ -220,12 +220,12 @@ class TestServerPagination:
|
|||
# First page
|
||||
result = await client.list_tools_mcp()
|
||||
assert len(result.tools) == 10
|
||||
assert result.nextCursor is not None
|
||||
assert result.next_cursor is not None
|
||||
|
||||
# Second (last) page
|
||||
result2 = await client.list_tools_mcp(cursor=result.nextCursor)
|
||||
result2 = await client.list_tools_mcp(cursor=result.next_cursor)
|
||||
assert len(result2.tools) == 10
|
||||
assert result2.nextCursor is None
|
||||
assert result2.next_cursor is None
|
||||
|
||||
|
||||
class TestPageSizeValidation:
|
||||
|
|
@ -265,7 +265,7 @@ class TestPaginationCycleDetection:
|
|||
cursor: str | None = None,
|
||||
) -> mcp_types.ListToolsResult:
|
||||
result = await original(cursor=cursor)
|
||||
result.nextCursor = "stuck"
|
||||
result.next_cursor = "stuck"
|
||||
return result
|
||||
|
||||
with patch.object(
|
||||
|
|
@ -294,7 +294,7 @@ class TestPaginationCycleDetection:
|
|||
cursor: str | None = None,
|
||||
) -> mcp_types.ListPromptsResult:
|
||||
result = await original(cursor=cursor)
|
||||
result.nextCursor = "stuck"
|
||||
result.next_cursor = "stuck"
|
||||
return result
|
||||
|
||||
with patch.object(
|
||||
|
|
@ -321,7 +321,7 @@ class TestPaginationCycleDetection:
|
|||
cursor: str | None = None,
|
||||
) -> mcp_types.ListResourcesResult:
|
||||
result = await original(cursor=cursor)
|
||||
result.nextCursor = "stuck"
|
||||
result.next_cursor = "stuck"
|
||||
return result
|
||||
|
||||
with patch.object(
|
||||
|
|
@ -348,7 +348,7 @@ class TestPaginationCycleDetection:
|
|||
cursor: str | None = None,
|
||||
) -> mcp_types.ListResourceTemplatesResult:
|
||||
result = await original(cursor=cursor)
|
||||
result.nextCursor = "stuck"
|
||||
result.next_cursor = "stuck"
|
||||
return result
|
||||
|
||||
with patch.object(
|
||||
|
|
@ -380,7 +380,7 @@ class TestPaginationCycleDetection:
|
|||
result = await original(cursor=cursor)
|
||||
# Cycle through A -> B -> C -> A
|
||||
cursors = ["A", "B", "C"]
|
||||
result.nextCursor = cursors[call_count % 3]
|
||||
result.next_cursor = cursors[call_count % 3]
|
||||
call_count += 1
|
||||
return result
|
||||
|
||||
|
|
@ -409,7 +409,7 @@ class TestPaginationCycleDetection:
|
|||
cursor: str | None = None,
|
||||
) -> mcp_types.ListToolsResult:
|
||||
result = await original(cursor=cursor)
|
||||
result.nextCursor = ""
|
||||
result.next_cursor = ""
|
||||
return result
|
||||
|
||||
with patch.object(
|
||||
|
|
@ -439,7 +439,7 @@ class TestPaginationCycleDetection:
|
|||
nonlocal call_count
|
||||
result = await original(cursor=cursor)
|
||||
call_count += 1
|
||||
result.nextCursor = f"cursor-{call_count}"
|
||||
result.next_cursor = f"cursor-{call_count}"
|
||||
return result
|
||||
|
||||
with (
|
||||
|
|
@ -469,7 +469,7 @@ class TestPaginationCycleDetection:
|
|||
nonlocal call_count
|
||||
result = await original(cursor=cursor)
|
||||
call_count += 1
|
||||
result.nextCursor = f"cursor-{call_count}"
|
||||
result.next_cursor = f"cursor-{call_count}"
|
||||
return result
|
||||
|
||||
with (
|
||||
|
|
@ -499,7 +499,7 @@ class TestPaginationCycleDetection:
|
|||
nonlocal call_count
|
||||
result = await original(cursor=cursor)
|
||||
call_count += 1
|
||||
result.nextCursor = f"cursor-{call_count}"
|
||||
result.next_cursor = f"cursor-{call_count}"
|
||||
return result
|
||||
|
||||
with (
|
||||
|
|
|
|||
|
|
@ -365,7 +365,7 @@ class TestMeta:
|
|||
async with Client(mcp) as client:
|
||||
templates = await client.list_resource_templates()
|
||||
template = next(
|
||||
t for t in templates if t.uriTemplate == "test://template/{id}"
|
||||
t for t in templates if t.uri_template == "test://template/{id}"
|
||||
)
|
||||
assert template.meta is not None
|
||||
assert set(template.meta["fastmcp"]["tags"]) == {
|
||||
|
|
|
|||
|
|
@ -28,8 +28,8 @@ async def test_tool_annotations_in_tool_manager():
|
|||
assert len(tools) == 1
|
||||
assert tools[0].annotations is not None
|
||||
assert tools[0].annotations.title == "Echo Tool"
|
||||
assert tools[0].annotations.readOnlyHint is True
|
||||
assert tools[0].annotations.openWorldHint is False
|
||||
assert tools[0].annotations.read_only_hint is True
|
||||
assert tools[0].annotations.open_world_hint is False
|
||||
|
||||
|
||||
async def test_tool_annotations_in_mcp_protocol():
|
||||
|
|
@ -52,8 +52,8 @@ async def test_tool_annotations_in_mcp_protocol():
|
|||
assert len(result.tools) == 1
|
||||
assert result.tools[0].annotations is not None
|
||||
assert result.tools[0].annotations.title == "Echo Tool"
|
||||
assert result.tools[0].annotations.readOnlyHint is True
|
||||
assert result.tools[0].annotations.openWorldHint is False
|
||||
assert result.tools[0].annotations.read_only_hint is True
|
||||
assert result.tools[0].annotations.open_world_hint is False
|
||||
|
||||
|
||||
async def test_tool_annotations_in_client_api():
|
||||
|
|
@ -78,8 +78,8 @@ async def test_tool_annotations_in_client_api():
|
|||
assert tools_result[0].name == "echo"
|
||||
assert tools_result[0].annotations is not None
|
||||
assert tools_result[0].annotations.title == "Echo Tool"
|
||||
assert tools_result[0].annotations.readOnlyHint is True
|
||||
assert tools_result[0].annotations.openWorldHint is False
|
||||
assert tools_result[0].annotations.read_only_hint is True
|
||||
assert tools_result[0].annotations.open_world_hint is False
|
||||
|
||||
|
||||
async def test_provide_tool_annotations_as_dict_to_decorator():
|
||||
|
|
@ -104,8 +104,8 @@ async def test_provide_tool_annotations_as_dict_to_decorator():
|
|||
assert tools_result[0].name == "echo"
|
||||
assert tools_result[0].annotations is not None
|
||||
assert tools_result[0].annotations.title == "Echo Tool"
|
||||
assert tools_result[0].annotations.readOnlyHint is True
|
||||
assert tools_result[0].annotations.openWorldHint is False
|
||||
assert tools_result[0].annotations.read_only_hint is True
|
||||
assert tools_result[0].annotations.open_world_hint is False
|
||||
|
||||
|
||||
async def test_direct_tool_annotations_in_tool_manager():
|
||||
|
|
@ -130,10 +130,10 @@ async def test_direct_tool_annotations_in_tool_manager():
|
|||
assert len(tools) == 1
|
||||
assert tools[0].annotations is not None
|
||||
assert tools[0].annotations.title == "Direct Tool"
|
||||
assert tools[0].annotations.readOnlyHint is False
|
||||
assert tools[0].annotations.destructiveHint is True
|
||||
assert tools[0].annotations.idempotentHint is False
|
||||
assert tools[0].annotations.openWorldHint is True
|
||||
assert tools[0].annotations.read_only_hint is False
|
||||
assert tools[0].annotations.destructive_hint is True
|
||||
assert tools[0].annotations.idempotent_hint is False
|
||||
assert tools[0].annotations.open_world_hint is True
|
||||
|
||||
|
||||
async def test_direct_tool_annotations_in_client_api():
|
||||
|
|
@ -160,8 +160,8 @@ async def test_direct_tool_annotations_in_client_api():
|
|||
assert tools_result[0].name == "modify"
|
||||
assert tools_result[0].annotations is not None
|
||||
assert tools_result[0].annotations.title == "Direct Tool"
|
||||
assert tools_result[0].annotations.readOnlyHint is False
|
||||
assert tools_result[0].annotations.destructiveHint is True
|
||||
assert tools_result[0].annotations.read_only_hint is False
|
||||
assert tools_result[0].annotations.destructive_hint is True
|
||||
|
||||
|
||||
async def test_add_tool_method_annotations():
|
||||
|
|
@ -189,8 +189,8 @@ async def test_add_tool_method_annotations():
|
|||
assert len(tools) == 1
|
||||
assert tools[0].annotations is not None
|
||||
assert tools[0].annotations.title == "Create Item"
|
||||
assert tools[0].annotations.readOnlyHint is False
|
||||
assert tools[0].annotations.destructiveHint is False
|
||||
assert tools[0].annotations.read_only_hint is False
|
||||
assert tools[0].annotations.destructive_hint is False
|
||||
|
||||
|
||||
async def test_tool_functionality_with_annotations():
|
||||
|
|
@ -221,7 +221,7 @@ async def test_tool_functionality_with_annotations():
|
|||
|
||||
|
||||
async def test_task_execution_auto_populated_for_task_enabled_tool():
|
||||
"""Test that execution.taskSupport is automatically set when tool has task=True."""
|
||||
"""Test that execution.task_support is automatically set when tool has task=True."""
|
||||
mcp = FastMCP("Test Server")
|
||||
|
||||
@mcp.tool(task=True)
|
||||
|
|
@ -235,7 +235,7 @@ async def test_task_execution_auto_populated_for_task_enabled_tool():
|
|||
assert tools_result[0].name == "background_tool"
|
||||
assert isinstance(tools_result[0], MCPTool)
|
||||
assert isinstance(tools_result[0].execution, ToolExecution)
|
||||
assert tools_result[0].execution.taskSupport == "optional"
|
||||
assert tools_result[0].execution.task_support == "optional"
|
||||
|
||||
|
||||
async def test_task_execution_omitted_for_task_disabled_tool():
|
||||
|
|
|
|||
|
|
@ -277,9 +277,9 @@ async def test_openapi_path_params_not_duplicated_in_description():
|
|||
assert tool.description == "My endpoint"
|
||||
|
||||
# Hidden param gone from schema, visible param still present
|
||||
assert "version" not in tool.inputSchema.get("properties", {})
|
||||
assert "user_id" in tool.inputSchema["properties"]
|
||||
assert "version" not in tool.input_schema.get("properties", {})
|
||||
assert "user_id" in tool.input_schema["properties"]
|
||||
assert (
|
||||
tool.inputSchema["properties"]["user_id"]["description"]
|
||||
tool.input_schema["properties"]["user_id"]["description"]
|
||||
== "The user ID"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -241,7 +241,7 @@ class TestResourcesAsToolsAnnotations:
|
|||
tools = await client.list_tools()
|
||||
tool = next(t for t in tools if t.name == "list_resources")
|
||||
assert tool.annotations is not None
|
||||
assert tool.annotations.readOnlyHint is True
|
||||
assert tool.annotations.read_only_hint is True
|
||||
|
||||
async def test_read_resource_is_read_only(self):
|
||||
"""read_resource is annotated as read-only by default."""
|
||||
|
|
@ -252,7 +252,7 @@ class TestResourcesAsToolsAnnotations:
|
|||
tools = await client.list_tools()
|
||||
tool = next(t for t in tools if t.name == "read_resource")
|
||||
assert tool.annotations is not None
|
||||
assert tool.annotations.readOnlyHint is True
|
||||
assert tool.annotations.read_only_hint is True
|
||||
|
||||
|
||||
def _deny_all(ctx: AuthContext) -> bool:
|
||||
|
|
|
|||
|
|
@ -465,7 +465,7 @@ class TestIntegration:
|
|||
resources = await client.list_resources()
|
||||
assert len(resources) == 1
|
||||
assert str(resources[0].uri) == "ui://my-app/view.html"
|
||||
assert resources[0].mimeType == UI_MIME_TYPE
|
||||
assert resources[0].mime_type == UI_MIME_TYPE
|
||||
|
||||
async def test_ui_resource_read_preserves_mime_type(self):
|
||||
"""Reading a ui:// resource returns content with the correct MIME type."""
|
||||
|
|
@ -478,7 +478,7 @@ class TestIntegration:
|
|||
async with Client(server) as client:
|
||||
result = await client.read_resource_mcp("ui://my-app/view.html")
|
||||
assert len(result.contents) == 1
|
||||
assert result.contents[0].mimeType == UI_MIME_TYPE
|
||||
assert result.contents[0].mime_type == UI_MIME_TYPE
|
||||
|
||||
async def test_app_tool_callable(self):
|
||||
"""A tool registered with app= is still callable normally."""
|
||||
|
|
|
|||
|
|
@ -882,7 +882,7 @@ class TestAppIntegration:
|
|||
|
||||
# Call the UI tool through the client and check structured_content
|
||||
result = await client.call_tool_mcp("crm_contact_form", {})
|
||||
sc = result.structuredContent
|
||||
sc = result.structured_content
|
||||
assert sc is not None
|
||||
|
||||
# Call the backend tool via its hashed address — bypasses namespace
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ class TestFutureAnnotations:
|
|||
async with Client(fastmcp_server) as client:
|
||||
result = await client.call_tool("returns_image", {})
|
||||
assert result.content[0].type == "image"
|
||||
assert result.content[0].mimeType == "image/png"
|
||||
assert result.content[0].mime_type == "image/png"
|
||||
|
||||
async def test_async_with_context(self):
|
||||
async with Client(fastmcp_server) as client:
|
||||
|
|
|
|||
|
|
@ -83,10 +83,10 @@ class TestToolResultIsError:
|
|||
)
|
||||
mcp_result = result.to_mcp_result()
|
||||
assert isinstance(mcp_result, CallToolResult)
|
||||
assert mcp_result.isError is True
|
||||
assert mcp_result.is_error is True
|
||||
assert isinstance(mcp_result.content[0], TextContent)
|
||||
assert mcp_result.content[0].text == "boom"
|
||||
assert mcp_result.structuredContent == {"code": 42}
|
||||
assert mcp_result.structured_content == {"code": 42}
|
||||
|
||||
def test_default_is_not_error(self):
|
||||
result = ToolResult(content="ok")
|
||||
|
|
@ -269,7 +269,7 @@ class TestSerializeByAlias:
|
|||
"id": "123",
|
||||
"filepath": "/p",
|
||||
}
|
||||
assert set(tools["get_biofile"].outputSchema["properties"]) == { # type: ignore[index]
|
||||
assert set(tools["get_biofile"].output_schema["properties"]) == { # type: ignore[index]
|
||||
"id",
|
||||
"filepath",
|
||||
}
|
||||
|
|
@ -292,7 +292,7 @@ class TestSerializeByAlias:
|
|||
result = await client.call_tool("get_biofile", {})
|
||||
|
||||
assert result.structured_content == {"_id": "123", "filepath": "/p"}
|
||||
assert set(tools["get_biofile"].outputSchema["properties"]) == { # type: ignore[index]
|
||||
assert set(tools["get_biofile"].output_schema["properties"]) == { # type: ignore[index]
|
||||
"_id",
|
||||
"filepath",
|
||||
}
|
||||
|
|
@ -315,7 +315,7 @@ class TestSerializeByAlias:
|
|||
result = await client.call_tool("get_biofile", {})
|
||||
|
||||
assert result.structured_content == {"_id": "123"}
|
||||
assert set(tools["get_biofile"].outputSchema["properties"]) == {"_id"} # type: ignore[index]
|
||||
assert set(tools["get_biofile"].output_schema["properties"]) == {"_id"} # type: ignore[index]
|
||||
|
||||
async def test_nested_models_respect_config(self):
|
||||
"""serialize_by_alias=False propagates through nested models."""
|
||||
|
|
@ -364,7 +364,7 @@ class TestSerializeByAlias:
|
|||
# raises if they disagree
|
||||
result = await client.call_tool("get_biofile", {})
|
||||
|
||||
schema_props = set(tools["get_biofile"].outputSchema["properties"]) # type: ignore[index]
|
||||
schema_props = set(tools["get_biofile"].output_schema["properties"]) # type: ignore[index]
|
||||
assert schema_props == set(result.structured_content) # type: ignore[arg-type]
|
||||
assert result.structured_content == {"result": {"id": "1"}}
|
||||
|
||||
|
|
@ -391,5 +391,5 @@ class TestSerializeByAlias:
|
|||
tools = {t.name: t for t in await client.list_tools()}
|
||||
result = await client.call_tool("get_model", {})
|
||||
|
||||
schema_props = set(tools["get_model"].outputSchema["properties"]) # type: ignore[index]
|
||||
schema_props = set(tools["get_model"].output_schema["properties"]) # type: ignore[index]
|
||||
assert schema_props == set(result.structured_content) # type: ignore[arg-type]
|
||||
|
|
|
|||
|
|
@ -547,7 +547,7 @@ class TestToolExecutionField:
|
|||
|
||||
mcp_tool = tool.to_mcp_tool()
|
||||
assert mcp_tool.execution is not None
|
||||
assert mcp_tool.execution.taskSupport == "optional"
|
||||
assert mcp_tool.execution.task_support == "optional"
|
||||
|
||||
def test_tool_without_execution_field(self):
|
||||
"""Test that Tool without execution returns None."""
|
||||
|
|
@ -572,7 +572,7 @@ class TestToolExecutionField:
|
|||
override_execution = ToolExecution(taskSupport="required")
|
||||
mcp_tool = tool.to_mcp_tool(execution=override_execution)
|
||||
assert mcp_tool.execution is not None
|
||||
assert mcp_tool.execution.taskSupport == "required"
|
||||
assert mcp_tool.execution.task_support == "required"
|
||||
|
||||
async def test_function_tool_task_config_still_works(self):
|
||||
"""FunctionTool should still derive execution from task_config."""
|
||||
|
|
@ -585,7 +585,7 @@ class TestToolExecutionField:
|
|||
|
||||
# FunctionTool sets execution from task_config
|
||||
assert mcp_tool.execution is not None
|
||||
assert mcp_tool.execution.taskSupport == "optional"
|
||||
assert mcp_tool.execution.task_support == "optional"
|
||||
|
||||
def test_tool_execution_required_mode(self):
|
||||
"""Test that Tool can store required execution mode."""
|
||||
|
|
@ -598,7 +598,7 @@ class TestToolExecutionField:
|
|||
|
||||
mcp_tool = tool.to_mcp_tool()
|
||||
assert mcp_tool.execution is not None
|
||||
assert mcp_tool.execution.taskSupport == "required"
|
||||
assert mcp_tool.execution.task_support == "required"
|
||||
|
||||
def test_tool_execution_forbidden_mode(self):
|
||||
"""Test that Tool can store forbidden execution mode."""
|
||||
|
|
@ -611,4 +611,4 @@ class TestToolExecutionField:
|
|||
|
||||
mcp_tool = tool.to_mcp_tool()
|
||||
assert mcp_tool.execution is not None
|
||||
assert mcp_tool.execution.taskSupport == "forbidden"
|
||||
assert mcp_tool.execution.task_support == "forbidden"
|
||||
|
|
|
|||
|
|
@ -458,7 +458,7 @@ class TestNullableInputSchemaIntegration:
|
|||
"""Test that nullable fields are converted in tool input schemas end-to-end.
|
||||
|
||||
These tests exercise the full pipeline: OpenAPI spec -> OpenAPIProvider ->
|
||||
tool.inputSchema, verifying that `nullable: true` doesn't leak through.
|
||||
tool.input_schema, verifying that `nullable: true` doesn't leak through.
|
||||
"""
|
||||
|
||||
async def test_nullable_query_param_converted_in_tool_input_schema(self):
|
||||
|
|
@ -496,7 +496,7 @@ class TestNullableInputSchemaIntegration:
|
|||
async with Client(mcp) as mcp_client:
|
||||
tools = await mcp_client.list_tools()
|
||||
assert len(tools) == 1
|
||||
schema = tools[0].inputSchema
|
||||
schema = tools[0].input_schema
|
||||
category_prop = schema["properties"]["category"]
|
||||
assert "nullable" not in category_prop
|
||||
assert category_prop["type"] == ["string", "null"]
|
||||
|
|
@ -545,7 +545,7 @@ class TestNullableInputSchemaIntegration:
|
|||
async with Client(mcp) as mcp_client:
|
||||
tools = await mcp_client.list_tools()
|
||||
assert len(tools) == 1
|
||||
schema = tools[0].inputSchema
|
||||
schema = tools[0].input_schema
|
||||
|
||||
# Find the bio property — it may be inline or in $defs
|
||||
if "$defs" in schema:
|
||||
|
|
|
|||
|
|
@ -204,7 +204,7 @@ class TestImage:
|
|||
content = img.to_image_content()
|
||||
|
||||
assert content.type == "image"
|
||||
assert content.mimeType == "image/png"
|
||||
assert content.mime_type == "image/png"
|
||||
assert content.data == base64.b64encode(test_data).decode()
|
||||
|
||||
# Test with data
|
||||
|
|
@ -212,7 +212,7 @@ class TestImage:
|
|||
content = img.to_image_content()
|
||||
|
||||
assert content.type == "image"
|
||||
assert content.mimeType == "image/jpeg"
|
||||
assert content.mime_type == "image/jpeg"
|
||||
assert content.data == base64.b64encode(test_data).decode()
|
||||
|
||||
def test_to_image_content_error(self, monkeypatch):
|
||||
|
|
@ -327,7 +327,7 @@ class TestAudio:
|
|||
content = audio.to_audio_content()
|
||||
|
||||
assert content.type == "audio"
|
||||
assert content.mimeType == "audio/wav"
|
||||
assert content.mime_type == "audio/wav"
|
||||
assert content.data == base64.b64encode(test_data).decode()
|
||||
|
||||
# Test with data
|
||||
|
|
@ -335,7 +335,7 @@ class TestAudio:
|
|||
content = audio.to_audio_content()
|
||||
|
||||
assert content.type == "audio"
|
||||
assert content.mimeType == "audio/mp3"
|
||||
assert content.mime_type == "audio/mp3"
|
||||
assert content.data == base64.b64encode(test_data).decode()
|
||||
|
||||
def test_to_audio_content_error(self, monkeypatch):
|
||||
|
|
@ -359,7 +359,7 @@ class TestAudio:
|
|||
content = audio.to_audio_content(mime_type="audio/custom")
|
||||
|
||||
assert content.type == "audio"
|
||||
assert content.mimeType == "audio/custom"
|
||||
assert content.mime_type == "audio/custom"
|
||||
assert content.data == base64.b64encode(test_data).decode()
|
||||
|
||||
|
||||
|
|
@ -442,7 +442,7 @@ class TestFile:
|
|||
resource = file.to_resource_content()
|
||||
|
||||
assert resource.type == "resource"
|
||||
assert resource.resource.mimeType == "text/plain"
|
||||
assert resource.resource.mime_type == "text/plain"
|
||||
# Convert both to strings for comparison
|
||||
assert str(resource.resource.uri) == file_path.resolve().as_uri()
|
||||
if isinstance(resource.resource, BlobResourceContents):
|
||||
|
|
@ -455,7 +455,7 @@ class TestFile:
|
|||
resource = file.to_resource_content()
|
||||
|
||||
assert resource.type == "resource"
|
||||
assert resource.resource.mimeType == "application/pdf"
|
||||
assert resource.resource.mime_type == "application/pdf"
|
||||
# Convert URI to string for comparison
|
||||
assert str(resource.resource.uri) == "file:///resource.pdf"
|
||||
if isinstance(resource.resource, BlobResourceContents):
|
||||
|
|
@ -469,7 +469,7 @@ class TestFile:
|
|||
assert resource.type == "resource"
|
||||
# Should be TextResourceContents for text/plain
|
||||
assert isinstance(resource.resource, TextResourceContents)
|
||||
assert resource.resource.mimeType == "text/plain"
|
||||
assert resource.resource.mime_type == "text/plain"
|
||||
assert resource.resource.text == "hello world"
|
||||
|
||||
def test_to_resource_content_error(self, monkeypatch):
|
||||
|
|
@ -490,7 +490,7 @@ class TestFile:
|
|||
file = File(path=file_path)
|
||||
resource = file.to_resource_content(mime_type="application/custom")
|
||||
|
||||
assert resource.resource.mimeType == "application/custom"
|
||||
assert resource.resource.mime_type == "application/custom"
|
||||
|
||||
|
||||
class TestReplaceType:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue