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:
Marcelo Trylesinski 2026-01-26 14:37:44 +01:00 • committed by GitHub
commit acba5478a9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 253 additions and 326 deletions

View file

@ -337,7 +337,7 @@ if __name__ == "__main__":
Handle task errors gracefully: Handle task errors gracefully:
```python ```python
from mcp.shared.exceptions import McpError from mcp.shared.exceptions import MCPError
try: try:
result = await session.experimental.call_tool_as_task("my_tool", args) 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) final = await session.experimental.get_task_result(task_id, CallToolResult)
except McpError as e: except MCPError as e:
print(f"MCP error: {e.error.message}") print(f"MCP error: {e.message}")
except Exception as e: except Exception as e:
print(f"Error: {e}") print(f"Error: {e}")
``` ```

View file

@ -121,6 +121,38 @@ result = await session.list_resources(params=PaginatedRequestParams(cursor="next
result = await session.list_tools(params=PaginatedRequestParams(cursor="next_page_token")) 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` ### `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. 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.

View file

@ -33,7 +33,7 @@ from urllib.parse import urlparse
from mcp import ClientSession, types from mcp import ClientSession, types
from mcp.client.sse import sse_client from mcp.client.sse import sse_client
from mcp.shared.context import RequestContext 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 from mcp.types import URL_ELICITATION_REQUIRED
@ -160,9 +160,9 @@ async def call_tool_with_error_handling(
return result return result
except McpError as e: except MCPError as e:
# Check if this is a URL elicitation required error # 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]") print("\n[Tool requires URL elicitation to proceed]")
# Convert to typed error to access elicitations # Convert to typed error to access elicitations

View file

@ -4,7 +4,7 @@ from .client.session_group import ClientSessionGroup
from .client.stdio import StdioServerParameters, stdio_client from .client.stdio import StdioServerParameters, stdio_client
from .server.session import ServerSession from .server.session import ServerSession
from .server.stdio import stdio_server from .server.stdio import stdio_server
from .shared.exceptions import McpError, UrlElicitationRequiredError from .shared.exceptions import MCPError, UrlElicitationRequiredError
from .types import ( from .types import (
CallToolRequest, CallToolRequest,
ClientCapabilities, ClientCapabilities,
@ -96,7 +96,7 @@ __all__ = [
"ListToolsResult", "ListToolsResult",
"LoggingLevel", "LoggingLevel",
"LoggingMessageNotification", "LoggingMessageNotification",
"McpError", "MCPError",
"Notification", "Notification",
"PingRequest", "PingRequest",
"ProgressNotification", "ProgressNotification",

View file

@ -25,7 +25,7 @@ from mcp.client.sse import sse_client
from mcp.client.stdio import StdioServerParameters from mcp.client.stdio import StdioServerParameters
from mcp.client.streamable_http import streamable_http_client from mcp.client.streamable_http import streamable_http_client
from mcp.shared._httpx_utils import create_mcp_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 from mcp.shared.session import ProgressFnT
@ -216,11 +216,9 @@ class ClientSessionGroup:
session_known_for_stack = session in self._session_exit_stacks session_known_for_stack = session in self._session_exit_stacks
if not session_known_for_components and not session_known_for_stack: if not session_known_for_components and not session_known_for_stack:
raise McpError( raise MCPError(
types.ErrorData( code=types.INVALID_PARAMS,
code=types.INVALID_PARAMS, message="Provided session is not managed or already disconnected.",
message="Provided session is not managed or already disconnected.",
)
) )
if session_known_for_components: # pragma: no branch if session_known_for_components: # pragma: no branch
@ -352,7 +350,7 @@ class ClientSessionGroup:
name = self._component_name(prompt.name, server_info) name = self._component_name(prompt.name, server_info)
prompts_temp[name] = prompt prompts_temp[name] = prompt
component_names.prompts.add(name) 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}") logging.warning(f"Could not fetch prompts: {err}")
# Query the server for its resources and aggregate to list. # Query the server for its resources and aggregate to list.
@ -362,7 +360,7 @@ class ClientSessionGroup:
name = self._component_name(resource.name, server_info) name = self._component_name(resource.name, server_info)
resources_temp[name] = resource resources_temp[name] = resource
component_names.resources.add(name) 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}") logging.warning(f"Could not fetch resources: {err}")
# Query the server for its tools and aggregate to list. # Query the server for its tools and aggregate to list.
@ -373,7 +371,7 @@ class ClientSessionGroup:
tools_temp[name] = tool tools_temp[name] = tool
tool_to_session_temp[name] = session tool_to_session_temp[name] = session
component_names.tools.add(name) 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}") logging.warning(f"Could not fetch tools: {err}")
# Clean up exit stack for session if we couldn't retrieve anything # Clean up exit stack for session if we couldn't retrieve anything
@ -384,28 +382,19 @@ class ClientSessionGroup:
# Check for duplicates. # Check for duplicates.
matching_prompts = prompts_temp.keys() & self._prompts.keys() matching_prompts = prompts_temp.keys() & self._prompts.keys()
if matching_prompts: if matching_prompts:
raise McpError( # pragma: no cover raise MCPError( # pragma: no cover
types.ErrorData( code=types.INVALID_PARAMS,
code=types.INVALID_PARAMS, message=f"{matching_prompts} already exist in group prompts.",
message=f"{matching_prompts} already exist in group prompts.",
)
) )
matching_resources = resources_temp.keys() & self._resources.keys() matching_resources = resources_temp.keys() & self._resources.keys()
if matching_resources: if matching_resources:
raise McpError( # pragma: no cover raise MCPError( # pragma: no cover
types.ErrorData( code=types.INVALID_PARAMS,
code=types.INVALID_PARAMS, message=f"{matching_resources} already exist in group resources.",
message=f"{matching_resources} already exist in group resources.",
)
) )
matching_tools = tools_temp.keys() & self._tools.keys() matching_tools = tools_temp.keys() & self._tools.keys()
if matching_tools: if matching_tools:
raise McpError( raise MCPError(code=types.INVALID_PARAMS, message=f"{matching_tools} already exist in group tools.")
types.ErrorData(
code=types.INVALID_PARAMS,
message=f"{matching_tools} already exist in group tools.",
)
)
# Aggregate components. # Aggregate components.
self._sessions[session] = component_names self._sessions[session] = component_names

View file

@ -13,7 +13,7 @@ from typing import Any
from mcp.server.experimental.task_context import ServerTaskContext from mcp.server.experimental.task_context import ServerTaskContext
from mcp.server.experimental.task_support import TaskSupport from mcp.server.experimental.task_support import TaskSupport
from mcp.server.session import ServerSession 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.shared.experimental.tasks.helpers import MODEL_IMMEDIATE_RESPONSE_KEY, is_terminal
from mcp.types import ( from mcp.types import (
METHOD_NOT_FOUND, METHOD_NOT_FOUND,
@ -72,13 +72,13 @@ class Experimental:
Args: Args:
tool_task_mode: The tool's execution.taskSupport value tool_task_mode: The tool's execution.taskSupport value
("forbidden", "optional", "required", or None) ("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: Returns:
None if valid, ErrorData if invalid and raise_error=False None if valid, ErrorData if invalid and raise_error=False
Raises: Raises:
McpError: If invalid and raise_error=True MCPError: If invalid and raise_error=True
""" """
mode = tool_task_mode or TASK_FORBIDDEN mode = tool_task_mode or TASK_FORBIDDEN
@ -86,18 +86,12 @@ class Experimental:
error: ErrorData | None = None error: ErrorData | None = None
if mode == TASK_REQUIRED and not self.is_task: if mode == TASK_REQUIRED and not self.is_task:
error = ErrorData( error = ErrorData(code=METHOD_NOT_FOUND, message="This tool requires task-augmented invocation")
code=METHOD_NOT_FOUND,
message="This tool requires task-augmented invocation",
)
elif mode == TASK_FORBIDDEN and self.is_task: elif mode == TASK_FORBIDDEN and self.is_task:
error = ErrorData( error = ErrorData(code=METHOD_NOT_FOUND, message="This tool does not support task-augmented invocation")
code=METHOD_NOT_FOUND,
message="This tool does not support task-augmented invocation",
)
if error is not None and raise_error: if error is not None and raise_error:
raise McpError(error) raise MCPError(code=error.code, message=error.message)
return error return error
@ -113,7 +107,7 @@ class Experimental:
Args: Args:
tool: The Tool definition tool: The Tool definition
raise_error: If True, raises McpError on validation failure. raise_error: If True, raises MCPError on validation failure.
Returns: Returns:
None if valid, ErrorData if invalid and raise_error=False None if valid, ErrorData if invalid and raise_error=False

View file

@ -114,7 +114,7 @@ class ExperimentalServerSessionFeatures:
The client's elicitation response The client's elicitation response
Raises: 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 client_caps = self._session.client_params.capabilities if self._session.client_params else None
require_task_augmented_elicitation(client_caps) require_task_augmented_elicitation(client_caps)
@ -174,7 +174,7 @@ class ExperimentalServerSessionFeatures:
The sampling result from the client The sampling result from the client
Raises: 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 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 client_caps = self._session.client_params.capabilities if self._session.client_params else None

View file

@ -13,7 +13,7 @@ import anyio
from mcp.server.experimental.task_result_handler import TaskResultHandler from mcp.server.experimental.task_result_handler import TaskResultHandler
from mcp.server.session import ServerSession from mcp.server.session import ServerSession
from mcp.server.validation import validate_sampling_tools, validate_tool_use_result_messages 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 ( from mcp.shared.experimental.tasks.capabilities import (
require_task_augmented_elicitation, require_task_augmented_elicitation,
require_task_augmented_sampling, require_task_augmented_sampling,
@ -32,7 +32,6 @@ from mcp.types import (
ElicitationCapability, ElicitationCapability,
ElicitRequestedSchema, ElicitRequestedSchema,
ElicitResult, ElicitResult,
ErrorData,
IncludeContext, IncludeContext,
ModelPreferences, ModelPreferences,
RequestId, RequestId,
@ -173,22 +172,12 @@ class ServerTaskContext:
def _check_elicitation_capability(self) -> None: def _check_elicitation_capability(self) -> None:
"""Check if the client supports elicitation.""" """Check if the client supports elicitation."""
if not self._session.check_client_capability(ClientCapabilities(elicitation=ElicitationCapability())): if not self._session.check_client_capability(ClientCapabilities(elicitation=ElicitationCapability())):
raise McpError( raise MCPError(code=INVALID_REQUEST, message="Client does not support elicitation capability")
ErrorData(
code=INVALID_REQUEST,
message="Client does not support elicitation capability",
)
)
def _check_sampling_capability(self) -> None: def _check_sampling_capability(self) -> None:
"""Check if the client supports sampling.""" """Check if the client supports sampling."""
if not self._session.check_client_capability(ClientCapabilities(sampling=SamplingCapability())): if not self._session.check_client_capability(ClientCapabilities(sampling=SamplingCapability())):
raise McpError( raise MCPError(code=INVALID_REQUEST, message="Client does not support sampling capability")
ErrorData(
code=INVALID_REQUEST,
message="Client does not support sampling capability",
)
)
async def elicit( async def elicit(
self, self,
@ -213,7 +202,7 @@ class ServerTaskContext:
The client's response The client's response
Raises: Raises:
McpError: If client doesn't support elicitation capability MCPError: If client doesn't support elicitation capability
""" """
self._check_elicitation_capability() self._check_elicitation_capability()
@ -281,7 +270,7 @@ class ServerTaskContext:
The client's response indicating acceptance, decline, or cancellation The client's response indicating acceptance, decline, or cancellation
Raises: Raises:
McpError: If client doesn't support elicitation capability MCPError: If client doesn't support elicitation capability
RuntimeError: If handler is not configured RuntimeError: If handler is not configured
""" """
self._check_elicitation_capability() self._check_elicitation_capability()
@ -361,7 +350,7 @@ class ServerTaskContext:
The sampling result from the client The sampling result from the client
Raises: 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 ValueError: If tool_use or tool_result message structure is invalid
""" """
self._check_sampling_capability() self._check_sampling_capability()
@ -436,7 +425,7 @@ class ServerTaskContext:
The client's elicitation response The client's elicitation response
Raises: 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 RuntimeError: If handler is not configured
""" """
client_caps = self._session.client_params.capabilities if self._session.client_params else None 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 The sampling result from the client
Raises: 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 ValueError: If tool_use or tool_result message structure is invalid
RuntimeError: If handler is not configured RuntimeError: If handler is not configured
""" """

View file

@ -15,7 +15,7 @@ from typing import Any
import anyio import anyio
from mcp.server.session import ServerSession 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.helpers import RELATED_TASK_METADATA_KEY, is_terminal
from mcp.shared.experimental.tasks.message_queue import TaskMessageQueue from mcp.shared.experimental.tasks.message_queue import TaskMessageQueue
from mcp.shared.experimental.tasks.resolver import Resolver from mcp.shared.experimental.tasks.resolver import Resolver
@ -106,7 +106,7 @@ class TaskResultHandler:
while True: while True:
task = await self._store.get_task(task_id) task = await self._store.get_task(task_id)
if task is None: 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) await self._deliver_queued_messages(task_id, session, request_id)
@ -216,6 +216,6 @@ class TaskResultHandler:
""" """
resolver = self._pending_requests.pop(request_id, None) resolver = self._pending_requests.pop(request_id, None)
if resolver is not None and not resolver.done(): 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 True
return False return False

View file

@ -11,7 +11,7 @@ from typing import TYPE_CHECKING
from mcp.server.experimental.task_support import TaskSupport from mcp.server.experimental.task_support import TaskSupport
from mcp.server.lowlevel.func_inspection import create_call_wrapper 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.helpers import cancel_task
from mcp.shared.experimental.tasks.in_memory_task_store import InMemoryTaskStore from mcp.shared.experimental.tasks.in_memory_task_store import InMemoryTaskStore
from mcp.shared.experimental.tasks.message_queue import InMemoryTaskMessageQueue, TaskMessageQueue from mcp.shared.experimental.tasks.message_queue import InMemoryTaskMessageQueue, TaskMessageQueue
@ -20,7 +20,6 @@ from mcp.types import (
INVALID_PARAMS, INVALID_PARAMS,
CancelTaskRequest, CancelTaskRequest,
CancelTaskResult, CancelTaskResult,
ErrorData,
GetTaskPayloadRequest, GetTaskPayloadRequest,
GetTaskPayloadResult, GetTaskPayloadResult,
GetTaskRequest, GetTaskRequest,
@ -135,12 +134,7 @@ class ExperimentalHandlers:
async def _default_get_task(req: GetTaskRequest) -> ServerResult: async def _default_get_task(req: GetTaskRequest) -> ServerResult:
task = await support.store.get_task(req.params.task_id) task = await support.store.get_task(req.params.task_id)
if task is None: if task is None:
raise McpError( raise MCPError(code=INVALID_PARAMS, message=f"Task not found: {req.params.task_id}")
ErrorData(
code=INVALID_PARAMS,
message=f"Task not found: {req.params.task_id}",
)
)
return GetTaskResult( return GetTaskResult(
task_id=task.task_id, task_id=task.task_id,
status=task.status, status=task.status,

View file

@ -101,7 +101,7 @@ from mcp.server.streamable_http import EventStore
from mcp.server.streamable_http_manager import StreamableHTTPASGIApp, StreamableHTTPSessionManager from mcp.server.streamable_http_manager import StreamableHTTPASGIApp, StreamableHTTPSessionManager
from mcp.server.transport_security import TransportSecuritySettings from mcp.server.transport_security import TransportSecuritySettings
from mcp.shared.context import RequestContext 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.message import ServerMessageMetadata, SessionMessage
from mcp.shared.session import RequestResponder from mcp.shared.session import RequestResponder
from mcp.shared.tool_name_validation import validate_and_warn_tool_name 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 case _: # pragma: no cover
raise ValueError(f"Unexpected return type from read_resource: {type(result)}") raise ValueError(f"Unexpected return type from read_resource: {type(result)}")
return types.ReadResourceResult( # pragma: no cover return types.ReadResourceResult(contents=[content]) # pragma: no cover
contents=[content],
)
self.request_handlers[types.ReadResourceRequest] = handler self.request_handlers[types.ReadResourceRequest] = handler
return func return func
@ -781,13 +779,10 @@ class Server(Generic[LifespanResultT, RequestT]):
) )
) )
response = await handler(req) response = await handler(req)
except McpError as err: except MCPError as err:
response = err.error response = err.error
except anyio.get_cancelled_exc_class(): except anyio.get_cancelled_exc_class():
logger.info( logger.info("Request %s cancelled - duplicate response suppressed", message.request_id)
"Request %s cancelled - duplicate response suppressed",
message.request_id,
)
return return
except Exception as err: except Exception as err:
if raise_exceptions: # pragma: no cover if raise_exceptions: # pragma: no cover

View file

@ -314,7 +314,7 @@ class ServerSession(
The sampling result from the client. The sampling result from the client.
Raises: 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. ValueError: If tool_use or tool_result message structure is invalid.
StatelessModeNotSupported: If called in stateless HTTP mode. StatelessModeNotSupported: If called in stateless HTTP mode.
""" """

View file

@ -4,15 +4,8 @@ This module provides validation logic for sampling and elicitation requests
that is shared across normal and task-augmented code paths. that is shared across normal and task-augmented code paths.
""" """
from mcp.shared.exceptions import McpError from mcp.shared.exceptions import MCPError
from mcp.types import ( from mcp.types import INVALID_PARAMS, ClientCapabilities, SamplingMessage, Tool, ToolChoice
INVALID_PARAMS,
ClientCapabilities,
ErrorData,
SamplingMessage,
Tool,
ToolChoice,
)
def check_sampling_tools_capability(client_caps: ClientCapabilities | None) -> bool: 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 tool_choice: The tool choice setting, if provided
Raises: 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 tools is not None or tool_choice is not None:
if not check_sampling_tools_capability(client_caps): 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: def validate_tool_use_result_messages(messages: list[SamplingMessage]) -> None:

View file

@ -2,18 +2,40 @@ from __future__ import annotations
from typing import Any, cast 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.""" """Exception type raised when an error arrives over an MCP connection."""
error: ErrorData error: ErrorData
def __init__(self, error: ErrorData): def __init__(self, code: int, message: str, data: Any = None):
"""Initialize McpError.""" super().__init__(code, message, data)
super().__init__(error.message) self.error = ErrorData(code=code, message=message, data=data)
self.error = error
@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): class StatelessModeNotSupported(RuntimeError):
@ -33,7 +55,7 @@ class StatelessModeNotSupported(RuntimeError):
self.method = method self.method = method
class UrlElicitationRequiredError(McpError): class UrlElicitationRequiredError(MCPError):
"""Specialized error for when a tool requires URL mode elicitation(s) before proceeding. """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 Servers can raise this error from tool handlers to indicate that the client
@ -42,7 +64,6 @@ class UrlElicitationRequiredError(McpError):
Example: Example:
raise UrlElicitationRequiredError([ raise UrlElicitationRequiredError([
ElicitRequestURLParams( ElicitRequestURLParams(
mode="url",
message="Authorization required for your files", message="Authorization required for your files",
url="https://example.com/oauth/authorize", url="https://example.com/oauth/authorize",
elicitation_id="auth-001" elicitation_id="auth-001"
@ -50,23 +71,18 @@ class UrlElicitationRequiredError(McpError):
]) ])
""" """
def __init__( def __init__(self, elicitations: list[ElicitRequestURLParams], message: str | None = None):
self,
elicitations: list[ElicitRequestURLParams],
message: str | None = None,
):
"""Initialize UrlElicitationRequiredError.""" """Initialize UrlElicitationRequiredError."""
if message is None: if message is None:
message = f"URL elicitation{'s' if len(elicitations) > 1 else ''} required" message = f"URL elicitation{'s' if len(elicitations) > 1 else ''} required"
self._elicitations = elicitations self._elicitations = elicitations
error = ErrorData( super().__init__(
code=URL_ELICITATION_REQUIRED, code=URL_ELICITATION_REQUIRED,
message=message, message=message,
data={"elicitations": [e.model_dump(by_alias=True, exclude_none=True) for e in elicitations]}, data={"elicitations": [e.model_dump(by_alias=True, exclude_none=True) for e in elicitations]},
) )
super().__init__(error)
@property @property
def elicitations(self) -> list[ElicitRequestURLParams]: def elicitations(self) -> list[ElicitRequestURLParams]:

View file

@ -7,13 +7,8 @@ the main session code clean.
WARNING: These APIs are experimental and may change without notice. WARNING: These APIs are experimental and may change without notice.
""" """
from mcp.shared.exceptions import McpError from mcp.shared.exceptions import MCPError
from mcp.types import ( from mcp.types import INVALID_REQUEST, ClientCapabilities, ClientTasksCapability
INVALID_REQUEST,
ClientCapabilities,
ClientTasksCapability,
ErrorData,
)
def check_tasks_capability( 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: 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: Args:
client_caps: The client's declared capabilities, or None if not initialized client_caps: The client's declared capabilities, or None if not initialized
Raises: 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): if client_caps is None or not has_task_augmented_elicitation(client_caps):
raise McpError( raise MCPError(code=INVALID_REQUEST, message="Client does not support task-augmented elicitation")
ErrorData(
code=INVALID_REQUEST,
message="Client does not support task-augmented elicitation",
)
)
def require_task_augmented_sampling(client_caps: ClientCapabilities | None) -> None: 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: Args:
client_caps: The client's declared capabilities, or None if not initialized client_caps: The client's declared capabilities, or None if not initialized
Raises: 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): if client_caps is None or not has_task_augmented_sampling(client_caps):
raise McpError( raise MCPError(code=INVALID_REQUEST, message="Client does not support task-augmented sampling")
ErrorData(
code=INVALID_REQUEST,
message="Client does not support task-augmented sampling",
)
)

View file

@ -9,7 +9,7 @@ from contextlib import asynccontextmanager
from datetime import datetime, timezone from datetime import datetime, timezone
from uuid import uuid4 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.context import TaskContext
from mcp.shared.experimental.tasks.store import TaskStore from mcp.shared.experimental.tasks.store import TaskStore
from mcp.types import ( from mcp.types import (
@ -19,7 +19,6 @@ from mcp.types import (
TASK_STATUS_FAILED, TASK_STATUS_FAILED,
TASK_STATUS_WORKING, TASK_STATUS_WORKING,
CancelTaskResult, CancelTaskResult,
ErrorData,
Task, Task,
TaskMetadata, TaskMetadata,
TaskStatus, TaskStatus,
@ -68,7 +67,7 @@ async def cancel_task(
CancelTaskResult with the cancelled task state CancelTaskResult with the cancelled task state
Raises: Raises:
McpError: With INVALID_PARAMS (-32602) if: MCPError: With INVALID_PARAMS (-32602) if:
- Task does not exist - Task does not exist
- Task is already in a terminal state (completed, failed, cancelled) - 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) task = await store.get_task(task_id)
if task is None: if task is None:
raise McpError( raise MCPError(code=INVALID_PARAMS, message=f"Task not found: {task_id}")
ErrorData(
code=INVALID_PARAMS,
message=f"Task not found: {task_id}",
)
)
if is_terminal(task.status): if is_terminal(task.status):
raise McpError( raise MCPError(code=INVALID_PARAMS, message=f"Cannot cancel task in terminal state '{task.status}'")
ErrorData(
code=INVALID_PARAMS,
message=f"Cannot cancel task in terminal state '{task.status}'",
)
)
# Update task to cancelled status # Update task to cancelled status
cancelled_task = await store.update_task(task_id, status=TASK_STATUS_CANCELLED) cancelled_task = await store.update_task(task_id, status=TASK_STATUS_CANCELLED)

View file

@ -11,7 +11,7 @@ from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStre
from pydantic import BaseModel, TypeAdapter from pydantic import BaseModel, TypeAdapter
from typing_extensions import Self 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.message import MessageMetadata, ServerMessageMetadata, SessionMessage
from mcp.shared.response_router import ResponseRouter from mcp.shared.response_router import ResponseRouter
from mcp.types import ( from mcp.types import (
@ -237,7 +237,7 @@ class BaseSession(
) -> ReceiveResultT: ) -> ReceiveResultT:
"""Sends a request and wait for a response. """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. precedence over the session read timeout.
Do not use this method to emit notifications! Use send_notification() instead. Do not use this method to emit notifications! Use send_notification() instead.
@ -271,18 +271,12 @@ class BaseSession(
with anyio.fail_after(timeout): with anyio.fail_after(timeout):
response_or_error = await response_stream_reader.receive() response_or_error = await response_stream_reader.receive()
except TimeoutError: except TimeoutError:
raise McpError( class_name = request.__class__.__name__
ErrorData( message = f"Timed out while waiting for response to {class_name}. Waited {timeout} seconds."
code=REQUEST_TIMEOUT, raise MCPError(code=REQUEST_TIMEOUT, message=message)
message=(
f"Timed out while waiting for response to {request.__class__.__name__}. "
f"Waited {timeout} seconds."
),
)
)
if isinstance(response_or_error, JSONRPCError): if isinstance(response_or_error, JSONRPCError):
raise McpError(response_or_error.error) raise MCPError.from_jsonrpc_error(response_or_error)
else: else:
return result_type.model_validate(response_or_error.result, by_name=False) return result_type.model_validate(response_or_error.result, by_name=False)

View file

@ -1654,8 +1654,8 @@ class ElicitRequestURLParams(RequestParams):
"""The URL that the user should navigate to.""" """The URL that the user should navigate to."""
elicitation_id: str 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. The client MUST treat this ID as an opaque value.
""" """

View file

@ -59,15 +59,15 @@ class ErrorData(BaseModel):
"""The error type that occurred.""" """The error type that occurred."""
message: str message: str
""" """A short description of the error.
A short description of the error. The message SHOULD be limited to a concise single
sentence. The message SHOULD be limited to a concise single sentence.
""" """
data: Any = None data: Any = None
""" """Additional information about the error.
Additional information about the error. The value of this member is defined by the
sender (e.g. detailed error information, nested errors etc.). The value of this member is defined by the sender (e.g. detailed error information, nested errors, etc.).
""" """

View file

@ -13,7 +13,7 @@ from mcp.client.session_group import (
StreamableHttpParameters, StreamableHttpParameters,
) )
from mcp.client.stdio import StdioServerParameters from mcp.client.stdio import StdioServerParameters
from mcp.shared.exceptions import McpError from mcp.shared.exceptions import MCPError
@pytest.fixture @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( async def test_client_session_group_connect_to_server_duplicate_tool_raises_error(
mock_exit_stack: contextlib.AsyncExitStack, 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 --- # --- Setup Pre-existing State ---
group = ClientSessionGroup(exit_stack=mock_exit_stack) group = ClientSessionGroup(exit_stack=mock_exit_stack)
existing_tool_name = "shared_tool" 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=[]) mock_session_new.list_prompts.return_value = mock.AsyncMock(prompts=[])
# --- Test Execution and Assertion --- # --- Test Execution and Assertion ---
with pytest.raises(McpError) as excinfo: with pytest.raises(MCPError) as excinfo:
with mock.patch.object( with mock.patch.object(
group, group,
"_establish_session", "_establish_session",
@ -274,7 +274,7 @@ async def test_client_session_group_disconnect_non_existent_server():
"""Test disconnecting a server that isn't connected.""" """Test disconnecting a server that isn't connected."""
session = mock.Mock(spec=mcp.ClientSession) session = mock.Mock(spec=mcp.ClientSession)
group = ClientSessionGroup() group = ClientSessionGroup()
with pytest.raises(McpError): with pytest.raises(MCPError):
await group.disconnect_from_server(session) await group.disconnect_from_server(session)

Some files were not shown because too many files have changed in this diff Show more