diff --git a/app/en/references/mcp/python/errors/page.mdx b/app/en/references/mcp/python/errors/page.mdx index ba240f2e7..308b676d5 100644 --- a/app/en/references/mcp/python/errors/page.mdx +++ b/app/en/references/mcp/python/errors/page.mdx @@ -3,6 +3,8 @@ title: "Errors - Arcade MCP Python Reference" description: "Domain-specific error types raised by the MCP server and components" --- +import { Callout } from "nextra/components"; + # Errors Domain-specific error types raised by the MCP server and components. @@ -103,35 +105,162 @@ Error in session management. Error in transport layer (stdio, HTTP, etc). +## `arcade_core.errors` + +Tool execution errors with retry semantics. Re-exported via `arcade_mcp_server.exceptions`. + +### `RetryableToolError` + +**Bases:** `ToolExecutionError` + +The operation failed but can be retried. + +```python +from arcade_mcp_server.exceptions import RetryableToolError + +# Simple retry +raise RetryableToolError("Service temporarily unavailable") + +# With retry delay +raise RetryableToolError( + "Rate limited", + retry_after_ms=5000 +) + +# With guidance for the AI +raise RetryableToolError( + "Search returned no results", + additional_prompt_content="Try broader search terms or check spelling." +) +``` + +### `FatalToolError` + +**Bases:** `ToolExecutionError` + +Unrecoverable error — the AI should not retry. + +```python +from arcade_mcp_server.exceptions import FatalToolError + +raise FatalToolError("Account has been permanently deleted") + +# With developer-only details +raise FatalToolError( + "Configuration error", + developer_message="Missing required API key in environment" +) +``` + +### `ContextRequiredToolError` + +**Bases:** `ToolExecutionError` + +The operation needs additional context from the user before it can proceed. + +```python +from arcade_mcp_server.exceptions import ContextRequiredToolError + +raise ContextRequiredToolError( + "Multiple users found matching 'John'", + additional_prompt_content="Please specify: John Smith (john@work.com) or John Doe (john@home.com)" +) +``` + +### `UpstreamError` + +**Bases:** `ToolExecutionError` + +Error from an external API or service. + +```python +from arcade_mcp_server.exceptions import UpstreamError + +raise UpstreamError( + "Slack API error: channel_not_found", + status_code=404 +) +``` + +### `UpstreamRateLimitError` + +**Bases:** `UpstreamError` + +Rate limit from an external API. Includes retry information. + +```python +from arcade_mcp_server.exceptions import UpstreamRateLimitError + +raise UpstreamRateLimitError( + "Rate limited by Slack", + retry_after_ms=60_000 +) +``` + + + You rarely throw `UpstreamError` manually. Use [error adapters](/references/mcp/python/types#error-adapters) to automatically translate SDK errors. + + +## Error Hierarchy + +```text +MCPError (base) +├── MCPContextError (user/input caused the error) +│ ├── AuthorizationError +│ ├── NotFoundError +│ ├── PromptError +│ └── ResourceError +└── MCPRuntimeError (server/infrastructure caused the error) + ├── ProtocolError + ├── TransportError + └── ServerError + ├── RequestError + │ └── ServerRequestError + ├── ResponseError + ├── SessionError + └── LifespanError + +ToolkitError (base for tool errors) +└── ToolError + └── ToolRuntimeError + └── ToolExecutionError + ├── RetryableToolError (can retry) + ├── FatalToolError (do not retry) + ├── ContextRequiredToolError (needs user input) + └── UpstreamError (external API failure) + └── UpstreamRateLimitError +``` + ## Examples ### Raising exceptions for common error scenarios ```python from arcade_mcp_server.exceptions import ( - MCPError, NotFoundError, - ValidationError, - ToolError, + RetryableToolError, + FatalToolError, ) -# Raising a not-found when a resource is missing +# Resource not found async def read_resource_or_fail(uri: str) -> str: if not await exists(uri): raise NotFoundError(f"Resource not found: {uri}") return await read(uri) -# Validating input -def validate_age(age: int) -> None: - if age < 0: - raise ValidationError("age must be non-negative") - -# Handling tool execution errors in middleware or handlers -async def call_tool_safely(call): +# Retryable failure +async def fetch_data(url: str): try: - return await call() - except ToolError as e: - # Convert to an error result or re-raise - raise MCPError(f"Tool failed: {e}") + return await http_client.get(url) + except ConnectionError as e: + raise RetryableToolError( + "Failed to connect. Please try again.", + developer_message=str(e) + ) + +# Unrecoverable failure +def validate_config(config: dict): + if "api_key" not in config: + raise FatalToolError("Missing required configuration: api_key") ``` diff --git a/app/en/references/mcp/python/overview/page.mdx b/app/en/references/mcp/python/overview/page.mdx index b4af2d23d..be86ed877 100644 --- a/app/en/references/mcp/python/overview/page.mdx +++ b/app/en/references/mcp/python/overview/page.mdx @@ -139,6 +139,84 @@ Decorator for adding tools with optional parameters. #### Examples +##### With OAuth Authentication + +Use `requires_auth` when your tool needs to act on behalf of a user. Add a `context: Context` parameter to access the authorization token. + +```python +from arcade_mcp_server import MCPApp, Context +from arcade_mcp_server.auth import Google +from typing import Annotated + +app = MCPApp(name="my_server") + +@app.tool(requires_auth=Google(scopes=["profile", "email"])) +async def get_profile( + user_id: Annotated[str, "The user ID to look up"], + context: Context, +) -> dict: + """Get user profile from Google.""" + token = context.authorization.token # OAuth access token + # Use token to call Google APIs... + return {"user_id": user_id} +``` + +Available auth providers: + +```python +from arcade_mcp_server.auth import ( + Google, + GitHub, + Slack, + Microsoft, + Discord, + LinkedIn, + Dropbox, + # ... and more +) +``` + +##### With Secrets + +Use `requires_secrets` for API keys your server needs. Add a `context: Context` parameter to access secrets. + +```python +from arcade_mcp_server import MCPApp, Context +from typing import Annotated + +app = MCPApp(name="my_server") + +@app.tool(requires_secrets=["API_KEY"]) +async def call_external_api( + query: Annotated[str, "Search query"], + context: Context, +) -> str: + """Call an external API.""" + api_key = context.get_secret("API_KEY") + # Use api_key to call external service... + return f"Results for: {query}" +``` + +##### With Lifecycle Hooks + +Use `@app.on_event` for startup/shutdown logic. + +```python +from arcade_mcp_server import MCPApp + +app = MCPApp(name="my_server") + +@app.on_event("startup") +async def on_startup(): + await db.connect() + +@app.on_event("shutdown") +async def on_shutdown(): + await db.disconnect() +``` + +##### Full Example + ```python # --- server.py --- # Programmatic server creation with a simple tool and HTTP transport diff --git a/app/en/references/mcp/python/transports/page.mdx b/app/en/references/mcp/python/transports/page.mdx index 07a70e46a..c8dca7d01 100644 --- a/app/en/references/mcp/python/transports/page.mdx +++ b/app/en/references/mcp/python/transports/page.mdx @@ -103,10 +103,19 @@ app.run(transport="http", host="0.0.0.0", port=8080) When running in HTTP mode, the server provides: -- `GET /worker/health` - Health check endpoint -- `GET /mcp` - SSE endpoint for MCP protocol -- `GET /docs` - Swagger UI documentation (debug mode) -- `GET /redoc` - ReDoc documentation (debug mode) +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/worker/health` | GET | Health check (returns 200 OK) | +| `/mcp` | GET | SSE stream for server-initiated messages | +| `/mcp` | POST | Send JSON-RPC message | +| `/mcp` | DELETE | Terminate session (when using session IDs) | +| `/docs` | GET | Swagger UI documentation (debug mode) | +| `/redoc` | GET | ReDoc documentation (debug mode) | + + + POST requests may return `application/json` (single response) or `text/event-stream` (SSE stream). + Clients should include `Accept: application/json, text/event-stream` in requests. + ### Development Features diff --git a/app/en/references/mcp/python/types/page.mdx b/app/en/references/mcp/python/types/page.mdx index ba9947cb2..7ad5b3733 100644 --- a/app/en/references/mcp/python/types/page.mdx +++ b/app/en/references/mcp/python/types/page.mdx @@ -1,3 +1,8 @@ +--- +title: "Types - Arcade MCP Python Reference" +description: "Core types, models, and error adapters for MCP servers" +--- + # Types Core Pydantic models and enums for the MCP protocol shapes. @@ -54,3 +59,55 @@ result = CallToolResult( ) ``` +## Error Adapters + +Error adapters translate vendor-specific exceptions into Arcade errors automatically. + +### `arcade_tdk.error_adapters` + +#### `ErrorAdapter` + +**Type:** Protocol + +Base interface for error adapters. + +```python +class ErrorAdapter(Protocol): + slug: str + def from_exception(self, error: Exception) -> MCPError | None: ... +``` + +### Built-in Adapters + +| Adapter | Handles | +|---------|---------| +| `SlackErrorAdapter` | Slack SDK errors | +| `GoogleErrorAdapter` | Google API Client errors | +| `MicrosoftGraphErrorAdapter` | Microsoft Graph SDK errors | +| `HTTPErrorAdapter` | fetch/HTTP library errors | +| `GraphQLErrorAdapter` | GraphQL client errors | + +### Using Error Adapters + +Pass adapters to `@app.tool` to automatically translate exceptions: + +```python +from arcade_mcp_server import MCPApp +from arcade_tdk.error_adapters import SlackErrorAdapter +from typing import Annotated + +app = MCPApp(name="slack_bot") + +@app.tool(adapters=[SlackErrorAdapter()]) +def send_message( + channel: Annotated[str, "Slack channel ID"], + text: Annotated[str, "Message text"] +) -> str: + """Send a message to Slack.""" + # SlackApiError → UpstreamError automatically + slack_client.chat_postMessage(channel=channel, text=text) + return "Sent!" +``` + +Adapters are tried in order. First match wins. `HTTPErrorAdapter` is always added as fallback. +