claude-agent-sdk 0.0.23__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of claude-agent-sdk might be problematic. Click here for more details.

@@ -0,0 +1,325 @@
1
+ """Claude SDK for Python."""
2
+
3
+ from collections.abc import Awaitable, Callable
4
+ from dataclasses import dataclass
5
+ from typing import Any, Generic, TypeVar
6
+
7
+ from ._errors import (
8
+ ClaudeSDKError,
9
+ CLIConnectionError,
10
+ CLIJSONDecodeError,
11
+ CLINotFoundError,
12
+ ProcessError,
13
+ )
14
+ from ._internal.transport import Transport
15
+ from ._version import __version__
16
+ from .client import ClaudeSDKClient
17
+ from .query import query
18
+ from .types import (
19
+ AgentDefinition,
20
+ AssistantMessage,
21
+ CanUseTool,
22
+ ClaudeAgentOptions,
23
+ ContentBlock,
24
+ HookCallback,
25
+ HookContext,
26
+ HookMatcher,
27
+ McpSdkServerConfig,
28
+ McpServerConfig,
29
+ Message,
30
+ PermissionMode,
31
+ PermissionResult,
32
+ PermissionResultAllow,
33
+ PermissionResultDeny,
34
+ PermissionUpdate,
35
+ ResultMessage,
36
+ SettingSource,
37
+ SystemMessage,
38
+ TextBlock,
39
+ ThinkingBlock,
40
+ ToolPermissionContext,
41
+ ToolResultBlock,
42
+ ToolUseBlock,
43
+ UserMessage,
44
+ )
45
+
46
+ # MCP Server Support
47
+
48
+ T = TypeVar("T")
49
+
50
+
51
+ @dataclass
52
+ class SdkMcpTool(Generic[T]):
53
+ """Definition for an SDK MCP tool."""
54
+
55
+ name: str
56
+ description: str
57
+ input_schema: type[T] | dict[str, Any]
58
+ handler: Callable[[T], Awaitable[dict[str, Any]]]
59
+
60
+
61
+ def tool(
62
+ name: str, description: str, input_schema: type | dict[str, Any]
63
+ ) -> Callable[[Callable[[Any], Awaitable[dict[str, Any]]]], SdkMcpTool[Any]]:
64
+ """Decorator for defining MCP tools with type safety.
65
+
66
+ Creates a tool that can be used with SDK MCP servers. The tool runs
67
+ in-process within your Python application, providing better performance
68
+ than external MCP servers.
69
+
70
+ Args:
71
+ name: Unique identifier for the tool. This is what Claude will use
72
+ to reference the tool in function calls.
73
+ description: Human-readable description of what the tool does.
74
+ This helps Claude understand when to use the tool.
75
+ input_schema: Schema defining the tool's input parameters.
76
+ Can be either:
77
+ - A dictionary mapping parameter names to types (e.g., {"text": str})
78
+ - A TypedDict class for more complex schemas
79
+ - A JSON Schema dictionary for full validation
80
+
81
+ Returns:
82
+ A decorator function that wraps the tool implementation and returns
83
+ an SdkMcpTool instance ready for use with create_sdk_mcp_server().
84
+
85
+ Example:
86
+ Basic tool with simple schema:
87
+ >>> @tool("greet", "Greet a user", {"name": str})
88
+ ... async def greet(args):
89
+ ... return {"content": [{"type": "text", "text": f"Hello, {args['name']}!"}]}
90
+
91
+ Tool with multiple parameters:
92
+ >>> @tool("add", "Add two numbers", {"a": float, "b": float})
93
+ ... async def add_numbers(args):
94
+ ... result = args["a"] + args["b"]
95
+ ... return {"content": [{"type": "text", "text": f"Result: {result}"}]}
96
+
97
+ Tool with error handling:
98
+ >>> @tool("divide", "Divide two numbers", {"a": float, "b": float})
99
+ ... async def divide(args):
100
+ ... if args["b"] == 0:
101
+ ... return {"content": [{"type": "text", "text": "Error: Division by zero"}], "is_error": True}
102
+ ... return {"content": [{"type": "text", "text": f"Result: {args['a'] / args['b']}"}]}
103
+
104
+ Notes:
105
+ - The tool function must be async (defined with async def)
106
+ - The function receives a single dict argument with the input parameters
107
+ - The function should return a dict with a "content" key containing the response
108
+ - Errors can be indicated by including "is_error": True in the response
109
+ """
110
+
111
+ def decorator(
112
+ handler: Callable[[Any], Awaitable[dict[str, Any]]],
113
+ ) -> SdkMcpTool[Any]:
114
+ return SdkMcpTool(
115
+ name=name,
116
+ description=description,
117
+ input_schema=input_schema,
118
+ handler=handler,
119
+ )
120
+
121
+ return decorator
122
+
123
+
124
+ def create_sdk_mcp_server(
125
+ name: str, version: str = "1.0.0", tools: list[SdkMcpTool[Any]] | None = None
126
+ ) -> McpSdkServerConfig:
127
+ """Create an in-process MCP server that runs within your Python application.
128
+
129
+ Unlike external MCP servers that run as separate processes, SDK MCP servers
130
+ run directly in your application's process. This provides:
131
+ - Better performance (no IPC overhead)
132
+ - Simpler deployment (single process)
133
+ - Easier debugging (same process)
134
+ - Direct access to your application's state
135
+
136
+ Args:
137
+ name: Unique identifier for the server. This name is used to reference
138
+ the server in the mcp_servers configuration.
139
+ version: Server version string. Defaults to "1.0.0". This is for
140
+ informational purposes and doesn't affect functionality.
141
+ tools: List of SdkMcpTool instances created with the @tool decorator.
142
+ These are the functions that Claude can call through this server.
143
+ If None or empty, the server will have no tools (rarely useful).
144
+
145
+ Returns:
146
+ McpSdkServerConfig: A configuration object that can be passed to
147
+ ClaudeAgentOptions.mcp_servers. This config contains the server
148
+ instance and metadata needed for the SDK to route tool calls.
149
+
150
+ Example:
151
+ Simple calculator server:
152
+ >>> @tool("add", "Add numbers", {"a": float, "b": float})
153
+ ... async def add(args):
154
+ ... return {"content": [{"type": "text", "text": f"Sum: {args['a'] + args['b']}"}]}
155
+ >>>
156
+ >>> @tool("multiply", "Multiply numbers", {"a": float, "b": float})
157
+ ... async def multiply(args):
158
+ ... return {"content": [{"type": "text", "text": f"Product: {args['a'] * args['b']}"}]}
159
+ >>>
160
+ >>> calculator = create_sdk_mcp_server(
161
+ ... name="calculator",
162
+ ... version="2.0.0",
163
+ ... tools=[add, multiply]
164
+ ... )
165
+ >>>
166
+ >>> # Use with Claude
167
+ >>> options = ClaudeAgentOptions(
168
+ ... mcp_servers={"calc": calculator},
169
+ ... allowed_tools=["add", "multiply"]
170
+ ... )
171
+
172
+ Server with application state access:
173
+ >>> class DataStore:
174
+ ... def __init__(self):
175
+ ... self.items = []
176
+ ...
177
+ >>> store = DataStore()
178
+ >>>
179
+ >>> @tool("add_item", "Add item to store", {"item": str})
180
+ ... async def add_item(args):
181
+ ... store.items.append(args["item"])
182
+ ... return {"content": [{"type": "text", "text": f"Added: {args['item']}"}]}
183
+ >>>
184
+ >>> server = create_sdk_mcp_server("store", tools=[add_item])
185
+
186
+ Notes:
187
+ - The server runs in the same process as your Python application
188
+ - Tools have direct access to your application's variables and state
189
+ - No subprocess or IPC overhead for tool calls
190
+ - Server lifecycle is managed automatically by the SDK
191
+
192
+ See Also:
193
+ - tool(): Decorator for creating tool functions
194
+ - ClaudeAgentOptions: Configuration for using servers with query()
195
+ """
196
+ from mcp.server import Server
197
+ from mcp.types import TextContent, Tool
198
+
199
+ # Create MCP server instance
200
+ server = Server(name, version=version)
201
+
202
+ # Register tools if provided
203
+ if tools:
204
+ # Store tools for access in handlers
205
+ tool_map = {tool_def.name: tool_def for tool_def in tools}
206
+
207
+ # Register list_tools handler to expose available tools
208
+ @server.list_tools() # type: ignore[no-untyped-call,misc]
209
+ async def list_tools() -> list[Tool]:
210
+ """Return the list of available tools."""
211
+ tool_list = []
212
+ for tool_def in tools:
213
+ # Convert input_schema to JSON Schema format
214
+ if isinstance(tool_def.input_schema, dict):
215
+ # Check if it's already a JSON schema
216
+ if (
217
+ "type" in tool_def.input_schema
218
+ and "properties" in tool_def.input_schema
219
+ ):
220
+ schema = tool_def.input_schema
221
+ else:
222
+ # Simple dict mapping names to types - convert to JSON schema
223
+ properties = {}
224
+ for param_name, param_type in tool_def.input_schema.items():
225
+ if param_type is str:
226
+ properties[param_name] = {"type": "string"}
227
+ elif param_type is int:
228
+ properties[param_name] = {"type": "integer"}
229
+ elif param_type is float:
230
+ properties[param_name] = {"type": "number"}
231
+ elif param_type is bool:
232
+ properties[param_name] = {"type": "boolean"}
233
+ else:
234
+ properties[param_name] = {"type": "string"} # Default
235
+ schema = {
236
+ "type": "object",
237
+ "properties": properties,
238
+ "required": list(properties.keys()),
239
+ }
240
+ else:
241
+ # For TypedDict or other types, create basic schema
242
+ schema = {"type": "object", "properties": {}}
243
+
244
+ tool_list.append(
245
+ Tool(
246
+ name=tool_def.name,
247
+ description=tool_def.description,
248
+ inputSchema=schema,
249
+ )
250
+ )
251
+ return tool_list
252
+
253
+ # Register call_tool handler to execute tools
254
+ @server.call_tool() # type: ignore[misc]
255
+ async def call_tool(name: str, arguments: dict[str, Any]) -> Any:
256
+ """Execute a tool by name with given arguments."""
257
+ if name not in tool_map:
258
+ raise ValueError(f"Tool '{name}' not found")
259
+
260
+ tool_def = tool_map[name]
261
+ # Call the tool's handler with arguments
262
+ result = await tool_def.handler(arguments)
263
+
264
+ # Convert result to MCP format
265
+ # The decorator expects us to return the content, not a CallToolResult
266
+ # It will wrap our return value in CallToolResult
267
+ content = []
268
+ if "content" in result:
269
+ for item in result["content"]:
270
+ if item.get("type") == "text":
271
+ content.append(TextContent(type="text", text=item["text"]))
272
+
273
+ # Return just the content list - the decorator wraps it
274
+ return content
275
+
276
+ # Return SDK server configuration
277
+ return McpSdkServerConfig(type="sdk", name=name, instance=server)
278
+
279
+
280
+ __all__ = [
281
+ # Main exports
282
+ "query",
283
+ "__version__",
284
+ # Transport
285
+ "Transport",
286
+ "ClaudeSDKClient",
287
+ # Types
288
+ "PermissionMode",
289
+ "McpServerConfig",
290
+ "McpSdkServerConfig",
291
+ "UserMessage",
292
+ "AssistantMessage",
293
+ "SystemMessage",
294
+ "ResultMessage",
295
+ "Message",
296
+ "ClaudeAgentOptions",
297
+ "TextBlock",
298
+ "ThinkingBlock",
299
+ "ToolUseBlock",
300
+ "ToolResultBlock",
301
+ "ContentBlock",
302
+ # Tool callbacks
303
+ "CanUseTool",
304
+ "ToolPermissionContext",
305
+ "PermissionResult",
306
+ "PermissionResultAllow",
307
+ "PermissionResultDeny",
308
+ "PermissionUpdate",
309
+ "HookCallback",
310
+ "HookContext",
311
+ "HookMatcher",
312
+ # Agent support
313
+ "AgentDefinition",
314
+ "SettingSource",
315
+ # MCP Server Support
316
+ "create_sdk_mcp_server",
317
+ "tool",
318
+ "SdkMcpTool",
319
+ # Errors
320
+ "ClaudeSDKError",
321
+ "CLIConnectionError",
322
+ "CLINotFoundError",
323
+ "ProcessError",
324
+ "CLIJSONDecodeError",
325
+ ]
@@ -0,0 +1,56 @@
1
+ """Error types for Claude SDK."""
2
+
3
+ from typing import Any
4
+
5
+
6
+ class ClaudeSDKError(Exception):
7
+ """Base exception for all Claude SDK errors."""
8
+
9
+
10
+ class CLIConnectionError(ClaudeSDKError):
11
+ """Raised when unable to connect to Claude Code."""
12
+
13
+
14
+ class CLINotFoundError(CLIConnectionError):
15
+ """Raised when Claude Code is not found or not installed."""
16
+
17
+ def __init__(
18
+ self, message: str = "Claude Code not found", cli_path: str | None = None
19
+ ):
20
+ if cli_path:
21
+ message = f"{message}: {cli_path}"
22
+ super().__init__(message)
23
+
24
+
25
+ class ProcessError(ClaudeSDKError):
26
+ """Raised when the CLI process fails."""
27
+
28
+ def __init__(
29
+ self, message: str, exit_code: int | None = None, stderr: str | None = None
30
+ ):
31
+ self.exit_code = exit_code
32
+ self.stderr = stderr
33
+
34
+ if exit_code is not None:
35
+ message = f"{message} (exit code: {exit_code})"
36
+ if stderr:
37
+ message = f"{message}\nError output: {stderr}"
38
+
39
+ super().__init__(message)
40
+
41
+
42
+ class CLIJSONDecodeError(ClaudeSDKError):
43
+ """Raised when unable to decode JSON from CLI output."""
44
+
45
+ def __init__(self, line: str, original_error: Exception):
46
+ self.line = line
47
+ self.original_error = original_error
48
+ super().__init__(f"Failed to decode JSON: {line[:100]}...")
49
+
50
+
51
+ class MessageParseError(ClaudeSDKError):
52
+ """Raised when unable to parse a message from CLI output."""
53
+
54
+ def __init__(self, message: str, data: dict[str, Any] | None = None):
55
+ self.data = data
56
+ super().__init__(message)
@@ -0,0 +1 @@
1
+ """Internal implementation details."""
@@ -0,0 +1,121 @@
1
+ """Internal client implementation."""
2
+
3
+ from collections.abc import AsyncIterable, AsyncIterator
4
+ from dataclasses import replace
5
+ from typing import Any
6
+
7
+ from ..types import (
8
+ ClaudeAgentOptions,
9
+ HookEvent,
10
+ HookMatcher,
11
+ Message,
12
+ )
13
+ from .message_parser import parse_message
14
+ from .query import Query
15
+ from .transport import Transport
16
+ from .transport.subprocess_cli import SubprocessCLITransport
17
+
18
+
19
+ class InternalClient:
20
+ """Internal client implementation."""
21
+
22
+ def __init__(self) -> None:
23
+ """Initialize the internal client."""
24
+
25
+ def _convert_hooks_to_internal_format(
26
+ self, hooks: dict[HookEvent, list[HookMatcher]]
27
+ ) -> dict[str, list[dict[str, Any]]]:
28
+ """Convert HookMatcher format to internal Query format."""
29
+ internal_hooks: dict[str, list[dict[str, Any]]] = {}
30
+ for event, matchers in hooks.items():
31
+ internal_hooks[event] = []
32
+ for matcher in matchers:
33
+ # Convert HookMatcher to internal dict format
34
+ internal_matcher = {
35
+ "matcher": matcher.matcher if hasattr(matcher, "matcher") else None,
36
+ "hooks": matcher.hooks if hasattr(matcher, "hooks") else [],
37
+ }
38
+ internal_hooks[event].append(internal_matcher)
39
+ return internal_hooks
40
+
41
+ async def process_query(
42
+ self,
43
+ prompt: str | AsyncIterable[dict[str, Any]],
44
+ options: ClaudeAgentOptions,
45
+ transport: Transport | None = None,
46
+ ) -> AsyncIterator[Message]:
47
+ """Process a query through transport and Query."""
48
+
49
+ # Validate and configure permission settings (matching TypeScript SDK logic)
50
+ configured_options = options
51
+ if options.can_use_tool:
52
+ # canUseTool callback requires streaming mode (AsyncIterable prompt)
53
+ if isinstance(prompt, str):
54
+ raise ValueError(
55
+ "can_use_tool callback requires streaming mode. "
56
+ "Please provide prompt as an AsyncIterable instead of a string."
57
+ )
58
+
59
+ # canUseTool and permission_prompt_tool_name are mutually exclusive
60
+ if options.permission_prompt_tool_name:
61
+ raise ValueError(
62
+ "can_use_tool callback cannot be used with permission_prompt_tool_name. "
63
+ "Please use one or the other."
64
+ )
65
+
66
+ # Automatically set permission_prompt_tool_name to "stdio" for control protocol
67
+ configured_options = replace(options, permission_prompt_tool_name="stdio")
68
+
69
+ # Use provided transport or create subprocess transport
70
+ if transport is not None:
71
+ chosen_transport = transport
72
+ else:
73
+ chosen_transport = SubprocessCLITransport(
74
+ prompt=prompt, options=configured_options
75
+ )
76
+
77
+ # Connect transport
78
+ await chosen_transport.connect()
79
+
80
+ # Extract SDK MCP servers from configured options
81
+ sdk_mcp_servers = {}
82
+ if configured_options.mcp_servers and isinstance(
83
+ configured_options.mcp_servers, dict
84
+ ):
85
+ for name, config in configured_options.mcp_servers.items():
86
+ if isinstance(config, dict) and config.get("type") == "sdk":
87
+ sdk_mcp_servers[name] = config["instance"] # type: ignore[typeddict-item]
88
+
89
+ # Create Query to handle control protocol
90
+ is_streaming = not isinstance(prompt, str)
91
+ query = Query(
92
+ transport=chosen_transport,
93
+ is_streaming_mode=is_streaming,
94
+ can_use_tool=configured_options.can_use_tool,
95
+ hooks=self._convert_hooks_to_internal_format(configured_options.hooks)
96
+ if configured_options.hooks
97
+ else None,
98
+ sdk_mcp_servers=sdk_mcp_servers,
99
+ )
100
+
101
+ try:
102
+ # Start reading messages
103
+ await query.start()
104
+
105
+ # Initialize if streaming
106
+ if is_streaming:
107
+ await query.initialize()
108
+
109
+ # Stream input if it's an AsyncIterable
110
+ if isinstance(prompt, AsyncIterable) and query._tg:
111
+ # Start streaming in background
112
+ # Create a task that will run in the background
113
+ query._tg.start_soon(query.stream_input, prompt)
114
+ # For string prompts, the prompt is already passed via CLI args
115
+
116
+ # Yield parsed messages
117
+ async for data in query.receive_messages():
118
+ yield parse_message(data)
119
+
120
+ finally:
121
+ await query.close()
@@ -0,0 +1,172 @@
1
+ """Message parser for Claude Code SDK responses."""
2
+
3
+ import logging
4
+ from typing import Any
5
+
6
+ from .._errors import MessageParseError
7
+ from ..types import (
8
+ AssistantMessage,
9
+ ContentBlock,
10
+ Message,
11
+ ResultMessage,
12
+ StreamEvent,
13
+ SystemMessage,
14
+ TextBlock,
15
+ ThinkingBlock,
16
+ ToolResultBlock,
17
+ ToolUseBlock,
18
+ UserMessage,
19
+ )
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ def parse_message(data: dict[str, Any]) -> Message:
25
+ """
26
+ Parse message from CLI output into typed Message objects.
27
+
28
+ Args:
29
+ data: Raw message dictionary from CLI output
30
+
31
+ Returns:
32
+ Parsed Message object
33
+
34
+ Raises:
35
+ MessageParseError: If parsing fails or message type is unrecognized
36
+ """
37
+ if not isinstance(data, dict):
38
+ raise MessageParseError(
39
+ f"Invalid message data type (expected dict, got {type(data).__name__})",
40
+ data,
41
+ )
42
+
43
+ message_type = data.get("type")
44
+ if not message_type:
45
+ raise MessageParseError("Message missing 'type' field", data)
46
+
47
+ match message_type:
48
+ case "user":
49
+ try:
50
+ parent_tool_use_id = data.get("parent_tool_use_id")
51
+ if isinstance(data["message"]["content"], list):
52
+ user_content_blocks: list[ContentBlock] = []
53
+ for block in data["message"]["content"]:
54
+ match block["type"]:
55
+ case "text":
56
+ user_content_blocks.append(
57
+ TextBlock(text=block["text"])
58
+ )
59
+ case "tool_use":
60
+ user_content_blocks.append(
61
+ ToolUseBlock(
62
+ id=block["id"],
63
+ name=block["name"],
64
+ input=block["input"],
65
+ )
66
+ )
67
+ case "tool_result":
68
+ user_content_blocks.append(
69
+ ToolResultBlock(
70
+ tool_use_id=block["tool_use_id"],
71
+ content=block.get("content"),
72
+ is_error=block.get("is_error"),
73
+ )
74
+ )
75
+ return UserMessage(
76
+ content=user_content_blocks,
77
+ parent_tool_use_id=parent_tool_use_id,
78
+ )
79
+ return UserMessage(
80
+ content=data["message"]["content"],
81
+ parent_tool_use_id=parent_tool_use_id,
82
+ )
83
+ except KeyError as e:
84
+ raise MessageParseError(
85
+ f"Missing required field in user message: {e}", data
86
+ ) from e
87
+
88
+ case "assistant":
89
+ try:
90
+ content_blocks: list[ContentBlock] = []
91
+ for block in data["message"]["content"]:
92
+ match block["type"]:
93
+ case "text":
94
+ content_blocks.append(TextBlock(text=block["text"]))
95
+ case "thinking":
96
+ content_blocks.append(
97
+ ThinkingBlock(
98
+ thinking=block["thinking"],
99
+ signature=block["signature"],
100
+ )
101
+ )
102
+ case "tool_use":
103
+ content_blocks.append(
104
+ ToolUseBlock(
105
+ id=block["id"],
106
+ name=block["name"],
107
+ input=block["input"],
108
+ )
109
+ )
110
+ case "tool_result":
111
+ content_blocks.append(
112
+ ToolResultBlock(
113
+ tool_use_id=block["tool_use_id"],
114
+ content=block.get("content"),
115
+ is_error=block.get("is_error"),
116
+ )
117
+ )
118
+
119
+ return AssistantMessage(
120
+ content=content_blocks,
121
+ model=data["message"]["model"],
122
+ parent_tool_use_id=data.get("parent_tool_use_id"),
123
+ )
124
+ except KeyError as e:
125
+ raise MessageParseError(
126
+ f"Missing required field in assistant message: {e}", data
127
+ ) from e
128
+
129
+ case "system":
130
+ try:
131
+ return SystemMessage(
132
+ subtype=data["subtype"],
133
+ data=data,
134
+ )
135
+ except KeyError as e:
136
+ raise MessageParseError(
137
+ f"Missing required field in system message: {e}", data
138
+ ) from e
139
+
140
+ case "result":
141
+ try:
142
+ return ResultMessage(
143
+ subtype=data["subtype"],
144
+ duration_ms=data["duration_ms"],
145
+ duration_api_ms=data["duration_api_ms"],
146
+ is_error=data["is_error"],
147
+ num_turns=data["num_turns"],
148
+ session_id=data["session_id"],
149
+ total_cost_usd=data.get("total_cost_usd"),
150
+ usage=data.get("usage"),
151
+ result=data.get("result"),
152
+ )
153
+ except KeyError as e:
154
+ raise MessageParseError(
155
+ f"Missing required field in result message: {e}", data
156
+ ) from e
157
+
158
+ case "stream_event":
159
+ try:
160
+ return StreamEvent(
161
+ uuid=data["uuid"],
162
+ session_id=data["session_id"],
163
+ event=data["event"],
164
+ parent_tool_use_id=data.get("parent_tool_use_id"),
165
+ )
166
+ except KeyError as e:
167
+ raise MessageParseError(
168
+ f"Missing required field in stream_event message: {e}", data
169
+ ) from e
170
+
171
+ case _:
172
+ raise MessageParseError(f"Unknown message type: {message_type}", data)