Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/claude_agent_sdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@
HookMatcher,
McpSdkServerConfig,
McpServerConfig,
McpServerConnectionStatus,
McpServerInfo,
McpServerStatus,
McpServerStatusConfig,
McpStatusResponse,
McpToolAnnotations,
McpToolInfo,
Message,
NotificationHookInput,
NotificationHookSpecificOutput,
Expand Down Expand Up @@ -330,6 +337,13 @@ async def call_tool(name: str, arguments: dict[str, Any]) -> Any:
"PermissionMode",
"McpServerConfig",
"McpSdkServerConfig",
"McpServerStatus",
"McpServerStatusConfig",
"McpServerConnectionStatus",
"McpServerInfo",
"McpStatusResponse",
"McpToolAnnotations",
"McpToolInfo",
"UserMessage",
"AssistantMessage",
"SystemMessage",
Expand Down
41 changes: 41 additions & 0 deletions src/claude_agent_sdk/_internal/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,47 @@ async def rewind_files(self, user_message_id: str) -> None:
}
)

async def reconnect_mcp_server(self, server_name: str) -> None:
"""Reconnect a disconnected or failed MCP server.

Args:
server_name: The name of the MCP server to reconnect
"""
await self._send_control_request(
{
"subtype": "mcp_reconnect",
"serverName": server_name,
}
)

async def toggle_mcp_server(self, server_name: str, enabled: bool) -> None:
"""Enable or disable an MCP server.

Args:
server_name: The name of the MCP server to toggle
enabled: Whether the server should be enabled
"""
await self._send_control_request(
{
"subtype": "mcp_toggle",
"serverName": server_name,
"enabled": enabled,
}
)

async def stop_task(self, task_id: str) -> None:
"""Stop a running task.

Args:
task_id: The task ID from task_notification events
"""
await self._send_control_request(
{
"subtype": "stop_task",
"task_id": task_id,
}
)

async def stream_input(self, stream: AsyncIterable[dict[str, Any]]) -> None:
"""Stream input messages to transport.

Expand Down
97 changes: 91 additions & 6 deletions src/claude_agent_sdk/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,14 @@

from . import Transport
from ._errors import CLIConnectionError
from .types import ClaudeAgentOptions, HookEvent, HookMatcher, Message, ResultMessage
from .types import (
ClaudeAgentOptions,
HookEvent,
HookMatcher,
McpStatusResponse,
Message,
ResultMessage,
)


class ClaudeSDKClient:
Expand Down Expand Up @@ -304,30 +311,108 @@ async def rewind_files(self, user_message_id: str) -> None:
raise CLIConnectionError("Not connected. Call connect() first.")
await self._query.rewind_files(user_message_id)

async def get_mcp_status(self) -> dict[str, Any]:
async def reconnect_mcp_server(self, server_name: str) -> None:
"""Reconnect a disconnected or failed MCP server (only works with streaming mode).

Use this to retry connecting to an MCP server that failed to connect
or was disconnected. Raises an exception if the reconnection fails.

Args:
server_name: The name of the MCP server to reconnect

Example:
```python
async with ClaudeSDKClient(options) as client:
status = await client.get_mcp_status()
for server in status.get("mcpServers", []):
if server["status"] == "failed":
await client.reconnect_mcp_server(server["name"])
```
"""
if not self._query:
raise CLIConnectionError("Not connected. Call connect() first.")
await self._query.reconnect_mcp_server(server_name)

async def toggle_mcp_server(self, server_name: str, enabled: bool) -> None:
"""Enable or disable an MCP server (only works with streaming mode).

Disabling a server disconnects it and removes its tools from the
available tool set. Enabling a server reconnects it and makes its
tools available again. Raises an exception on failure.

Args:
server_name: The name of the MCP server to toggle
enabled: True to enable the server, False to disable it

Example:
```python
async with ClaudeSDKClient(options) as client:
# Temporarily disable a server
await client.toggle_mcp_server("my-server", enabled=False)
await client.query("Do something without my-server tools")

# Re-enable it later
await client.toggle_mcp_server("my-server", enabled=True)
```
"""
if not self._query:
raise CLIConnectionError("Not connected. Call connect() first.")
await self._query.toggle_mcp_server(server_name, enabled)

async def stop_task(self, task_id: str) -> None:
"""Stop a running task (only works with streaming mode).

After this resolves, a `task_notification` system message with
status `'stopped'` will be emitted by the CLI in the message stream.

Args:
task_id: The task ID from `task_notification` events.

Example:
```python
async with ClaudeSDKClient() as client:
await client.query("Start a long-running task")

# Listen for task_notification to get task_id, then:
await client.stop_task("task-abc123")
# A task_notification with status 'stopped' will follow
```
"""
if not self._query:
raise CLIConnectionError("Not connected. Call connect() first.")
await self._query.stop_task(task_id)

async def get_mcp_status(self) -> McpStatusResponse:
"""Get current MCP server connection status (only works with streaming mode).

Queries the Claude Code CLI for the live connection status of all
configured MCP servers.

