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.
Files changed (79) hide show
  1. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/PKG-INFO +1 -1
  2. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/pyproject.toml +1 -1
  3. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/_internal/transport/subprocess_cli.py +2 -2
  4. clawd_code_sdk-1.0.5/src/clawd_code_sdk/anthropic_types.py +39 -0
  5. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/client.py +7 -4
  6. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/list_sessions.py +15 -72
  7. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/mcp_utils.py +107 -4
  8. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/models/__init__.py +1 -3
  9. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/models/content_blocks.py +1 -0
  10. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/models/messages.py +0 -36
  11. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/models/options.py +43 -77
  12. clawd_code_sdk-1.0.5/src/clawd_code_sdk/models/session.py +97 -0
  13. clawd_code_sdk-1.0.5/tests/e2e/test_mcp_resources.py +72 -0
  14. clawd_code_sdk-1.0.5/tests/e2e/test_sdk_mcp_resources.py +78 -0
  15. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/tests/e2e/test_tool_permissions.py +1 -1
  16. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/tests/mcp_server.py +12 -0
  17. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/tests/test_sdk_mcp_integration.py +138 -5
  18. clawd_code_sdk-1.0.3/src/clawd_code_sdk/anthropic_types.py +0 -116
  19. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/.gitignore +0 -0
  20. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/LICENSE +0 -0
  21. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/README.md +0 -0
  22. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/__init__.py +0 -0
  23. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/_bundled/.gitignore +0 -0
  24. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/_errors.py +0 -0
  25. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/_internal/__init__.py +0 -0
  26. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/_internal/message_parser.py +0 -0
  27. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/_internal/query.py +0 -0
  28. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/_internal/transport/__init__.py +0 -0
  29. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/_version.py +0 -0
  30. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/models/agents.py +0 -0
  31. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/models/base.py +0 -0
  32. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/models/control.py +0 -0
  33. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/models/hooks.py +0 -0
  34. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/models/input_types.py +0 -0
  35. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/models/mcp.py +0 -0
  36. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/models/output_types.py +0 -0
  37. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/models/permissions.py +0 -0
  38. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/models/prompt_requests.py +0 -0
  39. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/models/prompts.py +0 -0
  40. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/models/server_info.py +0 -0
  41. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/models/settings.py +0 -0
  42. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/models/system_messages.py +0 -0
  43. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/models/thinking.py +0 -0
  44. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/py.typed +0 -0
  45. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/query.py +0 -0
  46. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/session.py +0 -0
  47. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/storage/ARCHITECTURE.md +0 -0
  48. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/storage/__init__.py +0 -0
  49. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/storage/helpers.py +0 -0
  50. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/storage/models.py +0 -0
  51. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/storage/replay.py +0 -0
  52. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/src/clawd_code_sdk/usage.py +0 -0
  53. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/tests/__init__.py +0 -0
  54. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/tests/conftest.py +0 -0
  55. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/tests/e2e/__init__.py +0 -0
  56. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/tests/e2e/test_agents_and_settings.py +0 -0
  57. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/tests/e2e/test_dynamic_control.py +0 -0
  58. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/tests/e2e/test_hook_events.py +0 -0
  59. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/tests/e2e/test_hooks.py +0 -0
  60. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/tests/e2e/test_include_partial_messages.py +0 -0
  61. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/tests/e2e/test_mcp_tools.py +0 -0
  62. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/tests/e2e/test_sdk_mcp_tools.py +0 -0
  63. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/tests/e2e/test_slash_commands.py +0 -0
  64. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/tests/e2e/test_stderr_callback.py +0 -0
  65. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/tests/e2e/test_storage_parsing.py +0 -0
  66. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/tests/e2e/test_structured_output.py +0 -0
  67. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/tests/e2e/test_subagent_invocation.py +0 -0
  68. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/tests/mock_claude_server.py +0 -0
  69. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/tests/test_changelog.py +0 -0
  70. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/tests/test_client.py +0 -0
  71. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/tests/test_errors.py +0 -0
  72. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/tests/test_image.png +0 -0
  73. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/tests/test_integration.py +0 -0
  74. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/tests/test_message_parser.py +0 -0
  75. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/tests/test_session.py +0 -0
  76. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/tests/test_streaming_client.py +0 -0
  77. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/tests/test_subprocess_buffering.py +0 -0
  78. {clawd_code_sdk-1.0.3 → clawd_code_sdk-1.0.5}/tests/test_tool_callbacks.py +0 -0
  79. {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
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "clawd-code-sdk"
3
- version = "1.0.3"
3
+ version = "1.0.5"
4
4
  description = "Python SDK for Claude Code"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
@@ -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.permission_prompt_tool_name:
473
- cmd.extend(["--permission-prompt-tool", options.permission_prompt_tool_name])
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
- if self.options.can_use_tool:
98
- # Automatically set permission_prompt_tool_name to "stdio" for control protocol
99
- options = replace(self.options, permission_prompt_tool_name="stdio")
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=self.options.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.options import ListSessionsOptions
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
- entry: dict[str, object] = anyenv.load_json(stripped, return_type=dict)
57
- branch = entry.get("gitBranch") or entry.get("git_branch")
58
- if isinstance(branch, str) and branch:
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: dict[str, object] = anyenv.load_json(line, return_type=dict)
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
- entry = anyenv.load_json(line, return_type=dict)
97
- msg = entry.get("message")
98
- if (
99
- isinstance(msg, dict)
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
- directory = opts.get("dir")
174
+ dir_ = opts.get("dir")
218
175
  limit = opts.get("limit")
219
-
220
176
  # Collect session files
221
- session_files: list[tuple[Path, str | None]]
222
- if directory is not None:
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 CallToolRequest, CallToolRequestParams, CallToolResult, ListToolsRequest
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": {"tools": {}}, # Tools capability without listChanged
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, TypedDict, assert_never
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
- permission_prompt_tool_name: str | None = None
147
- """MCP tool to handle permission prompts."""
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
- When set, the SDK automatically adds ``--permission-prompt-tool stdio`` to
152
- the CLI, which tells the CLI to route permission requests through the
153
- control protocol to this callback.
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 callback.
158
- - ``"acceptEdits"``: All tool calls are routed to this callback.
159
- The callback is responsible for implementing the auto-approve-edits policy.
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 callback.
162
- - ``"dontAsk"``: This callback is NEVER invoked. The CLI auto-denies all
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 callback is NEVER invoked. The CLI
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 can_use_tool (if set) for backwards compatibility.
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 has_sandbox and Path(path).exists():
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 has_sandbox:
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 has_sandbox:
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."""