clawd-code-sdk 0.1.76__tar.gz → 0.1.78__tar.gz
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.
- {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/PKG-INFO +1 -1
- {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/pyproject.toml +1 -1
- {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/_errors.py +33 -0
- {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/_internal/message_parser.py +0 -2
- {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/_internal/query.py +22 -20
- {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/_internal/transport/subprocess_cli.py +10 -13
- {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/client.py +12 -8
- {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/models/__init__.py +10 -0
- {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/models/base.py +9 -0
- {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/models/hooks.py +12 -0
- {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/models/input_types.py +22 -2
- {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/models/mcp.py +65 -4
- {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/models/messages.py +4 -2
- {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/models/options.py +2 -2
- {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/models/server_info.py +21 -9
- {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/storage/helpers.py +8 -37
- {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/tests/test_client.py +150 -2
- {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/.gitignore +0 -0
- {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/LICENSE +0 -0
- {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/README.md +0 -0
- {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/__init__.py +0 -0
- {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/_bundled/.gitignore +0 -0
- {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/_internal/__init__.py +0 -0
- {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/_internal/transport/__init__.py +0 -0
- {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/_version.py +0 -0
- {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/anthropic_types.py +0 -0
- {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/mcp_utils.py +0 -0
- {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/models/agents.py +0 -0
- {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/models/content_blocks.py +0 -0
- {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/models/control.py +0 -0
- {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/models/output_types.py +0 -0
- {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/models/permissions.py +0 -0
- {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/models/sandbox.py +0 -0
- {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/models/tool_use_results.py +0 -0
- {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/py.typed +0 -0
- {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/query.py +0 -0
- {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/storage/ARCHITECTURE.md +0 -0
- {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/storage/__init__.py +0 -0
- {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/storage/models.py +0 -0
- {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/usage.py +0 -0
- {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/tests/conftest.py +0 -0
- {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/tests/test_changelog.py +0 -0
- {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/tests/test_errors.py +0 -0
- {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/tests/test_integration.py +0 -0
- {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/tests/test_message_parser.py +0 -0
- {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/tests/test_sdk_mcp_integration.py +0 -0
- {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/tests/test_streaming_client.py +0 -0
- {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/tests/test_subprocess_buffering.py +0 -0
- {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/tests/test_tool_callbacks.py +0 -0
- {clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/tests/test_transport.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: clawd-code-sdk
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.78
|
|
4
4
|
Summary: Python SDK for Claude Code
|
|
5
5
|
Project-URL: Documentation, https://github.com/phil65/claude-agent-sdk-python
|
|
6
6
|
Project-URL: Homepage, https://github.com/phil65/claude-agent-sdk-python
|
|
@@ -142,3 +142,36 @@ class ServerError(APIError):
|
|
|
142
142
|
|
|
143
143
|
def __init__(self, message: str, model: str | None = None):
|
|
144
144
|
super().__init__(message, "server_error", model)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class ControlRequestError(ClaudeSDKError):
|
|
148
|
+
"""Raised when a control protocol request fails."""
|
|
149
|
+
|
|
150
|
+
def __init__(self, message: str, subtype: str | None = None):
|
|
151
|
+
self.subtype = subtype
|
|
152
|
+
super().__init__(message)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class ControlRequestTimeoutError(ControlRequestError):
|
|
156
|
+
"""Raised when a control protocol request times out."""
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class McpError(ControlRequestError):
|
|
160
|
+
"""Raised when an MCP-related control request fails.
|
|
161
|
+
|
|
162
|
+
Attributes:
|
|
163
|
+
error_code: The JSON-RPC error code from the MCP server.
|
|
164
|
+
server_name: The name of the MCP server, if known.
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
def __init__(
|
|
168
|
+
self,
|
|
169
|
+
message: str,
|
|
170
|
+
*,
|
|
171
|
+
error_code: int | None = None,
|
|
172
|
+
server_name: str | None = None,
|
|
173
|
+
subtype: str | None = None,
|
|
174
|
+
):
|
|
175
|
+
self.error_code = error_code
|
|
176
|
+
self.server_name = server_name
|
|
177
|
+
super().__init__(message, subtype=subtype)
|
{clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/_internal/message_parser.py
RENAMED
|
@@ -92,8 +92,6 @@ def parse_message(data: dict[str, Any]) -> Message:
|
|
|
92
92
|
return ToolUseSummaryMessage(**summary_data)
|
|
93
93
|
case {"type": "auth_status", **auth_data}:
|
|
94
94
|
# Convert camelCase isAuthenticating to snake_case
|
|
95
|
-
if "isAuthenticating" in auth_data:
|
|
96
|
-
auth_data["is_authenticating"] = auth_data.pop("isAuthenticating")
|
|
97
95
|
return AuthStatusMessage(**auth_data)
|
|
98
96
|
case {"type": unknown_type}:
|
|
99
97
|
raise MessageParseError(f"Unknown message type: {unknown_type}", data)
|
|
@@ -12,6 +12,7 @@ import anyenv
|
|
|
12
12
|
import anyio
|
|
13
13
|
from pydantic import BaseModel
|
|
14
14
|
|
|
15
|
+
from clawd_code_sdk._errors import ControlRequestError, ControlRequestTimeoutError
|
|
15
16
|
from clawd_code_sdk.models import (
|
|
16
17
|
ControlResponse,
|
|
17
18
|
PermissionResultAllow,
|
|
@@ -27,6 +28,7 @@ from clawd_code_sdk.models import (
|
|
|
27
28
|
ToolPermissionContext,
|
|
28
29
|
parse_control_request,
|
|
29
30
|
)
|
|
31
|
+
from clawd_code_sdk.models.control import ControlErrorResponse
|
|
30
32
|
from clawd_code_sdk.models.mcp import JSONRPCError, JSONRPCErrorResponse, JSONRPCResultResponse
|
|
31
33
|
from clawd_code_sdk.models.server_info import ClaudeCodeServerInfo
|
|
32
34
|
|
|
@@ -98,6 +100,7 @@ class Query:
|
|
|
98
100
|
system_prompt: str | None = None,
|
|
99
101
|
append_system_prompt: str | None = None,
|
|
100
102
|
json_schema: dict[str, Any] | None = None,
|
|
103
|
+
prompt_suggestions: bool | None = None,
|
|
101
104
|
):
|
|
102
105
|
"""Initialize Query with transport and callbacks.
|
|
103
106
|
|
|
@@ -111,6 +114,7 @@ class Query:
|
|
|
111
114
|
system_prompt: Optional system prompt to send via initialize
|
|
112
115
|
append_system_prompt: Optional text to append to preset system prompt
|
|
113
116
|
json_schema: Optional JSON schema for structured output
|
|
117
|
+
prompt_suggestions: Optional flag to enable prompt suggestions
|
|
114
118
|
"""
|
|
115
119
|
self._initialize_timeout = initialize_timeout
|
|
116
120
|
self.transport = transport
|
|
@@ -121,6 +125,7 @@ class Query:
|
|
|
121
125
|
self._system_prompt = system_prompt
|
|
122
126
|
self._append_system_prompt = append_system_prompt
|
|
123
127
|
self._json_schema = json_schema
|
|
128
|
+
self._prompt_suggestions = prompt_suggestions
|
|
124
129
|
# Control protocol state
|
|
125
130
|
self.pending_control_responses: dict[str, anyio.Event] = {}
|
|
126
131
|
self.pending_control_results: dict[str, dict[str, Any] | Exception] = {}
|
|
@@ -184,6 +189,8 @@ class Query:
|
|
|
184
189
|
request["appendSystemPrompt"] = self._append_system_prompt
|
|
185
190
|
if self._json_schema is not None:
|
|
186
191
|
request["jsonSchema"] = self._json_schema
|
|
192
|
+
if self._prompt_suggestions is not None:
|
|
193
|
+
request["promptSuggestions"] = self._prompt_suggestions
|
|
187
194
|
|
|
188
195
|
# Use longer timeout for initialize since MCP servers may take time to start
|
|
189
196
|
response = await self._send_control_request(request, timeout=self._initialize_timeout)
|
|
@@ -234,7 +241,9 @@ class Query:
|
|
|
234
241
|
event = self.pending_control_responses[request_id]
|
|
235
242
|
if response.get("subtype") == "error":
|
|
236
243
|
msg = response.get("error", "Unknown error")
|
|
237
|
-
self.pending_control_results[request_id] =
|
|
244
|
+
self.pending_control_results[request_id] = ControlRequestError(
|
|
245
|
+
msg, subtype=response.get("subtype")
|
|
246
|
+
)
|
|
238
247
|
else:
|
|
239
248
|
self.pending_control_results[request_id] = response
|
|
240
249
|
event.set()
|
|
@@ -304,10 +313,8 @@ class Query:
|
|
|
304
313
|
await self.transport.write(anyenv.dump_json(success_response) + "\n")
|
|
305
314
|
|
|
306
315
|
except Exception as e:
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
response={"subtype": "error", "request_id": request_id, "error": str(e)},
|
|
310
|
-
)
|
|
316
|
+
response = ControlErrorResponse(subtype="error", request_id=request_id, error=str(e))
|
|
317
|
+
error_response = SDKControlResponse(type="control_response", response=response)
|
|
311
318
|
await self.transport.write(anyenv.dump_json(error_response) + "\n")
|
|
312
319
|
|
|
313
320
|
async def _handle_permission_request(
|
|
@@ -315,8 +322,7 @@ class Query:
|
|
|
315
322
|
) -> PermissionResult:
|
|
316
323
|
"""Handle a tool permission request."""
|
|
317
324
|
if not self.can_use_tool:
|
|
318
|
-
|
|
319
|
-
raise RuntimeError(msg)
|
|
325
|
+
raise RuntimeError("canUseTool callback is not provided")
|
|
320
326
|
|
|
321
327
|
context = ToolPermissionContext(
|
|
322
328
|
tool_use_id=req.tool_use_id,
|
|
@@ -332,10 +338,8 @@ class Query:
|
|
|
332
338
|
|
|
333
339
|
async def _handle_hook_callback(self, req: SDKHookCallbackRequest) -> dict[str, Any]:
|
|
334
340
|
"""Handle a hook callback request."""
|
|
335
|
-
callback
|
|
336
|
-
|
|
337
|
-
msg = f"No hook callback found for ID: {req.callback_id}"
|
|
338
|
-
raise RuntimeError(msg)
|
|
341
|
+
if not (callback := self.hook_callbacks.get(req.callback_id)):
|
|
342
|
+
raise RuntimeError(f"No hook callback found for ID: {req.callback_id}")
|
|
339
343
|
|
|
340
344
|
hook_output = await callback(req.input, req.tool_use_id, {"signal": None})
|
|
341
345
|
# Strip trailing underscores from Python-safe names (async_, continue_) for CLI
|
|
@@ -375,7 +379,10 @@ class Query:
|
|
|
375
379
|
except TimeoutError as e:
|
|
376
380
|
self.pending_control_responses.pop(request_id, None)
|
|
377
381
|
self.pending_control_results.pop(request_id, None)
|
|
378
|
-
|
|
382
|
+
subtype = request.get("subtype")
|
|
383
|
+
raise ControlRequestTimeoutError(
|
|
384
|
+
f"Control request timeout: {subtype}", subtype=subtype
|
|
385
|
+
) from e
|
|
379
386
|
|
|
380
387
|
async def _handle_sdk_mcp_request(
|
|
381
388
|
self, server_name: str, message: JSONRPCMessage
|
|
@@ -418,12 +425,7 @@ class Query:
|
|
|
418
425
|
await self._send_control_request({"subtype": "mcp_reconnect", "serverName": server_name})
|
|
419
426
|
|
|
420
427
|
async def mcp_toggle(self, server_name: str, *, enabled: bool) -> None:
|
|
421
|
-
"""Enable or disable an MCP server.
|
|
422
|
-
|
|
423
|
-
Args:
|
|
424
|
-
server_name: Name of the MCP server to toggle
|
|
425
|
-
enabled: Whether the server should be enabled
|
|
426
|
-
"""
|
|
428
|
+
"""Enable or disable an MCP server."""
|
|
427
429
|
await self._send_control_request(
|
|
428
430
|
{"subtype": "mcp_toggle", "serverName": server_name, "enabled": enabled}
|
|
429
431
|
)
|
|
@@ -605,12 +607,12 @@ async def process_mcp_request(message: JSONRPCMessage, server: McpServer) -> JSO
|
|
|
605
607
|
match method:
|
|
606
608
|
case "initialize":
|
|
607
609
|
# Handle MCP initialization - hardcoded for tools only, no listChanged
|
|
608
|
-
|
|
610
|
+
init_result = {
|
|
609
611
|
"protocolVersion": "2024-11-05",
|
|
610
612
|
"capabilities": {"tools": {}}, # Tools capability without listChanged
|
|
611
613
|
"serverInfo": {"name": server.name, "version": server.version or "1.0.0"},
|
|
612
614
|
}
|
|
613
|
-
return JSONRPCResultResponse(jsonrpc="2.0", id=msg_id, result=
|
|
615
|
+
return JSONRPCResultResponse(jsonrpc="2.0", id=msg_id, result=init_result)
|
|
614
616
|
|
|
615
617
|
case "tools/list" if handler := server.request_handlers.get(ListToolsRequest):
|
|
616
618
|
request = ListToolsRequest()
|
|
@@ -161,15 +161,18 @@ class SubprocessCLITransport(Transport):
|
|
|
161
161
|
if self._options.resume_session_at:
|
|
162
162
|
cmd.extend(["--resume-session-at", self._options.resume_session_at])
|
|
163
163
|
|
|
164
|
-
if self._options.debug or self._options.debug_file:
|
|
165
|
-
cmd.append("--debug")
|
|
166
|
-
|
|
167
164
|
if self._options.debug_file:
|
|
168
165
|
cmd.extend(["--debug-file", self._options.debug_file])
|
|
169
166
|
|
|
170
167
|
if self._options.strict_mcp_config:
|
|
171
168
|
cmd.append("--strict-mcp-config")
|
|
172
169
|
|
|
170
|
+
match self._options.worktree:
|
|
171
|
+
case True:
|
|
172
|
+
cmd.append("--worktree")
|
|
173
|
+
case str():
|
|
174
|
+
cmd.extend(["--worktree", self._options.worktree])
|
|
175
|
+
|
|
173
176
|
sources_value = ",".join(self._options.setting_sources or [])
|
|
174
177
|
cmd.extend(["--setting-sources", sources_value])
|
|
175
178
|
|
|
@@ -286,7 +289,6 @@ class SubprocessCLITransport(Transport):
|
|
|
286
289
|
|
|
287
290
|
# Always collect stderr lines for error reporting
|
|
288
291
|
self._stderr_lines.append(line_str)
|
|
289
|
-
|
|
290
292
|
# Call the stderr callback if provided
|
|
291
293
|
if self._options.stderr:
|
|
292
294
|
self._options.stderr(line_str)
|
|
@@ -401,13 +403,11 @@ class SubprocessCLITransport(Transport):
|
|
|
401
403
|
json_buffer = ""
|
|
402
404
|
raise SDKJSONDecodeError(
|
|
403
405
|
f"JSON message exceeded maximum buffer size of {self._max_buffer_size} bytes",
|
|
404
|
-
ValueError(
|
|
405
|
-
f"Buffer size {buffer_length} exceeds limit {self._max_buffer_size}"
|
|
406
|
-
),
|
|
406
|
+
ValueError(f"{buffer_length=} exceeds {self._max_buffer_size=}"),
|
|
407
407
|
)
|
|
408
408
|
|
|
409
409
|
try:
|
|
410
|
-
data = anyenv.load_json(json_buffer)
|
|
410
|
+
data = anyenv.load_json(json_buffer, return_type=dict)
|
|
411
411
|
json_buffer = ""
|
|
412
412
|
yield data
|
|
413
413
|
except anyenv.JsonLoadError:
|
|
@@ -439,11 +439,8 @@ class SubprocessCLITransport(Transport):
|
|
|
439
439
|
# Use exit code for error detection
|
|
440
440
|
if returncode is not None and returncode != 0:
|
|
441
441
|
stderr_output = "\n".join(self._stderr_lines) if self._stderr_lines else None
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
exit_code=returncode,
|
|
445
|
-
stderr=stderr_output,
|
|
446
|
-
)
|
|
442
|
+
msg = f"Command failed with exit code {returncode}"
|
|
443
|
+
self._exit_error = ProcessError(msg, exit_code=returncode, stderr=stderr_output)
|
|
447
444
|
raise self._exit_error
|
|
448
445
|
|
|
449
446
|
|
|
@@ -10,7 +10,13 @@ from typing import TYPE_CHECKING, Any
|
|
|
10
10
|
import anyenv
|
|
11
11
|
|
|
12
12
|
from clawd_code_sdk._errors import CLIConnectionError
|
|
13
|
-
from clawd_code_sdk.models import
|
|
13
|
+
from clawd_code_sdk.models import (
|
|
14
|
+
ClaudeAgentOptions,
|
|
15
|
+
McpStdioServerConfig,
|
|
16
|
+
ResultMessage,
|
|
17
|
+
UserPromptMessage,
|
|
18
|
+
)
|
|
19
|
+
from clawd_code_sdk.models.mcp import McpStatusResponse
|
|
14
20
|
from clawd_code_sdk.models.messages import AssistantMessage
|
|
15
21
|
|
|
16
22
|
|
|
@@ -136,6 +142,7 @@ class ClaudeSDKClient:
|
|
|
136
142
|
system_prompt=system_prompt,
|
|
137
143
|
append_system_prompt=append_system_prompt,
|
|
138
144
|
json_schema=json_schema,
|
|
145
|
+
prompt_suggestions=self.options.prompt_suggestions,
|
|
139
146
|
)
|
|
140
147
|
# Start reading messages and initialize
|
|
141
148
|
await self._query.start()
|
|
@@ -246,19 +253,16 @@ class ClaudeSDKClient:
|
|
|
246
253
|
query = self._ensure_connected()
|
|
247
254
|
await query.rewind_files(user_message_id)
|
|
248
255
|
|
|
249
|
-
async def get_mcp_status(self) ->
|
|
256
|
+
async def get_mcp_status(self) -> McpStatusResponse:
|
|
250
257
|
"""Get current MCP server connection status.
|
|
251
258
|
|
|
252
259
|
Returns:
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
- 'name': Server name (str)
|
|
256
|
-
- 'status': Connection status ('connected', 'pending', 'failed',
|
|
257
|
-
'needs-auth', 'disabled')
|
|
260
|
+
Validated MCP status response containing server statuses,
|
|
261
|
+
configurations, tools, and connection information.
|
|
258
262
|
"""
|
|
259
263
|
query = self._ensure_connected()
|
|
260
264
|
result = await query.get_mcp_status()
|
|
261
|
-
return result
|
|
265
|
+
return McpStatusResponse.model_validate(result)
|
|
262
266
|
|
|
263
267
|
async def set_mcp_servers(self, servers: dict[str, McpServerConfig]) -> dict[str, Any]:
|
|
264
268
|
"""Add, replace, or remove MCP servers dynamically mid-session.
|
|
@@ -112,11 +112,16 @@ from .mcp import (
|
|
|
112
112
|
JSONRPCRequest,
|
|
113
113
|
JSONRPCResponse,
|
|
114
114
|
JSONRPCResultResponse,
|
|
115
|
+
McpConnectionStatus,
|
|
115
116
|
McpHttpServerConfig,
|
|
116
117
|
McpSdkServerConfig,
|
|
117
118
|
McpServerConfig,
|
|
119
|
+
McpServerStatusEntry,
|
|
120
|
+
McpServerVersionInfo,
|
|
118
121
|
McpSSEServerConfig,
|
|
122
|
+
McpStatusResponse,
|
|
119
123
|
McpStdioServerConfig,
|
|
124
|
+
McpToolStatus,
|
|
120
125
|
RequestId,
|
|
121
126
|
SdkPluginConfig,
|
|
122
127
|
)
|
|
@@ -289,11 +294,16 @@ __all__ = [
|
|
|
289
294
|
"JSONRPCRequest",
|
|
290
295
|
"JSONRPCResponse",
|
|
291
296
|
"JSONRPCResultResponse",
|
|
297
|
+
"McpConnectionStatus",
|
|
292
298
|
"McpHttpServerConfig",
|
|
293
299
|
"McpSdkServerConfig",
|
|
294
300
|
"McpServerConfig",
|
|
301
|
+
"McpServerStatusEntry",
|
|
302
|
+
"McpServerVersionInfo",
|
|
295
303
|
"McpSSEServerConfig",
|
|
304
|
+
"McpStatusResponse",
|
|
296
305
|
"McpStdioServerConfig",
|
|
306
|
+
"McpToolStatus",
|
|
297
307
|
"RequestId",
|
|
298
308
|
"SdkPluginConfig",
|
|
299
309
|
# sandbox
|
|
@@ -4,6 +4,15 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from typing import Literal, TypedDict
|
|
6
6
|
|
|
7
|
+
from pydantic import BaseModel, ConfigDict
|
|
8
|
+
from pydantic.alias_generators import to_camel
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ClaudeCodeBaseModel(BaseModel):
|
|
12
|
+
"""Base model for all Claude Code Pydantic models."""
|
|
13
|
+
|
|
14
|
+
model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel, extra="forbid")
|
|
15
|
+
|
|
7
16
|
|
|
8
17
|
# Permission modes
|
|
9
18
|
# - 'default': Standard behavior, prompts for dangerous operations
|
|
@@ -27,6 +27,7 @@ HookEvent = Literal[
|
|
|
27
27
|
"Setup",
|
|
28
28
|
"TeammateIdle",
|
|
29
29
|
"TaskCompleted",
|
|
30
|
+
"ConfigChange",
|
|
30
31
|
]
|
|
31
32
|
|
|
32
33
|
|
|
@@ -170,6 +171,16 @@ class TaskCompletedHookInput(BaseHookInput):
|
|
|
170
171
|
team_name: NotRequired[str]
|
|
171
172
|
|
|
172
173
|
|
|
174
|
+
class ConfigChangeHookInput(BaseHookInput):
|
|
175
|
+
"""Input data for ConfigChange hook events."""
|
|
176
|
+
|
|
177
|
+
hook_event_name: Literal["ConfigChange"]
|
|
178
|
+
source: Literal[
|
|
179
|
+
"user_settings", "project_settings", "local_settings", "policy_settings", "skills"
|
|
180
|
+
]
|
|
181
|
+
file_path: NotRequired[str]
|
|
182
|
+
|
|
183
|
+
|
|
173
184
|
# Union type for all hook inputs
|
|
174
185
|
HookInput = (
|
|
175
186
|
PreToolUseHookInput
|
|
@@ -187,6 +198,7 @@ HookInput = (
|
|
|
187
198
|
| SetupHookInput
|
|
188
199
|
| TeammateIdleHookInput
|
|
189
200
|
| TaskCompletedHookInput
|
|
201
|
+
| ConfigChangeHookInput
|
|
190
202
|
)
|
|
191
203
|
|
|
192
204
|
|
|
@@ -9,6 +9,10 @@ from __future__ import annotations
|
|
|
9
9
|
from typing import Literal, NotRequired, TypedDict
|
|
10
10
|
|
|
11
11
|
|
|
12
|
+
ModelName = Literal["sonnet", "opus", "haiku"]
|
|
13
|
+
PermissionMode = Literal["acceptEdits", "bypassPermissions", "default", "dontAsk", "plan"]
|
|
14
|
+
|
|
15
|
+
|
|
12
16
|
class AgentInput(TypedDict):
|
|
13
17
|
"""Input for the Task tool. Launches a new agent to handle complex, multi-step tasks."""
|
|
14
18
|
|
|
@@ -18,6 +22,20 @@ class AgentInput(TypedDict):
|
|
|
18
22
|
"""The task for the agent to perform."""
|
|
19
23
|
subagent_type: str
|
|
20
24
|
"""The type of specialized agent to use for this task."""
|
|
25
|
+
model: NotRequired[ModelName]
|
|
26
|
+
"""Optional model to use for the agent."""
|
|
27
|
+
resume: NotRequired[str]
|
|
28
|
+
"""Optional agent ID to resume from in order to continue from the previous execution transcript."""
|
|
29
|
+
run_in_background: NotRequired[bool]
|
|
30
|
+
"""Whether to run the agent in the background."""
|
|
31
|
+
max_turns: NotRequired[int]
|
|
32
|
+
"""Maximum number of agentic turns (API round-trips) before stopping. Used internally for warmup."""
|
|
33
|
+
name: NotRequired[str]
|
|
34
|
+
"""Name for the spawned agent."""
|
|
35
|
+
team_name: NotRequired[str]
|
|
36
|
+
"""Team name for spawning. Uses current team context if omitted."""
|
|
37
|
+
mode: NotRequired[PermissionMode]
|
|
38
|
+
"""Permission mode for spawned teammate (e.g., "plan" to require plan approval)."""
|
|
21
39
|
|
|
22
40
|
|
|
23
41
|
class AskUserQuestionOption(TypedDict):
|
|
@@ -96,9 +114,11 @@ class FileReadInput(TypedDict):
|
|
|
96
114
|
file_path: str
|
|
97
115
|
"""The absolute path to the file to read."""
|
|
98
116
|
offset: NotRequired[int]
|
|
99
|
-
"""The line number to start reading from."""
|
|
117
|
+
"""The line number to start reading from. Only provide if the file is too large to read at once."""
|
|
100
118
|
limit: NotRequired[int]
|
|
101
|
-
"""The number of lines to read."""
|
|
119
|
+
"""The number of lines to read. Only provide if the file is too large to read at once"""
|
|
120
|
+
pages: NotRequired[str]
|
|
121
|
+
"""Page range for PDF files (e.g., "1-5", "3", "10-20"). Only applicable to PDF files. Maximum 20 pages per request."""
|
|
102
122
|
|
|
103
123
|
|
|
104
124
|
class FileWriteInput(TypedDict):
|
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from typing import TYPE_CHECKING, Literal, NotRequired, TypedDict
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Literal, NotRequired, TypedDict
|
|
6
|
+
|
|
7
|
+
from pydantic import Field
|
|
8
|
+
|
|
9
|
+
from clawd_code_sdk.models.base import ClaudeCodeBaseModel
|
|
6
10
|
|
|
7
11
|
|
|
8
12
|
if TYPE_CHECKING:
|
|
@@ -12,6 +16,8 @@ if TYPE_CHECKING:
|
|
|
12
16
|
RequestId = str | int
|
|
13
17
|
JSONRPC_VERSION = "2.0"
|
|
14
18
|
|
|
19
|
+
McpConnectionStatus = Literal["connected", "pending", "failed", "needs-auth", "disabled"]
|
|
20
|
+
|
|
15
21
|
|
|
16
22
|
# JSON-RPC types (from MCP specification)
|
|
17
23
|
|
|
@@ -30,7 +36,7 @@ class JSONRPCRequest(TypedDict):
|
|
|
30
36
|
jsonrpc: str
|
|
31
37
|
id: RequestId
|
|
32
38
|
method: str
|
|
33
|
-
params: NotRequired[dict[str,
|
|
39
|
+
params: NotRequired[dict[str, Any]]
|
|
34
40
|
|
|
35
41
|
|
|
36
42
|
class JSONRPCNotification(TypedDict):
|
|
@@ -38,7 +44,7 @@ class JSONRPCNotification(TypedDict):
|
|
|
38
44
|
|
|
39
45
|
jsonrpc: str
|
|
40
46
|
method: str
|
|
41
|
-
params: NotRequired[dict[str,
|
|
47
|
+
params: NotRequired[dict[str, Any]]
|
|
42
48
|
|
|
43
49
|
|
|
44
50
|
class JSONRPCResultResponse(TypedDict):
|
|
@@ -46,7 +52,7 @@ class JSONRPCResultResponse(TypedDict):
|
|
|
46
52
|
|
|
47
53
|
jsonrpc: str
|
|
48
54
|
id: RequestId
|
|
49
|
-
result: dict[str,
|
|
55
|
+
result: dict[str, Any]
|
|
50
56
|
|
|
51
57
|
|
|
52
58
|
class JSONRPCErrorResponse(TypedDict):
|
|
@@ -111,3 +117,58 @@ class SdkPluginConfig(TypedDict):
|
|
|
111
117
|
|
|
112
118
|
type: Literal["local"]
|
|
113
119
|
path: str
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# Pydantic models for MCP status responses
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class ToolAnnotations(ClaudeCodeBaseModel):
|
|
126
|
+
"""
|
|
127
|
+
Additional properties describing a Tool to clients.
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
title: str | None = None
|
|
131
|
+
"""A human-readable title for the tool."""
|
|
132
|
+
|
|
133
|
+
read_only_hint: bool | None = None
|
|
134
|
+
"""Read-only hint."""
|
|
135
|
+
|
|
136
|
+
destructive_hint: bool | None = None
|
|
137
|
+
"""Destructive hint."""
|
|
138
|
+
|
|
139
|
+
idempotent_hint: bool | None = None
|
|
140
|
+
"""Idempodent hint."""
|
|
141
|
+
|
|
142
|
+
open_world_hint: bool | None = None
|
|
143
|
+
"""Open-world hint."""
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class McpToolStatus(ClaudeCodeBaseModel):
|
|
147
|
+
"""Status information for a single MCP tool."""
|
|
148
|
+
|
|
149
|
+
name: str
|
|
150
|
+
annotations: ToolAnnotations = Field(default_factory=ToolAnnotations)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class McpServerVersionInfo(ClaudeCodeBaseModel):
|
|
154
|
+
"""Server version information returned in MCP status."""
|
|
155
|
+
|
|
156
|
+
name: str
|
|
157
|
+
version: str
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class McpServerStatusEntry(ClaudeCodeBaseModel):
|
|
161
|
+
"""Status information for a single MCP server."""
|
|
162
|
+
|
|
163
|
+
name: str
|
|
164
|
+
status: McpConnectionStatus
|
|
165
|
+
server_info: McpServerVersionInfo | None = None
|
|
166
|
+
config: dict[str, Any] = Field(default_factory=dict)
|
|
167
|
+
scope: str | None = None
|
|
168
|
+
tools: list[McpToolStatus] = Field(default_factory=list)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class McpStatusResponse(ClaudeCodeBaseModel):
|
|
172
|
+
"""Response from get_mcp_status() containing all MCP server statuses."""
|
|
173
|
+
|
|
174
|
+
mcp_servers: list[McpServerStatusEntry] = Field(default_factory=list)
|
|
@@ -91,7 +91,7 @@ class UserMessage(BaseMessage):
|
|
|
91
91
|
|
|
92
92
|
|
|
93
93
|
@dataclass(kw_only=True)
|
|
94
|
-
class AssistantMessage
|
|
94
|
+
class AssistantMessage:
|
|
95
95
|
"""Assistant message with content blocks."""
|
|
96
96
|
|
|
97
97
|
type: Literal["assistant"] = "assistant"
|
|
@@ -99,6 +99,8 @@ class AssistantMessage(BaseMessage):
|
|
|
99
99
|
model: str
|
|
100
100
|
parent_tool_use_id: str | None = None
|
|
101
101
|
error: AssistantMessageError | None = None
|
|
102
|
+
session_id: str | None = None # not sure these two are needed.
|
|
103
|
+
uuid: str | None = None
|
|
102
104
|
|
|
103
105
|
def raise_if_api_error(self) -> None:
|
|
104
106
|
"""Raise the appropriate API exception if error is set.
|
|
@@ -391,7 +393,7 @@ class AuthStatusMessage(BaseMessage):
|
|
|
391
393
|
"""Authentication status update."""
|
|
392
394
|
|
|
393
395
|
type: Literal["auth_status"] = "auth_status"
|
|
394
|
-
|
|
396
|
+
isAuthenticating: bool = False # noqa: N815
|
|
395
397
|
output: list[str] | None = None
|
|
396
398
|
error: str | None = None
|
|
397
399
|
|
|
@@ -76,14 +76,14 @@ class ClaudeAgentOptions:
|
|
|
76
76
|
allow_dangerously_skip_permissions: bool = False
|
|
77
77
|
# Resume from a specific message UUID (use with `resume`).
|
|
78
78
|
resume_session_at: str | None = None
|
|
79
|
-
# Enable debug mode for the Claude Code process.
|
|
80
|
-
debug: bool = False
|
|
81
79
|
# Write debug logs to a specific file path. Implicitly enables debug mode.
|
|
82
80
|
debug_file: str | None = None
|
|
83
81
|
# Enforce strict validation of MCP server configurations.
|
|
84
82
|
strict_mcp_config: bool = False
|
|
85
83
|
# Enable 1M token context window (Sonnet 4/4.5 only).
|
|
86
84
|
context_1m: bool = False
|
|
85
|
+
prompt_suggestions: bool | None = None
|
|
86
|
+
worktree: bool | str = False
|
|
87
87
|
|
|
88
88
|
def build_settings_value(self) -> str | None:
|
|
89
89
|
"""Build settings value, merging sandbox settings if provided.
|
|
@@ -2,17 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from
|
|
6
|
-
from pydantic.alias_generators import to_camel
|
|
5
|
+
from typing import Literal
|
|
7
6
|
|
|
7
|
+
from pydantic import Field
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
"""Base model."""
|
|
9
|
+
from clawd_code_sdk.models.base import ClaudeCodeBaseModel
|
|
11
10
|
|
|
12
|
-
model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel)
|
|
13
11
|
|
|
12
|
+
EffortLevel = Literal["low", "medium", "high", "max"]
|
|
14
13
|
|
|
15
|
-
|
|
14
|
+
|
|
15
|
+
class ClaudeCodeModelInfo(ClaudeCodeBaseModel):
|
|
16
16
|
"""Information about an available AI model from Claude Code."""
|
|
17
17
|
|
|
18
18
|
value: str
|
|
@@ -24,8 +24,17 @@ class ClaudeCodeModelInfo(ClaudeCodeBasemodel):
|
|
|
24
24
|
description: str
|
|
25
25
|
"""Full description including capabilities and pricing."""
|
|
26
26
|
|
|
27
|
+
supports_effort: bool | None = None
|
|
28
|
+
"""Whether the model supports effort setting."""
|
|
29
|
+
|
|
30
|
+
supported_effort_levels: list[EffortLevel] | None = None
|
|
31
|
+
"""Supported effort levels."""
|
|
32
|
+
|
|
33
|
+
supports_adaptive_thinking: bool | None = None
|
|
34
|
+
"""Whether the model supports adaptive thinking."""
|
|
27
35
|
|
|
28
|
-
|
|
36
|
+
|
|
37
|
+
class ClaudeCodeCommandInfo(ClaudeCodeBaseModel):
|
|
29
38
|
"""Information about an available slash command from Claude Code."""
|
|
30
39
|
|
|
31
40
|
name: str
|
|
@@ -38,7 +47,7 @@ class ClaudeCodeCommandInfo(ClaudeCodeBasemodel):
|
|
|
38
47
|
"""Usage hint for command arguments (may be empty string)."""
|
|
39
48
|
|
|
40
49
|
|
|
41
|
-
class ClaudeCodeAccountInfo(
|
|
50
|
+
class ClaudeCodeAccountInfo(ClaudeCodeBaseModel):
|
|
42
51
|
"""Account information from Claude Code."""
|
|
43
52
|
|
|
44
53
|
email: str | None = None
|
|
@@ -54,7 +63,7 @@ class ClaudeCodeAccountInfo(ClaudeCodeBasemodel):
|
|
|
54
63
|
"""Where API key comes from (e.g., "ANTHROPIC_API_KEY")."""
|
|
55
64
|
|
|
56
65
|
|
|
57
|
-
class ClaudeCodeServerInfo(
|
|
66
|
+
class ClaudeCodeServerInfo(ClaudeCodeBaseModel):
|
|
58
67
|
"""Complete server initialization info from Claude Code.
|
|
59
68
|
|
|
60
69
|
This is returned by the Claude Code server during initialization and contains
|
|
@@ -76,3 +85,6 @@ class ClaudeCodeServerInfo(ClaudeCodeBasemodel):
|
|
|
76
85
|
|
|
77
86
|
account: ClaudeCodeAccountInfo | None = Field(default=None)
|
|
78
87
|
"""Account and authentication information."""
|
|
88
|
+
|
|
89
|
+
pid: int | None = None
|
|
90
|
+
"""Process id."""
|
|
@@ -6,7 +6,7 @@ See ARCHITECTURE.md for detailed documentation of the storage format.
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
8
|
import logging
|
|
9
|
-
from
|
|
9
|
+
from pathlib import Path
|
|
10
10
|
|
|
11
11
|
import anyenv
|
|
12
12
|
from pydantic import TypeAdapter, ValidationError
|
|
@@ -14,9 +14,6 @@ from pydantic import TypeAdapter, ValidationError
|
|
|
14
14
|
from clawd_code_sdk.storage.models import ClaudeJSONLEntry
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
if TYPE_CHECKING:
|
|
18
|
-
from pathlib import Path
|
|
19
|
-
|
|
20
17
|
logger = logging.getLogger(__name__)
|
|
21
18
|
|
|
22
19
|
|
|
@@ -70,28 +67,16 @@ def read_session(session_path: Path) -> list[ClaudeJSONLEntry]:
|
|
|
70
67
|
entry = adapter.validate_python(data)
|
|
71
68
|
entries.append(entry)
|
|
72
69
|
except anyenv.JsonLoadError as e:
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
str(session_path),
|
|
76
|
-
str(e),
|
|
77
|
-
raw_line,
|
|
78
|
-
)
|
|
70
|
+
msg = "Failed to parse JSONL line (path: %s, error: %s, raw_line: %s)"
|
|
71
|
+
logger.warning(msg, str(session_path), str(e), raw_line)
|
|
79
72
|
except ValidationError as e:
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
str(session_path),
|
|
83
|
-
str(e),
|
|
84
|
-
)
|
|
73
|
+
msg = "Failed to validate JSONL entry (path: %s, error: %s)"
|
|
74
|
+
logger.warning(msg, str(session_path), str(e))
|
|
85
75
|
return entries
|
|
86
76
|
|
|
87
77
|
|
|
88
78
|
def get_claude_data_dir() -> Path:
|
|
89
|
-
"""Get the Claude Code data directory path.
|
|
90
|
-
|
|
91
|
-
Claude Code stores data in ~/.claude rather than the XDG data directory.
|
|
92
|
-
"""
|
|
93
|
-
from pathlib import Path
|
|
94
|
-
|
|
79
|
+
"""Get the Claude Code data directory path (~/.claude)."""
|
|
95
80
|
return Path.home() / ".claude"
|
|
96
81
|
|
|
97
82
|
|
|
@@ -115,27 +100,13 @@ def path_to_claude_dir_name(project_path: str) -> str:
|
|
|
115
100
|
|
|
116
101
|
|
|
117
102
|
def get_latest_session(project_path: str) -> Path | None:
|
|
118
|
-
"""Get the most recent session file for
|
|
119
|
-
|
|
120
|
-
Args:
|
|
121
|
-
project_path: The project path
|
|
122
|
-
|
|
123
|
-
Returns:
|
|
124
|
-
Path to the latest session file, or None if no sessions exist
|
|
125
|
-
"""
|
|
103
|
+
"""Get the path for the most recent session file for given project if existing."""
|
|
126
104
|
sessions = list_project_sessions(project_path)
|
|
127
105
|
return sessions[0] if sessions else None
|
|
128
106
|
|
|
129
107
|
|
|
130
108
|
def list_project_sessions(project_path: str) -> list[Path]:
|
|
131
|
-
"""List all session files for
|
|
132
|
-
|
|
133
|
-
Args:
|
|
134
|
-
project_path: The project path (will be converted to Claude's format)
|
|
135
|
-
|
|
136
|
-
Returns:
|
|
137
|
-
List of session file paths, sorted by modification time (newest first)
|
|
138
|
-
"""
|
|
109
|
+
"""List all session files for given project path, sorted by modification time (newest first)."""
|
|
139
110
|
projects_dir = get_claude_projects_dir()
|
|
140
111
|
project_dir_name = path_to_claude_dir_name(project_path)
|
|
141
112
|
project_dir = projects_dir / project_dir_name
|
|
@@ -12,11 +12,13 @@ from clawd_code_sdk import (
|
|
|
12
12
|
AssistantMessage,
|
|
13
13
|
AuthenticationError,
|
|
14
14
|
ClaudeAgentOptions,
|
|
15
|
+
ClaudeSDKClient,
|
|
15
16
|
InvalidRequestError,
|
|
16
17
|
RateLimitError,
|
|
17
18
|
ServerError,
|
|
18
19
|
query,
|
|
19
20
|
)
|
|
21
|
+
from clawd_code_sdk.models.mcp import McpServerStatusEntry, McpStatusResponse
|
|
20
22
|
|
|
21
23
|
|
|
22
24
|
def create_mock_transport_with_messages(messages: list[dict]):
|
|
@@ -55,8 +57,11 @@ def create_mock_transport_with_messages(messages: list[dict]):
|
|
|
55
57
|
"response": {
|
|
56
58
|
"request_id": msg.get("request_id"),
|
|
57
59
|
"subtype": "success",
|
|
58
|
-
"
|
|
59
|
-
|
|
60
|
+
"response": {
|
|
61
|
+
"commands": [],
|
|
62
|
+
"outputStyle": "default",
|
|
63
|
+
"pid": 12345,
|
|
64
|
+
},
|
|
60
65
|
},
|
|
61
66
|
}
|
|
62
67
|
break
|
|
@@ -365,3 +370,146 @@ class TestAPIErrorRaising:
|
|
|
365
370
|
assert messages[0].content[0].text == "Hello!"
|
|
366
371
|
|
|
367
372
|
anyio.run(_test)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def _create_control_protocol_transport(
|
|
376
|
+
control_responses: dict[str, dict],
|
|
377
|
+
) -> AsyncMock:
|
|
378
|
+
"""Create a mock transport that handles initialization and responds to control requests.
|
|
379
|
+
|
|
380
|
+
Args:
|
|
381
|
+
control_responses: Mapping of control request subtype to response payload.
|
|
382
|
+
The "initialize" subtype is handled automatically.
|
|
383
|
+
"""
|
|
384
|
+
mock_transport = AsyncMock()
|
|
385
|
+
mock_transport.connect = AsyncMock()
|
|
386
|
+
mock_transport.close = AsyncMock()
|
|
387
|
+
mock_transport.end_input = AsyncMock()
|
|
388
|
+
|
|
389
|
+
written_messages: list[str] = []
|
|
390
|
+
|
|
391
|
+
async def mock_write(data: str) -> None:
|
|
392
|
+
written_messages.append(data)
|
|
393
|
+
|
|
394
|
+
mock_transport.write = AsyncMock(side_effect=mock_write)
|
|
395
|
+
|
|
396
|
+
init_response = {
|
|
397
|
+
"subtype": "success",
|
|
398
|
+
"response": {
|
|
399
|
+
"commands": [],
|
|
400
|
+
"outputStyle": "default",
|
|
401
|
+
"pid": 12345,
|
|
402
|
+
},
|
|
403
|
+
}
|
|
404
|
+
all_responses = {"initialize": init_response, **control_responses}
|
|
405
|
+
|
|
406
|
+
async def mock_receive():
|
|
407
|
+
last_check = 0
|
|
408
|
+
timeout_counter = 0
|
|
409
|
+
while timeout_counter < 200:
|
|
410
|
+
await asyncio.sleep(0.01)
|
|
411
|
+
timeout_counter += 1
|
|
412
|
+
|
|
413
|
+
for msg_str in written_messages[last_check:]:
|
|
414
|
+
try:
|
|
415
|
+
msg = json.loads(msg_str.strip())
|
|
416
|
+
if msg.get("type") != "control_request":
|
|
417
|
+
continue
|
|
418
|
+
subtype = msg.get("request", {}).get("subtype")
|
|
419
|
+
if subtype in all_responses:
|
|
420
|
+
yield {
|
|
421
|
+
"type": "control_response",
|
|
422
|
+
"response": {
|
|
423
|
+
"request_id": msg.get("request_id"),
|
|
424
|
+
**all_responses[subtype],
|
|
425
|
+
},
|
|
426
|
+
}
|
|
427
|
+
except (json.JSONDecodeError, KeyError, AttributeError):
|
|
428
|
+
pass
|
|
429
|
+
last_check = len(written_messages)
|
|
430
|
+
|
|
431
|
+
mock_transport.read_messages = mock_receive
|
|
432
|
+
return mock_transport
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
class TestGetMcpStatus:
|
|
436
|
+
"""Test get_mcp_status returns validated McpStatusResponse."""
|
|
437
|
+
|
|
438
|
+
def test_get_mcp_status_parses_response(self):
|
|
439
|
+
"""Test that get_mcp_status returns a validated McpStatusResponse."""
|
|
440
|
+
|
|
441
|
+
async def _test():
|
|
442
|
+
mcp_status_payload = {
|
|
443
|
+
"subtype": "success",
|
|
444
|
+
"response": {
|
|
445
|
+
"mcpServers": [
|
|
446
|
+
{
|
|
447
|
+
"name": "git",
|
|
448
|
+
"status": "connected",
|
|
449
|
+
"serverInfo": {"name": "mcp-git", "version": "1.26.0"},
|
|
450
|
+
"config": {
|
|
451
|
+
"type": "stdio",
|
|
452
|
+
"command": "uvx",
|
|
453
|
+
"args": ["mcp-server-git"],
|
|
454
|
+
},
|
|
455
|
+
"scope": "dynamic",
|
|
456
|
+
"tools": [
|
|
457
|
+
{"name": "git_status", "annotations": {}},
|
|
458
|
+
{"name": "git_log", "annotations": {}},
|
|
459
|
+
],
|
|
460
|
+
}
|
|
461
|
+
]
|
|
462
|
+
},
|
|
463
|
+
}
|
|
464
|
+
mock_transport = _create_control_protocol_transport({"mcp_status": mcp_status_payload})
|
|
465
|
+
|
|
466
|
+
client = ClaudeSDKClient(transport=mock_transport)
|
|
467
|
+
await client.connect()
|
|
468
|
+
try:
|
|
469
|
+
status = await client.get_mcp_status()
|
|
470
|
+
|
|
471
|
+
assert isinstance(status, McpStatusResponse)
|
|
472
|
+
assert len(status.mcp_servers) == 1
|
|
473
|
+
|
|
474
|
+
server = status.mcp_servers[0]
|
|
475
|
+
assert isinstance(server, McpServerStatusEntry)
|
|
476
|
+
assert server.name == "git"
|
|
477
|
+
assert server.status == "connected"
|
|
478
|
+
assert server.scope == "dynamic"
|
|
479
|
+
assert server.server_info is not None
|
|
480
|
+
assert server.server_info.name == "mcp-git"
|
|
481
|
+
assert server.server_info.version == "1.26.0"
|
|
482
|
+
assert server.config == {
|
|
483
|
+
"type": "stdio",
|
|
484
|
+
"command": "uvx",
|
|
485
|
+
"args": ["mcp-server-git"],
|
|
486
|
+
}
|
|
487
|
+
assert len(server.tools) == 2
|
|
488
|
+
assert server.tools[0].name == "git_status"
|
|
489
|
+
assert server.tools[1].name == "git_log"
|
|
490
|
+
finally:
|
|
491
|
+
await client.disconnect()
|
|
492
|
+
|
|
493
|
+
anyio.run(_test)
|
|
494
|
+
|
|
495
|
+
def test_get_mcp_status_empty_servers(self):
|
|
496
|
+
"""Test get_mcp_status with no MCP servers configured."""
|
|
497
|
+
|
|
498
|
+
async def _test():
|
|
499
|
+
mcp_status_payload = {
|
|
500
|
+
"subtype": "success",
|
|
501
|
+
"response": {"mcpServers": []},
|
|
502
|
+
}
|
|
503
|
+
mock_transport = _create_control_protocol_transport({"mcp_status": mcp_status_payload})
|
|
504
|
+
|
|
505
|
+
client = ClaudeSDKClient(transport=mock_transport)
|
|
506
|
+
await client.connect()
|
|
507
|
+
try:
|
|
508
|
+
status = await client.get_mcp_status()
|
|
509
|
+
|
|
510
|
+
assert isinstance(status, McpStatusResponse)
|
|
511
|
+
assert len(status.mcp_servers) == 0
|
|
512
|
+
finally:
|
|
513
|
+
await client.disconnect()
|
|
514
|
+
|
|
515
|
+
anyio.run(_test)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/_internal/transport/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{clawd_code_sdk-0.1.76 → clawd_code_sdk-0.1.78}/src/clawd_code_sdk/models/tool_use_results.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|