Returns:
Dictionary with MCP server status information. Contains a
'mcpServers' key with a list of server status objects, each having:
McpStatusResponse dictionary with an 'mcpServers' key containing
a list of McpServerStatus entries. Each entry includes:
- 'name': Server name (str)
- 'status': Connection status ('connected', 'pending', 'failed',
'needs-auth', 'disabled')
- 'serverInfo': MCP server name/version (when connected)
- 'error': Error message (when status is 'failed')
- 'config': Server configuration (stdio/sse/http/sdk/claudeai-proxy)
- 'scope': Configuration scope (e.g., project, user, local)
- 'tools': List of tools provided by the server (when connected)

Example:
```python
async with ClaudeSDKClient(options) as client:
status = await client.get_mcp_status()
for server in status.get("mcpServers", []):
for server in status["mcpServers"]:
print(f"{server['name']}: {server['status']}")
if server["status"] == "failed":
print(f" Error: {server.get('error')}")
```
"""
if not self._query:
raise CLIConnectionError("Not connected. Call connect() first.")
result: dict[str, Any] = await self._query.get_mcp_status()
result: McpStatusResponse = await self._query.get_mcp_status()
return result

async def get_server_info(self) -> dict[str, Any] | None:
Expand Down
135 changes: 135 additions & 0 deletions src/claude_agent_sdk/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,116 @@ class McpSdkServerConfig(TypedDict):
)


# MCP Server Status types (returned by get_mcp_status)
# These mirror the TypeScript SDK's McpServerStatus type and use wire-format
# field names (camelCase where applicable) since they come directly from CLI
# JSON output.


class McpSdkServerConfigStatus(TypedDict):
"""SDK MCP server config as returned in status responses.

Unlike McpSdkServerConfig (which includes the in-process `instance`),
this output-only type only has serializable fields.
"""

type: Literal["sdk"]
name: str


class McpClaudeAIProxyServerConfig(TypedDict):
"""Claude.ai proxy MCP server config.

Output-only type that appears in status responses for servers proxied
through Claude.ai.
"""

type: Literal["claudeai-proxy"]
url: str
id: str


# Broader config type for status responses (includes claudeai-proxy which is
# output-only)
McpServerStatusConfig = (
McpStdioServerConfig
| McpSSEServerConfig
| McpHttpServerConfig
| McpSdkServerConfigStatus
| McpClaudeAIProxyServerConfig
)


class McpToolAnnotations(TypedDict, total=False):
"""Tool annotations as returned in MCP server status.

Wire format uses camelCase field names (from CLI JSON output).
"""

readOnly: bool
destructive: bool
openWorld: bool


class McpToolInfo(TypedDict):
"""Information about a tool provided by an MCP server."""

name: str
description: NotRequired[str]
annotations: NotRequired[McpToolAnnotations]


class McpServerInfo(TypedDict):
"""Server info from MCP initialize handshake (available when connected)."""

name: str
version: str


# Connection status values for an MCP server
McpServerConnectionStatus = Literal[
"connected", "failed", "needs-auth", "pending", "disabled"
]


class McpServerStatus(TypedDict):
"""Status information for an MCP server connection.

Returned by `ClaudeSDKClient.get_mcp_status()` in the `mcpServers` list.
"""

name: str
"""Server name as configured."""

status: McpServerConnectionStatus
"""Current connection status."""

serverInfo: NotRequired[McpServerInfo]
"""Server information from MCP handshake (available when connected)."""

error: NotRequired[str]
"""Error message (available when status is 'failed')."""

config: NotRequired[McpServerStatusConfig]
"""Server configuration (includes URL for HTTP/SSE servers)."""

scope: NotRequired[str]
"""Configuration scope (e.g., project, user, local, claudeai, managed)."""

tools: NotRequired[list[McpToolInfo]]
"""Tools provided by this server (available when connected)."""


class McpStatusResponse(TypedDict):
"""Response from `ClaudeSDKClient.get_mcp_status()`.

Wraps the list of server statuses under the `mcpServers` key, matching
the wire-format response shape.
"""

mcpServers: list[McpServerStatus]


class SdkPluginConfig(TypedDict):
"""SDK plugin configuration.

Expand Down Expand Up @@ -828,6 +938,28 @@ class SDKControlRewindFilesRequest(TypedDict):
user_message_id: str


class SDKControlMcpReconnectRequest(TypedDict):
"""Reconnects a disconnected or failed MCP server."""

subtype: Literal["mcp_reconnect"]
# Note: wire protocol uses camelCase for this field
serverName: str


class SDKControlMcpToggleRequest(TypedDict):
"""Enables or disables an MCP server."""

subtype: Literal["mcp_toggle"]
# Note: wire protocol uses camelCase for this field
serverName: str
enabled: bool


class SDKControlStopTaskRequest(TypedDict):
subtype: Literal["stop_task"]
task_id: str


class SDKControlRequest(TypedDict):
type: Literal["control_request"]
request_id: str
Expand All @@ -839,6 +971,9 @@ class SDKControlRequest(TypedDict):
| SDKHookCallbackRequest
| SDKControlMcpMessageRequest
| SDKControlRewindFilesRequest
| SDKControlMcpReconnectRequest
| SDKControlMcpToggleRequest
| SDKControlStopTaskRequest
)


Expand Down
Loading