clawd-code-sdk 1.0.3__tar.gz → 1.0.5__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-1.0.3 → clawd_code_sdk-1.0.5}/PKG-INFO +1 -1
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/pyproject.toml +1 -1
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/_internal/transport/subprocess_cli.py +2 -2
- clawd_code_sdk-1.0.5/src/clawd_code_sdk/anthropic_types.py +39 -0
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/client.py +7 -4
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/list_sessions.py +15 -72
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/mcp_utils.py +107 -4
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/models/__init__.py +1 -3
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/models/content_blocks.py +1 -0
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/models/messages.py +0 -36
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/models/options.py +43 -77
- clawd_code_sdk-1.0.5/src/clawd_code_sdk/models/session.py +97 -0
- clawd_code_sdk-1.0.5/tests/e2e/test_mcp_resources.py +72 -0
- clawd_code_sdk-1.0.5/tests/e2e/test_sdk_mcp_resources.py +78 -0
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/tests/e2e/test_tool_permissions.py +1 -1
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/tests/mcp_server.py +12 -0
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/tests/test_sdk_mcp_integration.py +138 -5
- clawd_code_sdk-1.0.3/src/clawd_code_sdk/anthropic_types.py +0 -116
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/.gitignore +0 -0
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/LICENSE +0 -0
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/README.md +0 -0
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/__init__.py +0 -0
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/_bundled/.gitignore +0 -0
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/_errors.py +0 -0
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/_internal/__init__.py +0 -0
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/_internal/message_parser.py +0 -0
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/_internal/query.py +0 -0
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/_internal/transport/__init__.py +0 -0
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/_version.py +0 -0
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/models/agents.py +0 -0
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/models/base.py +0 -0
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/models/control.py +0 -0
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/models/hooks.py +0 -0
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/models/input_types.py +0 -0
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/models/mcp.py +0 -0
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/models/output_types.py +0 -0
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/models/permissions.py +0 -0
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/models/prompt_requests.py +0 -0
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/models/prompts.py +0 -0
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/models/server_info.py +0 -0
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/models/settings.py +0 -0
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/models/system_messages.py +0 -0
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/models/thinking.py +0 -0
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/py.typed +0 -0
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/query.py +0 -0
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/session.py +0 -0
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/storage/ARCHITECTURE.md +0 -0
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/storage/__init__.py +0 -0
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/storage/helpers.py +0 -0
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/storage/models.py +0 -0
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/storage/replay.py +0 -0
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/usage.py +0 -0
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/tests/__init__.py +0 -0
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/tests/conftest.py +0 -0
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/tests/e2e/__init__.py +0 -0
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/tests/e2e/test_agents_and_settings.py +0 -0
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/tests/e2e/test_dynamic_control.py +0 -0
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/tests/e2e/test_hook_events.py +0 -0
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/tests/e2e/test_hooks.py +0 -0
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/tests/e2e/test_include_partial_messages.py +0 -0
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/tests/e2e/test_mcp_tools.py +0 -0
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/tests/e2e/test_sdk_mcp_tools.py +0 -0
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/tests/e2e/test_slash_commands.py +0 -0
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/tests/e2e/test_stderr_callback.py +0 -0
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/tests/e2e/test_storage_parsing.py +0 -0
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/tests/e2e/test_structured_output.py +0 -0
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/tests/e2e/test_subagent_invocation.py +0 -0
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/tests/mock_claude_server.py +0 -0
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/tests/test_changelog.py +0 -0
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/tests/test_client.py +0 -0
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/tests/test_errors.py +0 -0
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/tests/test_image.png +0 -0
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/tests/test_integration.py +0 -0
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/tests/test_message_parser.py +0 -0
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/tests/test_session.py +0 -0
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/tests/test_streaming_client.py +0 -0
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/tests/test_subprocess_buffering.py +0 -0
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/tests/test_tool_callbacks.py +0 -0
- {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/tests/test_transport.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: clawd-code-sdk
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.5
|
|
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
|
|
@@ -469,8 +469,8 @@ def to_cli_args(options: ClaudeAgentOptions) -> list[str]:
|
|
|
469
469
|
if options.context_1m:
|
|
470
470
|
cmd.extend(["--betas", "context-1m-2025-08-07"])
|
|
471
471
|
|
|
472
|
-
if options.
|
|
473
|
-
cmd.extend(["--permission-prompt-tool", options.
|
|
472
|
+
if isinstance(options.on_permission, str):
|
|
473
|
+
cmd.extend(["--permission-prompt-tool", options.on_permission])
|
|
474
474
|
|
|
475
475
|
if options.permission_mode:
|
|
476
476
|
cmd.extend(["--permission-mode", options.permission_mode])
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Anthropic SDK types for tool result content blocks.
|
|
2
|
+
|
|
3
|
+
This module re-exports BetaContentBlock from the Anthropic SDK as the
|
|
4
|
+
canonical union type for content blocks in tool results. BetaImageBlockParam
|
|
5
|
+
is included because image content in MCP tool results uses the input
|
|
6
|
+
(Param) type rather than an output block type.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from anthropic.types.beta import BetaContentBlock, BetaImageBlockParam
|
|
12
|
+
from pydantic import TypeAdapter
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# Union of content types that can appear in tool results.
|
|
16
|
+
# BetaContentBlock covers all server-side tool output blocks.
|
|
17
|
+
# BetaImageBlockParam covers image content from MCP tools (TypedDict, not a model).
|
|
18
|
+
ToolResultContentBlock = BetaContentBlock | BetaImageBlockParam
|
|
19
|
+
|
|
20
|
+
_tool_result_content_adapter: TypeAdapter[list[ToolResultContentBlock]] | None = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _get_adapter() -> TypeAdapter[list[ToolResultContentBlock]]:
|
|
24
|
+
global _tool_result_content_adapter # noqa: PLW0603
|
|
25
|
+
if _tool_result_content_adapter is None:
|
|
26
|
+
_tool_result_content_adapter = TypeAdapter(list[ToolResultContentBlock])
|
|
27
|
+
return _tool_result_content_adapter
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def validate_tool_result_content(content: list[dict[str, object]]) -> list[ToolResultContentBlock]:
|
|
31
|
+
"""Validate and parse raw tool result content into typed blocks.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
content: Raw list of content block dictionaries from CLI output
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
List of validated and typed content blocks
|
|
38
|
+
"""
|
|
39
|
+
return _get_adapter().validate_python(content)
|
|
@@ -94,10 +94,13 @@ class ClaudeSDKClient:
|
|
|
94
94
|
# Validate and configure permission settings (matching TypeScript SDK logic)
|
|
95
95
|
self.options.validate()
|
|
96
96
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
97
|
+
# If on_permission is a callback, extract it for Query and replace with
|
|
98
|
+
# "stdio" so the CLI routes permission requests through the control protocol.
|
|
99
|
+
if callable(self.options.on_permission):
|
|
100
|
+
can_use_tool = self.options.on_permission
|
|
101
|
+
options = replace(self.options, on_permission="stdio")
|
|
100
102
|
else:
|
|
103
|
+
can_use_tool = None
|
|
101
104
|
options = self.options
|
|
102
105
|
|
|
103
106
|
# Use provided custom transport or create subprocess transport
|
|
@@ -140,7 +143,7 @@ class ClaudeSDKClient:
|
|
|
140
143
|
# Create Query to handle control protocol
|
|
141
144
|
self._query = Query(
|
|
142
145
|
transport=self._transport,
|
|
143
|
-
can_use_tool=
|
|
146
|
+
can_use_tool=can_use_tool,
|
|
144
147
|
on_user_question=self.options.on_user_question,
|
|
145
148
|
on_elicitation=self.options.on_elicitation,
|
|
146
149
|
hooks=self.options.hooks,
|
|
@@ -8,7 +8,6 @@ from typing import TYPE_CHECKING
|
|
|
8
8
|
|
|
9
9
|
import anyenv
|
|
10
10
|
|
|
11
|
-
from clawd_code_sdk.models import SDKSessionInfo
|
|
12
11
|
from clawd_code_sdk.storage.helpers import (
|
|
13
12
|
decode_project_path,
|
|
14
13
|
get_claude_projects_dir,
|
|
@@ -17,7 +16,8 @@ from clawd_code_sdk.storage.helpers import (
|
|
|
17
16
|
|
|
18
17
|
|
|
19
18
|
if TYPE_CHECKING:
|
|
20
|
-
from clawd_code_sdk.models
|
|
19
|
+
from clawd_code_sdk.models import SDKSessionInfo
|
|
20
|
+
from clawd_code_sdk.models.session import ListSessionsOptions
|
|
21
21
|
|
|
22
22
|
logger = logging.getLogger(__name__)
|
|
23
23
|
|
|
@@ -53,10 +53,9 @@ def _read_git_branch_from_tail(path: Path) -> str | None:
|
|
|
53
53
|
if "gitBranch" not in stripped and "git_branch" not in stripped:
|
|
54
54
|
continue
|
|
55
55
|
try:
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
return branch
|
|
56
|
+
match anyenv.load_json(stripped, return_type=dict):
|
|
57
|
+
case {"gitBranch": str(branch)} | {"git_branch": str(branch)} if branch:
|
|
58
|
+
return branch
|
|
60
59
|
except anyenv.JsonLoadError:
|
|
61
60
|
continue
|
|
62
61
|
except OSError:
|
|
@@ -85,7 +84,7 @@ def _extract_session_metadata(session_path: Path) -> tuple[str | None, str | Non
|
|
|
85
84
|
for line in fp:
|
|
86
85
|
if '"type":"summary"' in line or '"type": "summary"' in line:
|
|
87
86
|
try:
|
|
88
|
-
entry
|
|
87
|
+
entry = anyenv.load_json(line, return_type=dict)
|
|
89
88
|
if summary := entry.get("summary"):
|
|
90
89
|
custom_title = str(summary)
|
|
91
90
|
except anyenv.JsonLoadError:
|
|
@@ -93,15 +92,10 @@ def _extract_session_metadata(session_path: Path) -> tuple[str | None, str | Non
|
|
|
93
92
|
|
|
94
93
|
elif first_prompt is None and ('"type":"user"' in line or '"type": "user"' in line):
|
|
95
94
|
try:
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
and (content := msg.get("content"))
|
|
101
|
-
and isinstance(content, str)
|
|
102
|
-
and (first_line := content.split("\n")[0].strip())
|
|
103
|
-
):
|
|
104
|
-
first_prompt = first_line
|
|
95
|
+
match anyenv.load_json(line, return_type=dict):
|
|
96
|
+
case {"message": {"content": str(content)}}:
|
|
97
|
+
if first_line := content.split("\n")[0].strip():
|
|
98
|
+
first_prompt = first_line
|
|
105
99
|
except anyenv.JsonLoadError:
|
|
106
100
|
pass
|
|
107
101
|
|
|
@@ -114,45 +108,6 @@ def _extract_session_metadata(session_path: Path) -> tuple[str | None, str | Non
|
|
|
114
108
|
return custom_title, first_prompt
|
|
115
109
|
|
|
116
110
|
|
|
117
|
-
def _build_session_info(session_path: Path, project_cwd: str | None) -> SDKSessionInfo | None:
|
|
118
|
-
"""Build an SDKSessionInfo from a session JSONL file.
|
|
119
|
-
|
|
120
|
-
Args:
|
|
121
|
-
session_path: Path to the ``.jsonl`` session file.
|
|
122
|
-
project_cwd: Working directory derived from the project folder name,
|
|
123
|
-
or None if unknown.
|
|
124
|
-
|
|
125
|
-
Returns:
|
|
126
|
-
Populated SDKSessionInfo, or None if the file cannot be read.
|
|
127
|
-
"""
|
|
128
|
-
try:
|
|
129
|
-
stat = session_path.stat()
|
|
130
|
-
except OSError:
|
|
131
|
-
return None
|
|
132
|
-
|
|
133
|
-
session_id = session_path.stem
|
|
134
|
-
last_modified = int(stat.st_mtime * 1000) # milliseconds since epoch
|
|
135
|
-
file_size = stat.st_size
|
|
136
|
-
custom_title, first_prompt = _extract_session_metadata(session_path)
|
|
137
|
-
# Get git branch from the tail of the file.
|
|
138
|
-
# The raw JSONL uses camelCase ("gitBranch") per ClaudeCodeBaseModel alias config.
|
|
139
|
-
# Not all entry types carry gitBranch, so we scan backward until we find one.
|
|
140
|
-
git_branch = _read_git_branch_from_tail(session_path)
|
|
141
|
-
# Build display summary: prefer custom title, then first prompt, then session ID
|
|
142
|
-
summary = custom_title or first_prompt or session_id
|
|
143
|
-
|
|
144
|
-
return SDKSessionInfo(
|
|
145
|
-
session_id=session_id,
|
|
146
|
-
summary=summary,
|
|
147
|
-
last_modified=last_modified,
|
|
148
|
-
file_size=file_size,
|
|
149
|
-
custom_title=custom_title,
|
|
150
|
-
first_prompt=first_prompt,
|
|
151
|
-
git_branch=git_branch,
|
|
152
|
-
cwd=project_cwd,
|
|
153
|
-
)
|
|
154
|
-
|
|
155
|
-
|
|
156
111
|
def _list_session_files_for_dir(directory: str) -> list[tuple[Path, str | None]]:
|
|
157
112
|
"""List session files for a specific project directory.
|
|
158
113
|
|
|
@@ -213,27 +168,15 @@ def list_sessions(options: ListSessionsOptions | None = None) -> list[SDKSession
|
|
|
213
168
|
# List all sessions across all projects
|
|
214
169
|
all_sessions = list_sessions()
|
|
215
170
|
"""
|
|
171
|
+
from clawd_code_sdk.models import SDKSessionInfo
|
|
172
|
+
|
|
216
173
|
opts = options or {}
|
|
217
|
-
|
|
174
|
+
dir_ = opts.get("dir")
|
|
218
175
|
limit = opts.get("limit")
|
|
219
|
-
|
|
220
176
|
# Collect session files
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
session_files = _list_session_files_for_dir(directory)
|
|
224
|
-
else:
|
|
225
|
-
session_files = _list_all_session_files()
|
|
226
|
-
|
|
227
|
-
# Build session info for each file
|
|
228
|
-
sessions: list[SDKSessionInfo] = []
|
|
229
|
-
for session_path, cwd in session_files:
|
|
230
|
-
if info := _build_session_info(session_path, cwd):
|
|
231
|
-
sessions.append(info)
|
|
232
|
-
|
|
233
|
-
# Sort by last_modified descending (newest first)
|
|
177
|
+
files = _list_all_session_files() if dir_ is None else _list_session_files_for_dir(dir_)
|
|
178
|
+
sessions = [SDKSessionInfo.from_session_file(p, cwd) for p, cwd in files if p.exists()]
|
|
234
179
|
sessions.sort(key=lambda s: s.last_modified, reverse=True)
|
|
235
|
-
# Apply limit
|
|
236
180
|
if limit is not None and limit > 0:
|
|
237
181
|
sessions = sessions[:limit]
|
|
238
|
-
|
|
239
182
|
return sessions
|
|
@@ -247,8 +247,52 @@ def create_sdk_mcp_server(
|
|
|
247
247
|
return McpSdkServerConfigWithInstance(type="sdk", name=name, instance=server)
|
|
248
248
|
|
|
249
249
|
|
|
250
|
+
def _detect_capabilities(server: McpServer) -> dict[str, Any]:
|
|
251
|
+
"""Detect which MCP capabilities a server supports based on registered handlers."""
|
|
252
|
+
from mcp.types import (
|
|
253
|
+
CallToolRequest,
|
|
254
|
+
GetPromptRequest,
|
|
255
|
+
ListPromptsRequest,
|
|
256
|
+
ListResourcesRequest,
|
|
257
|
+
ListResourceTemplatesRequest,
|
|
258
|
+
ListToolsRequest,
|
|
259
|
+
ReadResourceRequest,
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
capabilities: dict[str, Any] = {}
|
|
263
|
+
handlers = server.request_handlers
|
|
264
|
+
if handlers.get(ListToolsRequest) or handlers.get(CallToolRequest):
|
|
265
|
+
capabilities["tools"] = {}
|
|
266
|
+
if (
|
|
267
|
+
handlers.get(ListResourcesRequest)
|
|
268
|
+
or handlers.get(ReadResourceRequest)
|
|
269
|
+
or handlers.get(ListResourceTemplatesRequest)
|
|
270
|
+
):
|
|
271
|
+
capabilities["resources"] = {}
|
|
272
|
+
if handlers.get(ListPromptsRequest) or handlers.get(GetPromptRequest):
|
|
273
|
+
capabilities["prompts"] = {}
|
|
274
|
+
return capabilities
|
|
275
|
+
|
|
276
|
+
|
|
250
277
|
async def process_mcp_request(message: JSONRPCMessage, server: McpServer) -> JSONRPCResponse:
|
|
251
|
-
from mcp.types import
|
|
278
|
+
from mcp.types import (
|
|
279
|
+
CallToolRequest,
|
|
280
|
+
CallToolRequestParams,
|
|
281
|
+
CallToolResult,
|
|
282
|
+
GetPromptRequest,
|
|
283
|
+
GetPromptRequestParams,
|
|
284
|
+
GetPromptResult,
|
|
285
|
+
ListPromptsRequest,
|
|
286
|
+
ListPromptsResult,
|
|
287
|
+
ListResourcesRequest,
|
|
288
|
+
ListResourcesResult,
|
|
289
|
+
ListResourceTemplatesRequest,
|
|
290
|
+
ListResourceTemplatesResult,
|
|
291
|
+
ListToolsRequest,
|
|
292
|
+
ReadResourceRequest,
|
|
293
|
+
ReadResourceRequestParams,
|
|
294
|
+
ReadResourceResult,
|
|
295
|
+
)
|
|
252
296
|
|
|
253
297
|
raw_id = message.get("id")
|
|
254
298
|
msg_id = raw_id if isinstance(raw_id, str | int) else 0
|
|
@@ -264,7 +308,7 @@ async def process_mcp_request(message: JSONRPCMessage, server: McpServer) -> JSO
|
|
|
264
308
|
# Handle MCP initialization - hardcoded for tools only, no listChanged
|
|
265
309
|
init_result = {
|
|
266
310
|
"protocolVersion": "2024-11-05",
|
|
267
|
-
"capabilities":
|
|
311
|
+
"capabilities": _detect_capabilities(server),
|
|
268
312
|
"serverInfo": {"name": server.name, "version": server.version or "1.0.0"},
|
|
269
313
|
}
|
|
270
314
|
return JSONRPCResultResponse(jsonrpc="2.0", id=msg_id, result=init_result)
|
|
@@ -291,11 +335,70 @@ async def process_mcp_request(message: JSONRPCMessage, server: McpServer) -> JSO
|
|
|
291
335
|
if result.root.isError:
|
|
292
336
|
response_data["is_error"] = True
|
|
293
337
|
return JSONRPCResultResponse(jsonrpc="2.0", id=msg_id, result=response_data)
|
|
338
|
+
case {"method": "resources/list"} if handler := server.request_handlers.get(
|
|
339
|
+
ListResourcesRequest
|
|
340
|
+
):
|
|
341
|
+
list_resources_request = ListResourcesRequest()
|
|
342
|
+
result = await handler(list_resources_request)
|
|
343
|
+
assert isinstance(result.root, ListResourcesResult)
|
|
344
|
+
data = [
|
|
345
|
+
r.model_dump(exclude_none=True, by_alias=True) for r in result.root.resources
|
|
346
|
+
]
|
|
347
|
+
return JSONRPCResultResponse(jsonrpc="2.0", id=msg_id, result={"resources": data})
|
|
348
|
+
|
|
349
|
+
case {"method": "resources/read", "params": dict() as params} if (
|
|
350
|
+
handler := server.request_handlers.get(ReadResourceRequest)
|
|
351
|
+
):
|
|
352
|
+
read_params = ReadResourceRequestParams(**params)
|
|
353
|
+
read_resource_request = ReadResourceRequest(params=read_params)
|
|
354
|
+
result = await handler(read_resource_request)
|
|
355
|
+
assert isinstance(result.root, ReadResourceResult)
|
|
356
|
+
contents = [
|
|
357
|
+
c.model_dump(exclude_none=True, by_alias=True) for c in result.root.contents
|
|
358
|
+
]
|
|
359
|
+
return JSONRPCResultResponse(
|
|
360
|
+
jsonrpc="2.0", id=msg_id, result={"contents": contents}
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
case {"method": "resources/templates/list"} if handler := server.request_handlers.get(
|
|
364
|
+
ListResourceTemplatesRequest
|
|
365
|
+
):
|
|
366
|
+
list_templates_request = ListResourceTemplatesRequest()
|
|
367
|
+
result = await handler(list_templates_request)
|
|
368
|
+
assert isinstance(result.root, ListResourceTemplatesResult)
|
|
369
|
+
data = [
|
|
370
|
+
t.model_dump(exclude_none=True, by_alias=True)
|
|
371
|
+
for t in result.root.resourceTemplates
|
|
372
|
+
]
|
|
373
|
+
return JSONRPCResultResponse(
|
|
374
|
+
jsonrpc="2.0", id=msg_id, result={"resourceTemplates": data}
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
case {"method": "prompts/list"} if handler := server.request_handlers.get(
|
|
378
|
+
ListPromptsRequest
|
|
379
|
+
):
|
|
380
|
+
list_prompts_request = ListPromptsRequest()
|
|
381
|
+
result = await handler(list_prompts_request)
|
|
382
|
+
assert isinstance(result.root, ListPromptsResult)
|
|
383
|
+
data = [p.model_dump(exclude_none=True, by_alias=True) for p in result.root.prompts]
|
|
384
|
+
return JSONRPCResultResponse(jsonrpc="2.0", id=msg_id, result={"prompts": data})
|
|
385
|
+
|
|
386
|
+
case {"method": "prompts/get", "params": dict() as params} if (
|
|
387
|
+
handler := server.request_handlers.get(GetPromptRequest)
|
|
388
|
+
):
|
|
389
|
+
get_params = GetPromptRequestParams(**params)
|
|
390
|
+
get_prompt_request = GetPromptRequest(params=get_params)
|
|
391
|
+
result = await handler(get_prompt_request)
|
|
392
|
+
assert isinstance(result.root, GetPromptResult)
|
|
393
|
+
return JSONRPCResultResponse(
|
|
394
|
+
jsonrpc="2.0",
|
|
395
|
+
id=msg_id,
|
|
396
|
+
result=result.root.model_dump(exclude_none=True, by_alias=True),
|
|
397
|
+
)
|
|
398
|
+
|
|
294
399
|
case {"method": "notifications/initialized"}:
|
|
295
400
|
# Handle initialized notification - just acknowledge it
|
|
296
401
|
return JSONRPCResultResponse(jsonrpc="2.0", id=msg_id, result={})
|
|
297
|
-
# Add more methods here as MCP SDK adds them (resources, prompts, etc.)
|
|
298
|
-
# This is the limitation Ashwin pointed out - we have to manually update
|
|
299
402
|
case {"method": method}:
|
|
300
403
|
error = JSONRPCError(code=-32601, message=f"Method '{method}' not found")
|
|
301
404
|
return JSONRPCErrorResponse(jsonrpc="2.0", id=msg_id, error=error)
|
|
@@ -216,7 +216,7 @@ from .content_blocks import (
|
|
|
216
216
|
ToolUseBlock,
|
|
217
217
|
content_block_adapter,
|
|
218
218
|
)
|
|
219
|
-
|
|
219
|
+
from .session import SDKSessionInfo, ListSessionsOptions
|
|
220
220
|
from .messages import (
|
|
221
221
|
AssistantMessage,
|
|
222
222
|
AssistantMessageError,
|
|
@@ -229,7 +229,6 @@ from .messages import (
|
|
|
229
229
|
ResultMessage,
|
|
230
230
|
ResultSuccessMessage,
|
|
231
231
|
SDKPermissionDenial,
|
|
232
|
-
SDKSessionInfo,
|
|
233
232
|
StreamEvent,
|
|
234
233
|
AuthStatusMessage,
|
|
235
234
|
ToolProgressMessage,
|
|
@@ -257,7 +256,6 @@ from .options import (
|
|
|
257
256
|
BaseSessionConfig,
|
|
258
257
|
ContinueLatest,
|
|
259
258
|
FromPR,
|
|
260
|
-
ListSessionsOptions,
|
|
261
259
|
NewSession,
|
|
262
260
|
ResumeSession,
|
|
263
261
|
SessionConfig,
|
|
@@ -68,6 +68,7 @@ class ToolResultBlock(BaseContentBlock):
|
|
|
68
68
|
is_error: bool | None = None
|
|
69
69
|
|
|
70
70
|
def get_parsed_content(self) -> list[ToolResultContentBlock] | str | None:
|
|
71
|
+
# TODO: or is it anthropic.types.beta.beta_tool_result_block_param.Content?
|
|
71
72
|
from clawd_code_sdk.anthropic_types import validate_tool_result_content
|
|
72
73
|
|
|
73
74
|
if self.content is None or isinstance(self.content, str):
|
|
@@ -80,42 +80,6 @@ OverAgeDisabledReason = Literal[
|
|
|
80
80
|
]
|
|
81
81
|
|
|
82
82
|
|
|
83
|
-
class SDKSessionInfo(BaseModel):
|
|
84
|
-
"""Session metadata returned by list_sessions."""
|
|
85
|
-
|
|
86
|
-
model_config = ConfigDict(extra="forbid")
|
|
87
|
-
|
|
88
|
-
session_id: str
|
|
89
|
-
"""Unique session identifier (UUID)."""
|
|
90
|
-
|
|
91
|
-
summary: str
|
|
92
|
-
"""Display title for the session: custom title, auto-generated summary, or first prompt."""
|
|
93
|
-
|
|
94
|
-
last_modified: int
|
|
95
|
-
"""Last modified time in milliseconds since epoch."""
|
|
96
|
-
|
|
97
|
-
file_size: int | None = None
|
|
98
|
-
"""Session file size in bytes."""
|
|
99
|
-
|
|
100
|
-
custom_title: str | None = None
|
|
101
|
-
"""User-set session title via /rename."""
|
|
102
|
-
|
|
103
|
-
first_prompt: str | None = None
|
|
104
|
-
"""First meaningful user prompt in the session."""
|
|
105
|
-
|
|
106
|
-
git_branch: str | None = None
|
|
107
|
-
"""Git branch at the end of the session."""
|
|
108
|
-
|
|
109
|
-
cwd: str | None = None
|
|
110
|
-
"""Working directory for the session."""
|
|
111
|
-
|
|
112
|
-
tag: str | None = None
|
|
113
|
-
"""User-set session tag."""
|
|
114
|
-
|
|
115
|
-
created_at: float | None = None
|
|
116
|
-
"""Creation time in milliseconds since epoch."""
|
|
117
|
-
|
|
118
|
-
|
|
119
83
|
class RateLimitInfo(TypedDict):
|
|
120
84
|
"""Rate limit information."""
|
|
121
85
|
|
|
@@ -6,7 +6,7 @@ from dataclasses import dataclass, field
|
|
|
6
6
|
import json
|
|
7
7
|
import logging
|
|
8
8
|
from pathlib import Path
|
|
9
|
-
from typing import TYPE_CHECKING, Any, Literal,
|
|
9
|
+
from typing import TYPE_CHECKING, Any, Literal, assert_never
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
if TYPE_CHECKING:
|
|
@@ -101,25 +101,6 @@ Can also be specified as a plain ``str``, which is a shortcut for
|
|
|
101
101
|
"""
|
|
102
102
|
|
|
103
103
|
|
|
104
|
-
class ListSessionsOptions(TypedDict, total=False):
|
|
105
|
-
"""Options for listing sessions.
|
|
106
|
-
|
|
107
|
-
When ``dir`` is provided, returns sessions for that project directory
|
|
108
|
-
and its git worktrees. When omitted, returns sessions across all projects.
|
|
109
|
-
"""
|
|
110
|
-
|
|
111
|
-
dir: str
|
|
112
|
-
"""Directory to list sessions for.
|
|
113
|
-
|
|
114
|
-
When provided, returns sessions for this project directory
|
|
115
|
-
(and its git worktrees). When omitted, returns sessions
|
|
116
|
-
across all projects.
|
|
117
|
-
"""
|
|
118
|
-
|
|
119
|
-
limit: int
|
|
120
|
-
"""Maximum number of sessions to return."""
|
|
121
|
-
|
|
122
|
-
|
|
123
104
|
@dataclass
|
|
124
105
|
class ClaudeAgentOptions:
|
|
125
106
|
"""Query options for Claude SDK."""
|
|
@@ -131,6 +112,27 @@ class ClaudeAgentOptions:
|
|
|
131
112
|
"""Tools which execute without prompting for permission."""
|
|
132
113
|
disallowed_tools: list[str] | None = None
|
|
133
114
|
"""Tools that are removed from agent context and cant be used."""
|
|
115
|
+
enable_agent_teams: bool = False
|
|
116
|
+
"""Enable the experimental agent teams feature."""
|
|
117
|
+
disable_parallel_tool_use: bool = False
|
|
118
|
+
"""Disable parallel too use (only one tool_use block per response)."""
|
|
119
|
+
tool_config: ToolConfig | None = None
|
|
120
|
+
"""Per-tool configuration for built-in tools."""
|
|
121
|
+
enable_tool_search: bool | int | Literal["auto"] | None = None
|
|
122
|
+
"""Enable or disable MCP tool search.
|
|
123
|
+
|
|
124
|
+
When many MCP tools are configured, tool definitions can consume a
|
|
125
|
+
significant portion of the context window. Tool search dynamically
|
|
126
|
+
loads tools on-demand instead of preloading all of them.
|
|
127
|
+
|
|
128
|
+
- ``True``: Always enabled.
|
|
129
|
+
- ``False``: Always disabled, all MCP tools loaded upfront.
|
|
130
|
+
- ``"auto"``: Activates when MCP tools exceed 10% of context (default behavior).
|
|
131
|
+
- ``int``: Auto-activates at this percentage threshold (e.g. ``5`` for 5%).
|
|
132
|
+
- ``None`` (default): Uses Claude Code's default (auto at 10%).
|
|
133
|
+
|
|
134
|
+
Requires models that support ``tool_reference`` blocks (Sonnet 4+, Opus 4+).
|
|
135
|
+
"""
|
|
134
136
|
# MCP
|
|
135
137
|
mcp_servers: dict[str, McpServerConfig] | str | Path = field(default_factory=dict)
|
|
136
138
|
"""MCP servers for the agent."""
|
|
@@ -143,25 +145,27 @@ class ClaudeAgentOptions:
|
|
|
143
145
|
"""Permission mode."""
|
|
144
146
|
allow_dangerously_skip_permissions: bool = False
|
|
145
147
|
"""Must be True when using permission_mode='bypassPermissions'."""
|
|
146
|
-
|
|
147
|
-
"""
|
|
148
|
-
can_use_tool: CanUseTool | None = None
|
|
149
|
-
"""Tool permission callback.
|
|
148
|
+
on_permission: CanUseTool | str | None = None
|
|
149
|
+
"""Permission handler for tool execution.
|
|
150
150
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
151
|
+
Accepts either:
|
|
152
|
+
- A callback function (``CanUseTool``): The SDK routes permission requests
|
|
153
|
+
through the control protocol to this callback. Automatically adds
|
|
154
|
+
``--permission-prompt-tool stdio`` to the CLI.
|
|
155
|
+
- A string: Name of an MCP tool to handle permission prompts
|
|
156
|
+
(passed as ``--permission-prompt-tool <name>`` to the CLI).
|
|
157
|
+
- ``None``: Default behavior.
|
|
154
158
|
|
|
155
159
|
Interaction with ``permission_mode``:
|
|
156
160
|
|
|
157
|
-
- ``"default"``: All tool calls are routed to this
|
|
158
|
-
- ``"acceptEdits"``: All tool calls are routed to this
|
|
159
|
-
The
|
|
161
|
+
- ``"default"``: All tool calls are routed to this handler.
|
|
162
|
+
- ``"acceptEdits"``: All tool calls are routed to this handler.
|
|
163
|
+
The handler is responsible for implementing the auto-approve-edits policy.
|
|
160
164
|
- ``"plan"``: Only the synthetic ``ExitPlanMode`` tool is routed here.
|
|
161
|
-
Actual modification tools are blocked by the CLI before reaching the
|
|
162
|
-
- ``"dontAsk"``: This
|
|
165
|
+
Actual modification tools are blocked by the CLI before reaching the handler.
|
|
166
|
+
- ``"dontAsk"``: This handler is NEVER invoked. The CLI auto-denies all
|
|
163
167
|
tools not pre-approved via the permissions config internally.
|
|
164
|
-
- ``"bypassPermissions"``: This
|
|
168
|
+
- ``"bypassPermissions"``: This handler is NEVER invoked. The CLI
|
|
165
169
|
auto-approves all tools internally.
|
|
166
170
|
"""
|
|
167
171
|
on_user_question: OnUserQuestion | None = None
|
|
@@ -169,7 +173,7 @@ class ClaudeAgentOptions:
|
|
|
169
173
|
|
|
170
174
|
Called when Claude asks the user a clarifying question via the
|
|
171
175
|
AskUserQuestion tool. If not set, these requests fall through
|
|
172
|
-
to
|
|
176
|
+
to on_permission (if it's a callback) for backwards compatibility.
|
|
173
177
|
"""
|
|
174
178
|
on_elicitation: OnElicitation | None = None
|
|
175
179
|
"""Callback for handling MCP elicitation requests.
|
|
@@ -278,8 +282,6 @@ class ClaudeAgentOptions:
|
|
|
278
282
|
When enabled, files can be rewound to their state at any user message
|
|
279
283
|
using `ClaudeSDKClient.rewind_files()`.
|
|
280
284
|
"""
|
|
281
|
-
tool_config: ToolConfig | None = None
|
|
282
|
-
"""Per-tool configuration for built-in tools."""
|
|
283
285
|
agent: str | None = None
|
|
284
286
|
"""Agent name for the main thread. The agent must be defined in `agents` or settings."""
|
|
285
287
|
context_1m: bool = False
|
|
@@ -298,25 +300,6 @@ class ClaudeAgentOptions:
|
|
|
298
300
|
"""
|
|
299
301
|
worktree: bool | str = False
|
|
300
302
|
"""Create a new git worktree for the session (with optional name)."""
|
|
301
|
-
enable_agent_teams: bool = False
|
|
302
|
-
"""Enable the experimental agent teams feature."""
|
|
303
|
-
disable_parallel_tool_use: bool = False
|
|
304
|
-
"""Disable parallel too use (only one tool_use block per response)."""
|
|
305
|
-
enable_tool_search: bool | int | Literal["auto"] | None = None
|
|
306
|
-
"""Enable or disable MCP tool search.
|
|
307
|
-
|
|
308
|
-
When many MCP tools are configured, tool definitions can consume a
|
|
309
|
-
significant portion of the context window. Tool search dynamically
|
|
310
|
-
loads tools on-demand instead of preloading all of them.
|
|
311
|
-
|
|
312
|
-
- ``True``: Always enabled.
|
|
313
|
-
- ``False``: Always disabled, all MCP tools loaded upfront.
|
|
314
|
-
- ``"auto"``: Activates when MCP tools exceed 10% of context (default behavior).
|
|
315
|
-
- ``int``: Auto-activates at this percentage threshold (e.g. ``5`` for 5%).
|
|
316
|
-
- ``None`` (default): Uses Claude Code's default (auto at 10%).
|
|
317
|
-
|
|
318
|
-
Requires models that support ``tool_reference`` blocks (Sonnet 4+, Opus 4+).
|
|
319
|
-
"""
|
|
320
303
|
|
|
321
304
|
def build_settings_value(self) -> str | None:
|
|
322
305
|
"""Build the CLI ``--settings`` value, merging sandbox if provided.
|
|
@@ -328,20 +311,14 @@ class ClaudeAgentOptions:
|
|
|
328
311
|
|
|
329
312
|
from clawd_code_sdk.models.settings import ClaudeCodeSettings as _Settings
|
|
330
313
|
|
|
331
|
-
has_settings = self.settings is not None
|
|
332
|
-
has_sandbox = self.sandbox is not None
|
|
333
|
-
|
|
334
|
-
if not has_settings and not has_sandbox:
|
|
335
|
-
return None
|
|
336
|
-
|
|
337
314
|
# Resolve settings to a dict (or pass through as file path)
|
|
338
315
|
match self.settings:
|
|
339
316
|
case _Settings() as model:
|
|
340
317
|
settings_obj = model.model_dump(by_alias=True, exclude_none=True)
|
|
341
|
-
case str() | Path() as path if
|
|
318
|
+
case str() | Path() as path if self.sandbox and Path(path).exists():
|
|
342
319
|
with Path(path).open(encoding="utf-8") as f:
|
|
343
320
|
settings_obj = json.load(f)
|
|
344
|
-
case str() | Path() as path if
|
|
321
|
+
case str() | Path() as path if self.sandbox:
|
|
345
322
|
logger.warning("Settings file not found: %s", path)
|
|
346
323
|
settings_obj = {}
|
|
347
324
|
case str() | Path() as path: # No sandbox to merge, pass file path directly to CLI
|
|
@@ -352,21 +329,10 @@ class ClaudeAgentOptions:
|
|
|
352
329
|
assert_never(unreachable)
|
|
353
330
|
|
|
354
331
|
# Merge sandbox settings
|
|
355
|
-
if
|
|
356
|
-
assert self.sandbox is not None
|
|
332
|
+
if self.sandbox is not None:
|
|
357
333
|
settings_obj["sandbox"] = self.sandbox.model_dump(by_alias=True, exclude_none=True)
|
|
358
334
|
|
|
359
|
-
return anyenv.dump_json(settings_obj)
|
|
335
|
+
return anyenv.dump_json(settings_obj) if settings_obj else None
|
|
360
336
|
|
|
361
337
|
def validate(self) -> None:
|
|
362
|
-
"""Validate option constraints.
|
|
363
|
-
|
|
364
|
-
Raises:
|
|
365
|
-
ValueError: If mutually exclusive options are set.
|
|
366
|
-
"""
|
|
367
|
-
if self.can_use_tool and self.permission_prompt_tool_name:
|
|
368
|
-
msg = (
|
|
369
|
-
"can_use_tool callback cannot be used with permission_prompt_tool_name. "
|
|
370
|
-
"Please use one or the other."
|
|
371
|
-
)
|
|
372
|
-
raise ValueError(msg)
|
|
338
|
+
"""Validate option constraints."""
|