refactor: McpError renamed to MCPError and flatten parameters (#1956)
Co-authored-by: Max Isbey <224885523+maxisbey@users.noreply.github.com>
This commit is contained in:
parent
65c614e48e
commit
acba5478a9
38 changed files with 253 additions and 326 deletions
|
|
@ -337,7 +337,7 @@ if __name__ == "__main__":
|
|||
Handle task errors gracefully:
|
||||
|
||||
```python
|
||||
from mcp.shared.exceptions import McpError
|
||||
from mcp.shared.exceptions import MCPError
|
||||
|
||||
try:
|
||||
result = await session.experimental.call_tool_as_task("my_tool", args)
|
||||
|
|
@ -349,8 +349,8 @@ try:
|
|||
|
||||
final = await session.experimental.get_task_result(task_id, CallToolResult)
|
||||
|
||||
except McpError as e:
|
||||
print(f"MCP error: {e.error.message}")
|
||||
except MCPError as e:
|
||||
print(f"MCP error: {e.message}")
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
```
|
||||
|
|
|
|||
|
|
@ -121,6 +121,38 @@ result = await session.list_resources(params=PaginatedRequestParams(cursor="next
|
|||
result = await session.list_tools(params=PaginatedRequestParams(cursor="next_page_token"))
|
||||
```
|
||||
|
||||
### `McpError` renamed to `MCPError`
|
||||
|
||||
The `McpError` exception class has been renamed to `MCPError` for consistent naming with the MCP acronym style used throughout the SDK.
|
||||
|
||||
**Before (v1):**
|
||||
|
||||
```python
|
||||
from mcp.shared.exceptions import McpError
|
||||
|
||||
try:
|
||||
result = await session.call_tool("my_tool")
|
||||
except McpError as e:
|
||||
print(f"Error: {e.error.message}")
|
||||
```
|
||||
|
||||
**After (v2):**
|
||||
|
||||
```python
|
||||
from mcp.shared.exceptions import MCPError
|
||||
|
||||
try:
|
||||
result = await session.call_tool("my_tool")
|
||||
except MCPError as e:
|
||||
print(f"Error: {e.message}")
|
||||
```
|
||||
|
||||
`MCPError` is also exported from the top-level `mcp` package:
|
||||
|
||||
```python
|
||||
from mcp import MCPError
|
||||
```
|
||||
|
||||
### `FastMCP` renamed to `MCPServer`
|
||||
|
||||
The `FastMCP` class has been renamed to `MCPServer` to better reflect its role as the main server class in the SDK. This is a simple rename with no functional changes to the class itself.
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ from urllib.parse import urlparse
|
|||
from mcp import ClientSession, types
|
||||
from mcp.client.sse import sse_client
|
||||
from mcp.shared.context import RequestContext
|
||||
from mcp.shared.exceptions import McpError, UrlElicitationRequiredError
|
||||
from mcp.shared.exceptions import MCPError, UrlElicitationRequiredError
|
||||
from mcp.types import URL_ELICITATION_REQUIRED
|
||||
|
||||
|
||||
|
|
@ -160,9 +160,9 @@ async def call_tool_with_error_handling(
|
|||
|
||||
return result
|
||||
|
||||
except McpError as e:
|
||||
except MCPError as e:
|
||||
# Check if this is a URL elicitation required error
|
||||
if e.error.code == URL_ELICITATION_REQUIRED:
|
||||
if e.code == URL_ELICITATION_REQUIRED:
|
||||
print("\n[Tool requires URL elicitation to proceed]")
|
||||
|
||||
# Convert to typed error to access elicitations
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from .client.session_group import ClientSessionGroup
|
|||
from .client.stdio import StdioServerParameters, stdio_client
|
||||
from .server.session import ServerSession
|
||||
from .server.stdio import stdio_server
|
||||
from .shared.exceptions import McpError, UrlElicitationRequiredError
|
||||
from .shared.exceptions import MCPError, UrlElicitationRequiredError
|
||||
from .types import (
|
||||
CallToolRequest,
|
||||
ClientCapabilities,
|
||||
|
|
@ -96,7 +96,7 @@ __all__ = [
|
|||
"ListToolsResult",
|
||||
"LoggingLevel",
|
||||
"LoggingMessageNotification",
|
||||
"McpError",
|
||||
"MCPError",
|
||||
"Notification",
|
||||
"PingRequest",
|
||||
"ProgressNotification",
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ from mcp.client.sse import sse_client
|
|||
from mcp.client.stdio import StdioServerParameters
|
||||
from mcp.client.streamable_http import streamable_http_client
|
||||
from mcp.shared._httpx_utils import create_mcp_http_client
|
||||
from mcp.shared.exceptions import McpError
|
||||
from mcp.shared.exceptions import MCPError
|
||||
from mcp.shared.session import ProgressFnT
|
||||
|
||||
|
||||
|
|
@ -216,11 +216,9 @@ class ClientSessionGroup:
|
|||
session_known_for_stack = session in self._session_exit_stacks
|
||||
|
||||
if not session_known_for_components and not session_known_for_stack:
|
||||
raise McpError(
|
||||
types.ErrorData(
|
||||
code=types.INVALID_PARAMS,
|
||||
message="Provided session is not managed or already disconnected.",
|
||||
)
|
||||
raise MCPError(
|
||||
code=types.INVALID_PARAMS,
|
||||
message="Provided session is not managed or already disconnected.",
|
||||
)
|
||||
|
||||
if session_known_for_components: # pragma: no branch
|
||||
|
|
@ -352,7 +350,7 @@ class ClientSessionGroup:
|
|||
name = self._component_name(prompt.name, server_info)
|
||||
prompts_temp[name] = prompt
|
||||
component_names.prompts.add(name)
|
||||
except McpError as err: # pragma: no cover
|
||||
except MCPError as err: # pragma: no cover
|
||||
logging.warning(f"Could not fetch prompts: {err}")
|
||||
|
||||
# Query the server for its resources and aggregate to list.
|
||||
|
|
@ -362,7 +360,7 @@ class ClientSessionGroup:
|
|||
name = self._component_name(resource.name, server_info)
|
||||
resources_temp[name] = resource
|
||||
component_names.resources.add(name)
|
||||
except McpError as err: # pragma: no cover
|
||||
except MCPError as err: # pragma: no cover
|
||||
logging.warning(f"Could not fetch resources: {err}")
|
||||
|
||||
# Query the server for its tools and aggregate to list.
|
||||
|
|
@ -373,7 +371,7 @@ class ClientSessionGroup:
|
|||
tools_temp[name] = tool
|
||||
tool_to_session_temp[name] = session
|
||||
component_names.tools.add(name)
|
||||
except McpError as err: # pragma: no cover
|
||||
except MCPError as err: # pragma: no cover
|
||||
logging.warning(f"Could not fetch tools: {err}")
|
||||
|
||||
# Clean up exit stack for session if we couldn't retrieve anything
|
||||
|
|
@ -384,28 +382,19 @@ class ClientSessionGroup:
|
|||
# Check for duplicates.
|
||||
matching_prompts = prompts_temp.keys() & self._prompts.keys()
|
||||
if matching_prompts:
|
||||
raise McpError( # pragma: no cover
|
||||
types.ErrorData(
|
||||
code=types.INVALID_PARAMS,
|
||||
message=f"{matching_prompts} already exist in group prompts.",
|
||||
)
|
||||
raise MCPError( # pragma: no cover
|
||||
code=types.INVALID_PARAMS,
|
||||
message=f"{matching_prompts} already exist in group prompts.",
|
||||
)
|
||||
matching_resources = resources_temp.keys() & self._resources.keys()
|
||||
if matching_resources:
|
||||
raise McpError( # pragma: no cover
|
||||
types.ErrorData(
|
||||
code=types.INVALID_PARAMS,
|
||||
message=f"{matching_resources} already exist in group resources.",
|
||||
)
|
||||
raise MCPError( # pragma: no cover
|
||||
code=types.INVALID_PARAMS,
|
||||
message=f"{matching_resources} already exist in group resources.",
|
||||
)
|
||||
matching_tools = tools_temp.keys() & self._tools.keys()
|
||||
if matching_tools:
|
||||
raise McpError(
|
||||
types.ErrorData(
|
||||
code=types.INVALID_PARAMS,
|
||||
message=f"{matching_tools} already exist in group tools.",
|
||||
)
|
||||
)
|
||||
raise MCPError(code=types.INVALID_PARAMS, message=f"{matching_tools} already exist in group tools.")
|
||||
|
||||
# Aggregate components.
|
||||
self._sessions[session] = component_names
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ from typing import Any
|
|||
from mcp.server.experimental.task_context import ServerTaskContext
|
||||
from mcp.server.experimental.task_support import TaskSupport
|
||||
from mcp.server.session import ServerSession
|
||||
from mcp.shared.exceptions import McpError
|
||||
from mcp.shared.exceptions import MCPError
|
||||
from mcp.shared.experimental.tasks.helpers import MODEL_IMMEDIATE_RESPONSE_KEY, is_terminal
|
||||
from mcp.types import (
|
||||
METHOD_NOT_FOUND,
|
||||
|
|
@ -72,13 +72,13 @@ class Experimental:
|
|||
Args:
|
||||
tool_task_mode: The tool's execution.taskSupport value
|
||||
("forbidden", "optional", "required", or None)
|
||||
raise_error: If True, raises McpError on validation failure. If False, returns ErrorData.
|
||||
raise_error: If True, raises MCPError on validation failure. If False, returns ErrorData.
|
||||
|
||||
Returns:
|
||||
None if valid, ErrorData if invalid and raise_error=False
|
||||
|
||||
Raises:
|
||||
McpError: If invalid and raise_error=True
|
||||
MCPError: If invalid and raise_error=True
|
||||
"""
|
||||
|
||||
mode = tool_task_mode or TASK_FORBIDDEN
|
||||
|
|
@ -86,18 +86,12 @@ class Experimental:
|
|||
error: ErrorData | None = None
|
||||
|
||||
if mode == TASK_REQUIRED and not self.is_task:
|
||||
error = ErrorData(
|
||||
code=METHOD_NOT_FOUND,
|
||||
message="This tool requires task-augmented invocation",
|
||||
)
|
||||
error = ErrorData(code=METHOD_NOT_FOUND, message="This tool requires task-augmented invocation")
|
||||
elif mode == TASK_FORBIDDEN and self.is_task:
|
||||
error = ErrorData(
|
||||
code=METHOD_NOT_FOUND,
|
||||
message="This tool does not support task-augmented invocation",
|
||||
)
|
||||
error = ErrorData(code=METHOD_NOT_FOUND, message="This tool does not support task-augmented invocation")
|
||||
|
||||
if error is not None and raise_error:
|
||||
raise McpError(error)
|
||||
raise MCPError(code=error.code, message=error.message)
|
||||
|
||||
return error
|
||||
|
||||
|
|
@ -113,7 +107,7 @@ class Experimental:
|
|||
|
||||
Args:
|
||||
tool: The Tool definition
|
||||
raise_error: If True, raises McpError on validation failure.
|
||||
raise_error: If True, raises MCPError on validation failure.
|
||||
|
||||
Returns:
|
||||
None if valid, ErrorData if invalid and raise_error=False
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ class ExperimentalServerSessionFeatures:
|
|||
The client's elicitation response
|
||||
|
||||
Raises:
|
||||
McpError: If client doesn't support task-augmented elicitation
|
||||
MCPError: If client doesn't support task-augmented elicitation
|
||||
"""
|
||||
client_caps = self._session.client_params.capabilities if self._session.client_params else None
|
||||
require_task_augmented_elicitation(client_caps)
|
||||
|
|
@ -174,7 +174,7 @@ class ExperimentalServerSessionFeatures:
|
|||
The sampling result from the client
|
||||
|
||||
Raises:
|
||||
McpError: If client doesn't support task-augmented sampling or tools
|
||||
MCPError: If client doesn't support task-augmented sampling or tools
|
||||
ValueError: If tool_use or tool_result message structure is invalid
|
||||
"""
|
||||
client_caps = self._session.client_params.capabilities if self._session.client_params else None
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import anyio
|
|||
from mcp.server.experimental.task_result_handler import TaskResultHandler
|
||||
from mcp.server.session import ServerSession
|
||||
from mcp.server.validation import validate_sampling_tools, validate_tool_use_result_messages
|
||||
from mcp.shared.exceptions import McpError
|
||||
from mcp.shared.exceptions import MCPError
|
||||
from mcp.shared.experimental.tasks.capabilities import (
|
||||
require_task_augmented_elicitation,
|
||||
require_task_augmented_sampling,
|
||||
|
|
@ -32,7 +32,6 @@ from mcp.types import (
|
|||
ElicitationCapability,
|
||||
ElicitRequestedSchema,
|
||||
ElicitResult,
|
||||
ErrorData,
|
||||
IncludeContext,
|
||||
ModelPreferences,
|
||||
RequestId,
|
||||
|
|
@ -173,22 +172,12 @@ class ServerTaskContext:
|
|||
def _check_elicitation_capability(self) -> None:
|
||||
"""Check if the client supports elicitation."""
|
||||
if not self._session.check_client_capability(ClientCapabilities(elicitation=ElicitationCapability())):
|
||||
raise McpError(
|
||||
ErrorData(
|
||||
code=INVALID_REQUEST,
|
||||
message="Client does not support elicitation capability",
|
||||
)
|
||||
)
|
||||
raise MCPError(code=INVALID_REQUEST, message="Client does not support elicitation capability")
|
||||
|
||||
def _check_sampling_capability(self) -> None:
|
||||
"""Check if the client supports sampling."""
|
||||
if not self._session.check_client_capability(ClientCapabilities(sampling=SamplingCapability())):
|
||||
raise McpError(
|
||||
ErrorData(
|
||||
code=INVALID_REQUEST,
|
||||
message="Client does not support sampling capability",
|
||||
)
|
||||
)
|
||||
raise MCPError(code=INVALID_REQUEST, message="Client does not support sampling capability")
|
||||
|
||||
async def elicit(
|
||||
self,
|
||||
|
|
@ -213,7 +202,7 @@ class ServerTaskContext:
|
|||
The client's response
|
||||
|
||||
Raises:
|
||||
McpError: If client doesn't support elicitation capability
|
||||
MCPError: If client doesn't support elicitation capability
|
||||
"""
|
||||
self._check_elicitation_capability()
|
||||
|
||||
|
|
@ -281,7 +270,7 @@ class ServerTaskContext:
|
|||
The client's response indicating acceptance, decline, or cancellation
|
||||
|
||||
Raises:
|
||||
McpError: If client doesn't support elicitation capability
|
||||
MCPError: If client doesn't support elicitation capability
|
||||
RuntimeError: If handler is not configured
|
||||
"""
|
||||
self._check_elicitation_capability()
|
||||
|
|
@ -361,7 +350,7 @@ class ServerTaskContext:
|
|||
The sampling result from the client
|
||||
|
||||
Raises:
|
||||
McpError: If client doesn't support sampling capability or tools
|
||||
MCPError: If client doesn't support sampling capability or tools
|
||||
ValueError: If tool_use or tool_result message structure is invalid
|
||||
"""
|
||||
self._check_sampling_capability()
|
||||
|
|
@ -436,7 +425,7 @@ class ServerTaskContext:
|
|||
The client's elicitation response
|
||||
|
||||
Raises:
|
||||
McpError: If client doesn't support task-augmented elicitation
|
||||
MCPError: If client doesn't support task-augmented elicitation
|
||||
RuntimeError: If handler is not configured
|
||||
"""
|
||||
client_caps = self._session.client_params.capabilities if self._session.client_params else None
|
||||
|
|
@ -529,7 +518,7 @@ class ServerTaskContext:
|
|||
The sampling result from the client
|
||||
|
||||
Raises:
|
||||
McpError: If client doesn't support task-augmented sampling or tools
|
||||
MCPError: If client doesn't support task-augmented sampling or tools
|
||||
ValueError: If tool_use or tool_result message structure is invalid
|
||||
RuntimeError: If handler is not configured
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ from typing import Any
|
|||
import anyio
|
||||
|
||||
from mcp.server.session import ServerSession
|
||||
from mcp.shared.exceptions import McpError
|
||||
from mcp.shared.exceptions import MCPError
|
||||
from mcp.shared.experimental.tasks.helpers import RELATED_TASK_METADATA_KEY, is_terminal
|
||||
from mcp.shared.experimental.tasks.message_queue import TaskMessageQueue
|
||||
from mcp.shared.experimental.tasks.resolver import Resolver
|
||||
|
|
@ -106,7 +106,7 @@ class TaskResultHandler:
|
|||
while True:
|
||||
task = await self._store.get_task(task_id)
|
||||
if task is None:
|
||||
raise McpError(ErrorData(code=INVALID_PARAMS, message=f"Task not found: {task_id}"))
|
||||
raise MCPError(code=INVALID_PARAMS, message=f"Task not found: {task_id}")
|
||||
|
||||
await self._deliver_queued_messages(task_id, session, request_id)
|
||||
|
||||
|
|
@ -216,6 +216,6 @@ class TaskResultHandler:
|
|||
"""
|
||||
resolver = self._pending_requests.pop(request_id, None)
|
||||
if resolver is not None and not resolver.done():
|
||||
resolver.set_exception(McpError(error))
|
||||
resolver.set_exception(MCPError.from_error_data(error))
|
||||
return True
|
||||
return False
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ from typing import TYPE_CHECKING
|
|||
|
||||
from mcp.server.experimental.task_support import TaskSupport
|
||||
from mcp.server.lowlevel.func_inspection import create_call_wrapper
|
||||
from mcp.shared.exceptions import McpError
|
||||
from mcp.shared.exceptions import MCPError
|
||||
from mcp.shared.experimental.tasks.helpers import cancel_task
|
||||
from mcp.shared.experimental.tasks.in_memory_task_store import InMemoryTaskStore
|
||||
from mcp.shared.experimental.tasks.message_queue import InMemoryTaskMessageQueue, TaskMessageQueue
|
||||
|
|
@ -20,7 +20,6 @@ from mcp.types import (
|
|||
INVALID_PARAMS,
|
||||
CancelTaskRequest,
|
||||
CancelTaskResult,
|
||||
ErrorData,
|
||||
GetTaskPayloadRequest,
|
||||
GetTaskPayloadResult,
|
||||
GetTaskRequest,
|
||||
|
|
@ -135,12 +134,7 @@ class ExperimentalHandlers:
|
|||
async def _default_get_task(req: GetTaskRequest) -> ServerResult:
|
||||
task = await support.store.get_task(req.params.task_id)
|
||||
if task is None:
|
||||
raise McpError(
|
||||
ErrorData(
|
||||
code=INVALID_PARAMS,
|
||||
message=f"Task not found: {req.params.task_id}",
|
||||
)
|
||||
)
|
||||
raise MCPError(code=INVALID_PARAMS, message=f"Task not found: {req.params.task_id}")
|
||||
return GetTaskResult(
|
||||
task_id=task.task_id,
|
||||
status=task.status,
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ from mcp.server.streamable_http import EventStore
|
|||
from mcp.server.streamable_http_manager import StreamableHTTPASGIApp, StreamableHTTPSessionManager
|
||||
from mcp.server.transport_security import TransportSecuritySettings
|
||||
from mcp.shared.context import RequestContext
|
||||
from mcp.shared.exceptions import McpError, UrlElicitationRequiredError
|
||||
from mcp.shared.exceptions import MCPError, UrlElicitationRequiredError
|
||||
from mcp.shared.message import ServerMessageMetadata, SessionMessage
|
||||
from mcp.shared.session import RequestResponder
|
||||
from mcp.shared.tool_name_validation import validate_and_warn_tool_name
|
||||
|
|
@ -407,9 +407,7 @@ class Server(Generic[LifespanResultT, RequestT]):
|
|||
case _: # pragma: no cover
|
||||
raise ValueError(f"Unexpected return type from read_resource: {type(result)}")
|
||||
|
||||
return types.ReadResourceResult( # pragma: no cover
|
||||
contents=[content],
|
||||
)
|
||||
return types.ReadResourceResult(contents=[content]) # pragma: no cover
|
||||
|
||||
self.request_handlers[types.ReadResourceRequest] = handler
|
||||
return func
|
||||
|
|
@ -781,13 +779,10 @@ class Server(Generic[LifespanResultT, RequestT]):
|
|||
)
|
||||
)
|
||||
response = await handler(req)
|
||||
except McpError as err:
|
||||
except MCPError as err:
|
||||
response = err.error
|
||||
except anyio.get_cancelled_exc_class():
|
||||
logger.info(
|
||||
"Request %s cancelled - duplicate response suppressed",
|
||||
message.request_id,
|
||||
)
|
||||
logger.info("Request %s cancelled - duplicate response suppressed", message.request_id)
|
||||
return
|
||||
except Exception as err:
|
||||
if raise_exceptions: # pragma: no cover
|
||||
|
|
|
|||
|
|
@ -314,7 +314,7 @@ class ServerSession(
|
|||
The sampling result from the client.
|
||||
|
||||
Raises:
|
||||
McpError: If tools are provided but client doesn't support them.
|
||||
MCPError: If tools are provided but client doesn't support them.
|
||||
ValueError: If tool_use or tool_result message structure is invalid.
|
||||
StatelessModeNotSupported: If called in stateless HTTP mode.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -4,15 +4,8 @@ This module provides validation logic for sampling and elicitation requests
|
|||
that is shared across normal and task-augmented code paths.
|
||||
"""
|
||||
|
||||
from mcp.shared.exceptions import McpError
|
||||
from mcp.types import (
|
||||
INVALID_PARAMS,
|
||||
ClientCapabilities,
|
||||
ErrorData,
|
||||
SamplingMessage,
|
||||
Tool,
|
||||
ToolChoice,
|
||||
)
|
||||
from mcp.shared.exceptions import MCPError
|
||||
from mcp.types import INVALID_PARAMS, ClientCapabilities, SamplingMessage, Tool, ToolChoice
|
||||
|
||||
|
||||
def check_sampling_tools_capability(client_caps: ClientCapabilities | None) -> bool:
|
||||
|
|
@ -46,11 +39,11 @@ def validate_sampling_tools(
|
|||
tool_choice: The tool choice setting, if provided
|
||||
|
||||
Raises:
|
||||
McpError: If tools/tool_choice are provided but client doesn't support them
|
||||
MCPError: If tools/tool_choice are provided but client doesn't support them
|
||||
"""
|
||||
if tools is not None or tool_choice is not None:
|
||||
if not check_sampling_tools_capability(client_caps):
|
||||
raise McpError(ErrorData(code=INVALID_PARAMS, message="Client does not support sampling tools capability"))
|
||||
raise MCPError(code=INVALID_PARAMS, message="Client does not support sampling tools capability")
|
||||
|
||||
|
||||
def validate_tool_use_result_messages(messages: list[SamplingMessage]) -> None:
|
||||
|
|
|
|||
|
|
@ -2,18 +2,40 @@ from __future__ import annotations
|
|||
|
||||
from typing import Any, cast
|
||||
|
||||
from mcp.types import URL_ELICITATION_REQUIRED, ElicitRequestURLParams, ErrorData
|
||||
from mcp.types import URL_ELICITATION_REQUIRED, ElicitRequestURLParams, ErrorData, JSONRPCError
|
||||
|
||||
|
||||
class McpError(Exception):
|
||||
class MCPError(Exception):
|
||||
"""Exception type raised when an error arrives over an MCP connection."""
|
||||
|
||||
error: ErrorData
|
||||
|
||||
def __init__(self, error: ErrorData):
|
||||
"""Initialize McpError."""
|
||||
super().__init__(error.message)
|
||||
self.error = error
|
||||
def __init__(self, code: int, message: str, data: Any = None):
|
||||
super().__init__(code, message, data)
|
||||
self.error = ErrorData(code=code, message=message, data=data)
|
||||
|
||||
@property
|
||||
def code(self) -> int:
|
||||
return self.error.code
|
||||
|
||||
@property
|
||||
def message(self) -> str:
|
||||
return self.error.message
|
||||
|
||||
@property
|
||||
def data(self) -> Any:
|
||||
return self.error.data # pragma: no cover
|
||||
|
||||
@classmethod
|
||||
def from_jsonrpc_error(cls, error: JSONRPCError) -> MCPError:
|
||||
return cls.from_error_data(error.error)
|
||||
|
||||
@classmethod
|
||||
def from_error_data(cls, error: ErrorData) -> MCPError:
|
||||
return cls(code=error.code, message=error.message, data=error.data)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.message
|
||||
|
||||
|
||||
class StatelessModeNotSupported(RuntimeError):
|
||||
|
|
@ -33,7 +55,7 @@ class StatelessModeNotSupported(RuntimeError):
|
|||
self.method = method
|
||||
|
||||
|
||||
class UrlElicitationRequiredError(McpError):
|
||||
class UrlElicitationRequiredError(MCPError):
|
||||
"""Specialized error for when a tool requires URL mode elicitation(s) before proceeding.
|
||||
|
||||
Servers can raise this error from tool handlers to indicate that the client
|
||||
|
|
@ -42,7 +64,6 @@ class UrlElicitationRequiredError(McpError):
|
|||
Example:
|
||||
raise UrlElicitationRequiredError([
|
||||
ElicitRequestURLParams(
|
||||
mode="url",
|
||||
message="Authorization required for your files",
|
||||
url="https://example.com/oauth/authorize",
|
||||
elicitation_id="auth-001"
|
||||
|
|
@ -50,23 +71,18 @@ class UrlElicitationRequiredError(McpError):
|
|||
])
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
elicitations: list[ElicitRequestURLParams],
|
||||
message: str | None = None,
|
||||
):
|
||||
def __init__(self, elicitations: list[ElicitRequestURLParams], message: str | None = None):
|
||||
"""Initialize UrlElicitationRequiredError."""
|
||||
if message is None:
|
||||
message = f"URL elicitation{'s' if len(elicitations) > 1 else ''} required"
|
||||
|
||||
self._elicitations = elicitations
|
||||
|
||||
error = ErrorData(
|
||||
super().__init__(
|
||||
code=URL_ELICITATION_REQUIRED,
|
||||
message=message,
|
||||
data={"elicitations": [e.model_dump(by_alias=True, exclude_none=True) for e in elicitations]},
|
||||
)
|
||||
super().__init__(error)
|
||||
|
||||
@property
|
||||
def elicitations(self) -> list[ElicitRequestURLParams]:
|
||||
|
|
|
|||
|
|
@ -7,13 +7,8 @@ the main session code clean.
|
|||
WARNING: These APIs are experimental and may change without notice.
|
||||
"""
|
||||
|
||||
from mcp.shared.exceptions import McpError
|
||||
from mcp.types import (
|
||||
INVALID_REQUEST,
|
||||
ClientCapabilities,
|
||||
ClientTasksCapability,
|
||||
ErrorData,
|
||||
)
|
||||
from mcp.shared.exceptions import MCPError
|
||||
from mcp.types import INVALID_REQUEST, ClientCapabilities, ClientTasksCapability
|
||||
|
||||
|
||||
def check_tasks_capability(
|
||||
|
|
@ -76,36 +71,26 @@ def has_task_augmented_sampling(caps: ClientCapabilities) -> bool:
|
|||
|
||||
|
||||
def require_task_augmented_elicitation(client_caps: ClientCapabilities | None) -> None:
|
||||
"""Raise McpError if client doesn't support task-augmented elicitation.
|
||||
"""Raise MCPError if client doesn't support task-augmented elicitation.
|
||||
|
||||
Args:
|
||||
client_caps: The client's declared capabilities, or None if not initialized
|
||||
|
||||
Raises:
|
||||
McpError: If client doesn't support task-augmented elicitation
|
||||
MCPError: If client doesn't support task-augmented elicitation
|
||||
"""
|
||||
if client_caps is None or not has_task_augmented_elicitation(client_caps):
|
||||
raise McpError(
|
||||
ErrorData(
|
||||
code=INVALID_REQUEST,
|
||||
message="Client does not support task-augmented elicitation",
|
||||
)
|
||||
)
|
||||
raise MCPError(code=INVALID_REQUEST, message="Client does not support task-augmented elicitation")
|
||||
|
||||
|
||||
def require_task_augmented_sampling(client_caps: ClientCapabilities | None) -> None:
|
||||
"""Raise McpError if client doesn't support task-augmented sampling.
|
||||
"""Raise MCPError if client doesn't support task-augmented sampling.
|
||||
|
||||
Args:
|
||||
client_caps: The client's declared capabilities, or None if not initialized
|
||||
|
||||
Raises:
|
||||
McpError: If client doesn't support task-augmented sampling
|
||||
MCPError: If client doesn't support task-augmented sampling
|
||||
"""
|
||||
if client_caps is None or not has_task_augmented_sampling(client_caps):
|
||||
raise McpError(
|
||||
ErrorData(
|
||||
code=INVALID_REQUEST,
|
||||
message="Client does not support task-augmented sampling",
|
||||
)
|
||||
)
|
||||
raise MCPError(code=INVALID_REQUEST, message="Client does not support task-augmented sampling")
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ from contextlib import asynccontextmanager
|
|||
from datetime import datetime, timezone
|
||||
from uuid import uuid4
|
||||
|
||||
from mcp.shared.exceptions import McpError
|
||||
from mcp.shared.exceptions import MCPError
|
||||
from mcp.shared.experimental.tasks.context import TaskContext
|
||||
from mcp.shared.experimental.tasks.store import TaskStore
|
||||
from mcp.types import (
|
||||
|
|
@ -19,7 +19,6 @@ from mcp.types import (
|
|||
TASK_STATUS_FAILED,
|
||||
TASK_STATUS_WORKING,
|
||||
CancelTaskResult,
|
||||
ErrorData,
|
||||
Task,
|
||||
TaskMetadata,
|
||||
TaskStatus,
|
||||
|
|
@ -68,7 +67,7 @@ async def cancel_task(
|
|||
CancelTaskResult with the cancelled task state
|
||||
|
||||
Raises:
|
||||
McpError: With INVALID_PARAMS (-32602) if:
|
||||
MCPError: With INVALID_PARAMS (-32602) if:
|
||||
- Task does not exist
|
||||
- Task is already in a terminal state (completed, failed, cancelled)
|
||||
|
||||
|
|
@ -79,20 +78,10 @@ async def cancel_task(
|
|||
"""
|
||||
task = await store.get_task(task_id)
|
||||
if task is None:
|
||||
raise McpError(
|
||||
ErrorData(
|
||||
code=INVALID_PARAMS,
|
||||
message=f"Task not found: {task_id}",
|
||||
)
|
||||
)
|
||||
raise MCPError(code=INVALID_PARAMS, message=f"Task not found: {task_id}")
|
||||
|
||||
if is_terminal(task.status):
|
||||
raise McpError(
|
||||
ErrorData(
|
||||
code=INVALID_PARAMS,
|
||||
message=f"Cannot cancel task in terminal state '{task.status}'",
|
||||
)
|
||||
)
|
||||
raise MCPError(code=INVALID_PARAMS, message=f"Cannot cancel task in terminal state '{task.status}'")
|
||||
|
||||
# Update task to cancelled status
|
||||
cancelled_task = await store.update_task(task_id, status=TASK_STATUS_CANCELLED)
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStre
|
|||
from pydantic import BaseModel, TypeAdapter
|
||||
from typing_extensions import Self
|
||||
|
||||
from mcp.shared.exceptions import McpError
|
||||
from mcp.shared.exceptions import MCPError
|
||||
from mcp.shared.message import MessageMetadata, ServerMessageMetadata, SessionMessage
|
||||
from mcp.shared.response_router import ResponseRouter
|
||||
from mcp.types import (
|
||||
|
|
@ -237,7 +237,7 @@ class BaseSession(
|
|||
) -> ReceiveResultT:
|
||||
"""Sends a request and wait for a response.
|
||||
|
||||
Raises an McpError if the response contains an error. If a request read timeout is provided, it will take
|
||||
Raises an MCPError if the response contains an error. If a request read timeout is provided, it will take
|
||||
precedence over the session read timeout.
|
||||
|
||||
Do not use this method to emit notifications! Use send_notification() instead.
|
||||
|
|
@ -271,18 +271,12 @@ class BaseSession(
|
|||
with anyio.fail_after(timeout):
|
||||
response_or_error = await response_stream_reader.receive()
|
||||
except TimeoutError:
|
||||
raise McpError(
|
||||
ErrorData(
|
||||
code=REQUEST_TIMEOUT,
|
||||
message=(
|
||||
f"Timed out while waiting for response to {request.__class__.__name__}. "
|
||||
f"Waited {timeout} seconds."
|
||||
),
|
||||
)
|
||||
)
|
||||
class_name = request.__class__.__name__
|
||||
message = f"Timed out while waiting for response to {class_name}. Waited {timeout} seconds."
|
||||
raise MCPError(code=REQUEST_TIMEOUT, message=message)
|
||||
|
||||
if isinstance(response_or_error, JSONRPCError):
|
||||
raise McpError(response_or_error.error)
|
||||
raise MCPError.from_jsonrpc_error(response_or_error)
|
||||
else:
|
||||
return result_type.model_validate(response_or_error.result, by_name=False)
|
||||
|
||||
|
|
|
|||
|
|
@ -1654,8 +1654,8 @@ class ElicitRequestURLParams(RequestParams):
|
|||
"""The URL that the user should navigate to."""
|
||||
|
||||
elicitation_id: str
|
||||
"""
|
||||
The ID of the elicitation, which must be unique within the context of the server.
|
||||
"""The ID of the elicitation, which must be unique within the context of the server.
|
||||
|
||||
The client MUST treat this ID as an opaque value.
|
||||
"""
|
||||
|
||||
|
|
|
|||
|
|
@ -59,15 +59,15 @@ class ErrorData(BaseModel):
|
|||
"""The error type that occurred."""
|
||||
|
||||
message: str
|
||||
"""
|
||||
A short description of the error. The message SHOULD be limited to a concise single
|
||||
sentence.
|
||||
"""A short description of the error.
|
||||
|
||||
The message SHOULD be limited to a concise single sentence.
|
||||
"""
|
||||
|
||||
data: Any = None
|
||||
"""
|
||||
Additional information about the error. The value of this member is defined by the
|
||||
sender (e.g. detailed error information, nested errors etc.).
|
||||
"""Additional information about the error.
|
||||
|
||||
The value of this member is defined by the sender (e.g. detailed error information, nested errors, etc.).
|
||||
"""
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ from mcp.client.session_group import (
|
|||
StreamableHttpParameters,
|
||||
)
|
||||
from mcp.client.stdio import StdioServerParameters
|
||||
from mcp.shared.exceptions import McpError
|
||||
from mcp.shared.exceptions import MCPError
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
|
@ -225,7 +225,7 @@ async def test_client_session_group_disconnect_from_server():
|
|||
async def test_client_session_group_connect_to_server_duplicate_tool_raises_error(
|
||||
mock_exit_stack: contextlib.AsyncExitStack,
|
||||
):
|
||||
"""Test McpError raised when connecting a server with a dup name."""
|
||||
"""Test MCPError raised when connecting a server with a dup name."""
|
||||
# --- Setup Pre-existing State ---
|
||||
group = ClientSessionGroup(exit_stack=mock_exit_stack)
|
||||
existing_tool_name = "shared_tool"
|
||||
|
|
@ -251,7 +251,7 @@ async def test_client_session_group_connect_to_server_duplicate_tool_raises_erro
|
|||
mock_session_new.list_prompts.return_value = mock.AsyncMock(prompts=[])
|
||||
|
||||
# --- Test Execution and Assertion ---
|
||||
with pytest.raises(McpError) as excinfo:
|
||||
with pytest.raises(MCPError) as excinfo:
|
||||
with mock.patch.object(
|
||||
group,
|
||||
"_establish_session",
|
||||
|
|
@ -274,7 +274,7 @@ async def test_client_session_group_disconnect_non_existent_server():
|
|||
"""Test disconnecting a server that isn't connected."""
|
||||
session = mock.Mock(spec=mcp.ClientSession)
|
||||
group = ClientSessionGroup()
|
||||
with pytest.raises(McpError):
|
||||
with pytest.raises(MCPError):
|
||||
await group.disconnect_from_server(session)
|
||||
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue