clawd-code-sdk 1.0.4__tar.gz → 1.0.6__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.4 → clawd_code_sdk-1.0.6}/PKG-INFO +1 -1
  2. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/pyproject.toml +1 -1
  3. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/_internal/transport/subprocess_cli.py +2 -2
  4. clawd_code_sdk-1.0.6/src/clawd_code_sdk/anthropic_types.py +116 -0
  5. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/client.py +7 -4
  6. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/mcp_utils.py +107 -4
  7. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/models/content_blocks.py +1 -5
  8. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/models/options.py +43 -77
  9. clawd_code_sdk-1.0.6/tests/e2e/test_mcp_resources.py +72 -0
  10. clawd_code_sdk-1.0.6/tests/e2e/test_sdk_mcp_resources.py +78 -0
  11. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/tests/e2e/test_tool_permissions.py +1 -1
  12. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/tests/mcp_server.py +12 -0
  13. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/tests/test_sdk_mcp_integration.py +138 -5
  14. clawd_code_sdk-1.0.4/src/clawd_code_sdk/anthropic_types.py +0 -39
  15. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/.gitignore +0 -0
  16. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/LICENSE +0 -0
  17. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/README.md +0 -0
  18. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/__init__.py +0 -0
  19. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/_bundled/.gitignore +0 -0
  20. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/_errors.py +0 -0
  21. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/_internal/__init__.py +0 -0
  22. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/_internal/message_parser.py +0 -0
  23. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/_internal/query.py +0 -0
  24. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/_internal/transport/__init__.py +0 -0
  25. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/_version.py +0 -0
  26. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/list_sessions.py +0 -0
  27. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/models/__init__.py +0 -0
  28. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/models/agents.py +0 -0
  29. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/models/base.py +0 -0
  30. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/models/control.py +0 -0
  31. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/models/hooks.py +0 -0
  32. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/models/input_types.py +0 -0
  33. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/models/mcp.py +0 -0
  34. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/models/messages.py +0 -0
  35. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/models/output_types.py +0 -0
  36. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/models/permissions.py +0 -0
  37. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/models/prompt_requests.py +0 -0
  38. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/models/prompts.py +0 -0
  39. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/models/server_info.py +0 -0
  40. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/models/session.py +0 -0
  41. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/models/settings.py +0 -0
  42. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/models/system_messages.py +0 -0
  43. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/models/thinking.py +0 -0
  44. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/py.typed +0 -0
  45. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/query.py +0 -0
  46. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/session.py +0 -0
  47. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/storage/ARCHITECTURE.md +0 -0
  48. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/storage/__init__.py +0 -0
  49. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/storage/helpers.py +0 -0
  50. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/storage/models.py +0 -0
  51. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/storage/replay.py +0 -0
  52. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/usage.py +0 -0
  53. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/tests/__init__.py +0 -0
  54. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/tests/conftest.py +0 -0
  55. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/tests/e2e/__init__.py +0 -0
  56. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/tests/e2e/test_agents_and_settings.py +0 -0
  57. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/tests/e2e/test_dynamic_control.py +0 -0
  58. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/tests/e2e/test_hook_events.py +0 -0
  59. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/tests/e2e/test_hooks.py +0 -0
  60. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/tests/e2e/test_include_partial_messages.py +0 -0
  61. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/tests/e2e/test_mcp_tools.py +0 -0
  62. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/tests/e2e/test_sdk_mcp_tools.py +0 -0
  63. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/tests/e2e/test_slash_commands.py +0 -0
  64. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/tests/e2e/test_stderr_callback.py +0 -0
  65. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/tests/e2e/test_storage_parsing.py +0 -0
  66. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/tests/e2e/test_structured_output.py +0 -0
  67. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/tests/e2e/test_subagent_invocation.py +0 -0
  68. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/tests/mock_claude_server.py +0 -0
  69. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/tests/test_changelog.py +0 -0
  70. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/tests/test_client.py +0 -0
  71. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/tests/test_errors.py +0 -0
  72. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/tests/test_image.png +0 -0
  73. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/tests/test_integration.py +0 -0
  74. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/tests/test_message_parser.py +0 -0
  75. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/tests/test_session.py +0 -0
  76. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/tests/test_streaming_client.py +0 -0
  77. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/tests/test_subprocess_buffering.py +0 -0
  78. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/tests/test_tool_callbacks.py +0 -0
  79. {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/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.4
3
+ Version: 1.0.6
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.4"
3
+ version = "1.0.6"
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,116 @@
1
+ """Anthropic SDK types for tool result content blocks.
2
+
3
+ This module defines a discriminated union of all possible content types
4
+ that can appear in tool results from the Anthropic SDK. These types are
5
+ used to provide proper typing for ToolResultBlock.content.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Annotated
11
+
12
+ from anthropic.types.beta import (
13
+ BetaBashCodeExecutionResultBlock,
14
+ BetaBashCodeExecutionToolResultError,
15
+ BetaCodeExecutionResultBlock,
16
+ BetaCodeExecutionToolResultError,
17
+ BetaImageBlockParam,
18
+ BetaTextBlock,
19
+ BetaTextEditorCodeExecutionCreateResultBlock,
20
+ BetaTextEditorCodeExecutionStrReplaceResultBlock,
21
+ BetaTextEditorCodeExecutionToolResultError,
22
+ BetaTextEditorCodeExecutionViewResultBlock,
23
+ BetaToolReferenceBlock,
24
+ BetaToolSearchToolResultError,
25
+ BetaToolSearchToolSearchResultBlock,
26
+ BetaWebFetchBlock,
27
+ BetaWebFetchToolResultErrorBlock,
28
+ BetaWebSearchResultBlock,
29
+ BetaWebSearchToolResultError,
30
+ )
31
+ from pydantic import BaseModel, Field, TypeAdapter
32
+
33
+
34
+ # Union of all possible content types that can appear in tool results.
35
+ # These are the inner content blocks, not the outer tool result wrapper.
36
+ # Discriminated by the "type" field.
37
+ #
38
+ # Type discriminator values:
39
+ # - "text" -> TextBlock
40
+ # - "image" -> BetaImageBlockParam (TypedDict, no Pydantic model exists)
41
+ # - "tool_reference" -> BetaToolReferenceBlock
42
+ # - "tool_search_tool_search_result" -> BetaToolSearchToolSearchResultBlock
43
+ # - "tool_search_tool_result_error" -> BetaToolSearchToolResultError
44
+ # - "web_search_result" -> WebSearchResultBlock
45
+ # - "web_search_tool_result_error" -> WebSearchToolResultError
46
+ # - "web_fetch_result" -> BetaWebFetchBlock
47
+ # - "web_fetch_tool_result_error" -> BetaWebFetchToolResultErrorBlock
48
+ # - "code_execution_result" -> BetaCodeExecutionResultBlock
49
+ # - "code_execution_tool_result_error" -> BetaCodeExecutionToolResultError
50
+ # - "bash_code_execution_result" -> BetaBashCodeExecutionResultBlock
51
+ # - "bash_code_execution_tool_result_error" -> BetaBashCodeExecutionToolResultError
52
+ # - "text_editor_code_execution_view_result" -> BetaTextEditorCodeExecutionViewResultBlock
53
+ # - "text_editor_code_execution_create_result" -> BetaTextEditorCodeExecutionCreateResultBlock
54
+ # - "text_editor_code_execution_str_replace_result" -> BetaTextEditorCodeExecutionStrReplaceResultBlock # noqa: E501
55
+ # - "text_editor_code_execution_tool_result_error" -> BetaTextEditorCodeExecutionToolResultError
56
+ ToolResultContentBlock = Annotated[
57
+ BetaTextBlock
58
+ | BetaImageBlockParam
59
+ | BetaToolReferenceBlock
60
+ | BetaToolSearchToolSearchResultBlock
61
+ | BetaToolSearchToolResultError
62
+ | BetaWebSearchResultBlock
63
+ | BetaWebSearchToolResultError
64
+ | BetaWebFetchBlock
65
+ | BetaWebFetchToolResultErrorBlock
66
+ | BetaCodeExecutionResultBlock
67
+ | BetaCodeExecutionToolResultError
68
+ | BetaBashCodeExecutionResultBlock
69
+ | BetaBashCodeExecutionToolResultError
70
+ | BetaTextEditorCodeExecutionViewResultBlock
71
+ | BetaTextEditorCodeExecutionCreateResultBlock
72
+ | BetaTextEditorCodeExecutionStrReplaceResultBlock
73
+ | BetaTextEditorCodeExecutionToolResultError,
74
+ Field(discriminator="type"),
75
+ ]
76
+ _tool_result_content_adapter: TypeAdapter[list[ToolResultContentBlock]] | None = None
77
+
78
+
79
+ def _get_adapter() -> TypeAdapter[list[ToolResultContentBlock]]:
80
+ global _tool_result_content_adapter # noqa: PLW0603
81
+ if _tool_result_content_adapter is None:
82
+ # Force schema build for Anthropic models (deferred by default)
83
+ for model in [
84
+ BetaTextBlock,
85
+ BetaWebSearchResultBlock,
86
+ BetaWebSearchToolResultError,
87
+ BetaBashCodeExecutionResultBlock,
88
+ BetaBashCodeExecutionToolResultError,
89
+ BetaCodeExecutionResultBlock,
90
+ BetaCodeExecutionToolResultError,
91
+ BetaTextEditorCodeExecutionCreateResultBlock,
92
+ BetaTextEditorCodeExecutionStrReplaceResultBlock,
93
+ BetaTextEditorCodeExecutionToolResultError,
94
+ BetaTextEditorCodeExecutionViewResultBlock,
95
+ BetaToolReferenceBlock,
96
+ BetaToolSearchToolResultError,
97
+ BetaToolSearchToolSearchResultBlock,
98
+ BetaWebFetchBlock,
99
+ BetaWebFetchToolResultErrorBlock,
100
+ ]:
101
+ if isinstance(model, type) and issubclass(model, BaseModel):
102
+ model.model_rebuild()
103
+ _tool_result_content_adapter = TypeAdapter(list[ToolResultContentBlock])
104
+ return _tool_result_content_adapter
105
+
106
+
107
+ def validate_tool_result_content(content: list[dict[str, object]]) -> list[ToolResultContentBlock]:
108
+ """Validate and parse raw tool result content into typed blocks.
109
+
110
+ Args:
111
+ content: Raw list of content block dictionaries from CLI output
112
+
113
+ Returns:
114
+ List of validated and typed content blocks
115
+ """
116
+ 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,
@@ -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)
@@ -82,11 +82,7 @@ class ToolResultBlock(BaseContentBlock):
82
82
  return ""
83
83
  if isinstance(self.content, str):
84
84
  return self.content
85
- text_parts = [
86
- tc.get("text", "")
87
- for tc in self.content
88
- if isinstance(tc, dict) and tc.get("type") == "text"
89
- ]
85
+ text_parts = [tc.get("text", "") for tc in self.content if tc.get("type") == "text"]
90
86
  return "\n".join(text_parts)
91
87
 
92
88
 
@@ -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."""
@@ -0,0 +1,72 @@
1
+ """E2E tests for MCP resource support.
2
+
3
+ Tests whether MCP resources can be listed and read through external MCP servers.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from pathlib import Path
9
+ import sys
10
+
11
+ import pytest
12
+
13
+ from clawd_code_sdk import (
14
+ AssistantMessage,
15
+ ClaudeAgentOptions,
16
+ ClaudeSDKClient,
17
+ ResultMessage,
18
+ )
19
+ from clawd_code_sdk.models.mcp import McpStdioServerConfig
20
+
21
+
22
+ @pytest.mark.e2e
23
+ async def test_external_mcp_resource_list_and_read():
24
+ """Test that resources from an external MCP server can be listed and read."""
25
+ mcp_server_path = str(Path(__file__).parent.parent / "mcp_server.py")
26
+
27
+ options = ClaudeAgentOptions(
28
+ mcp_servers={
29
+ "res_test": McpStdioServerConfig(
30
+ command=sys.executable,
31
+ args=[mcp_server_path],
32
+ ),
33
+ },
34
+ permission_mode="bypassPermissions",
35
+ allow_dangerously_skip_permissions=True,
36
+ allowed_tools=[
37
+ "ListMcpResources",
38
+ "ReadMcpResource",
39
+ ],
40
+ max_turns=5,
41
+ enable_tool_search=False,
42
+ )
43
+
44
+ async with ClaudeSDKClient(options=options) as client:
45
+ await client.query(
46
+ "First, use the ListMcpResources tool to list available MCP resources. "
47
+ "Then use ReadMcpResource to read the 'test://greeting' resource from "
48
+ "the 'res_test' server. Report what you find."
49
+ )
50
+ messages = [msg async for msg in client.receive_response()]
51
+
52
+ # Check we got a result
53
+ result_messages = [m for m in messages if isinstance(m, ResultMessage)]
54
+ assert result_messages, f"No ResultMessage. Got: {[type(m).__name__ for m in messages]}"
55
+ assert not result_messages[0].is_error
56
+
57
+ # Look for evidence that resources were accessed
58
+ all_text = []
59
+ for msg in messages:
60
+ if isinstance(msg, AssistantMessage):
61
+ for block in msg.content:
62
+ if hasattr(block, "text"):
63
+ all_text.append(block.text) # noqa: PERF401
64
+
65
+ combined = " ".join(all_text).lower()
66
+ assert "hello from mcp resource" in combined or "greeting" in combined, (
67
+ f"Expected resource content in response. Got: {combined[:500]}"
68
+ )
69
+
70
+
71
+ if __name__ == "__main__":
72
+ pytest.main([__file__, "-vv", "-m", "e2e"])
@@ -0,0 +1,78 @@
1
+ """E2E tests for SDK MCP resource support (in-process servers).
2
+
3
+ Tests whether MCP resources registered on SDK servers can be listed and read.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import pytest
9
+
10
+ from clawd_code_sdk import (
11
+ AssistantMessage,
12
+ ClaudeAgentOptions,
13
+ ClaudeSDKClient,
14
+ ResultMessage,
15
+ )
16
+ from clawd_code_sdk.models import ToolUseBlock
17
+
18
+
19
+ @pytest.mark.e2e
20
+ @pytest.mark.skip(
21
+ reason="CLI does not route ListMcpResources/ReadMcpResource to SDK servers yet. "
22
+ "The SDK correctly advertises resources capability and routes resources/list + "
23
+ "resources/read, but the CLI only sends tools/list and tools/call for SDK servers."
24
+ )
25
+ async def test_sdk_mcp_resource_list_and_read():
26
+ """Test that resources on an SDK MCP server can be listed and read."""
27
+ from mcp.server.fastmcp import FastMCP
28
+
29
+ from clawd_code_sdk.models import McpSdkServerConfigWithInstance
30
+
31
+ mcp = FastMCP("sdk_res_test")
32
+
33
+ @mcp.resource("test://greeting")
34
+ def get_greeting() -> str:
35
+ return "Hello from SDK MCP resource!"
36
+
37
+ @mcp.tool()
38
+ def ping() -> str:
39
+ """Simple ping tool."""
40
+ return "pong"
41
+
42
+ server_config = McpSdkServerConfigWithInstance(
43
+ type="sdk", name="sdk_res", instance=mcp._mcp_server
44
+ )
45
+
46
+ options = ClaudeAgentOptions(
47
+ mcp_servers={"sdk_res": server_config},
48
+ permission_mode="bypassPermissions",
49
+ allow_dangerously_skip_permissions=True,
50
+ allowed_tools=["ListMcpResources", "ReadMcpResource", "mcp__sdk_res__ping"],
51
+ max_turns=5,
52
+ enable_tool_search=False,
53
+ )
54
+
55
+ async with ClaudeSDKClient(options=options) as client:
56
+ await client.query(
57
+ "Use ListMcpResources to list resources, then ReadMcpResource "
58
+ "to read 'test://greeting' from 'sdk_res'."
59
+ )
60
+ messages = [msg async for msg in client.receive_response()]
61
+
62
+ result_messages = [m for m in messages if isinstance(m, ResultMessage)]
63
+ assert result_messages
64
+ assert not result_messages[0].is_error
65
+
66
+ tool_uses = [
67
+ block.name
68
+ for msg in messages
69
+ if isinstance(msg, AssistantMessage)
70
+ for block in msg.content
71
+ if isinstance(block, ToolUseBlock)
72
+ ]
73
+ resource_tools_used = [t for t in tool_uses if "Resource" in t]
74
+ assert resource_tools_used, f"No resource tools used. Tools: {tool_uses}"
75
+
76
+
77
+ if __name__ == "__main__":
78
+ pytest.main([__file__, "-vv", "-m", "e2e"])
@@ -40,7 +40,7 @@ async def test_permission_callback_gets_called():
40
40
  callback_invocations.append((tool_name, input_data))
41
41
  return PermissionResultAllow()
42
42
 
43
- options = ClaudeAgentOptions(can_use_tool=permission_callback)
43
+ options = ClaudeAgentOptions(on_permission=permission_callback)
44
44
 
45
45
  try:
46
46
  async with ClaudeSDKClient(options=options) as client:
@@ -12,6 +12,18 @@ from fastmcp.utilities.types import Image
12
12
  mcp = FastMCP("Image Test Server")
13
13
 
14
14
 
15
+ @mcp.resource("test://greeting")
16
+ def get_greeting() -> str:
17
+ """A simple test resource that returns a greeting."""
18
+ return "Hello from MCP resource!"
19
+
20
+
21
+ @mcp.resource("test://data/{item_id}")
22
+ def get_data_item(item_id: str) -> str:
23
+ """A parameterized test resource."""
24
+ return f"Data for item: {item_id}"
25
+
26
+
15
27
  @mcp.tool
16
28
  async def get_test_image() -> Image:
17
29
  """Return a small test PNG image."""
@@ -7,7 +7,14 @@ matching the TypeScript SDK test/sdk.test.ts pattern.
7
7
  import base64
8
8
  from typing import Any
9
9
 
10
- from mcp.types import CallToolRequest, CallToolRequestParams, ToolAnnotations
10
+ from mcp.types import (
11
+ CallToolRequest,
12
+ CallToolRequestParams,
13
+ EmbeddedResource,
14
+ ImageContent,
15
+ TextContent,
16
+ ToolAnnotations,
17
+ )
11
18
  import pytest
12
19
 
13
20
  from clawd_code_sdk import ClaudeAgentOptions, create_sdk_mcp_server, tool
@@ -205,11 +212,11 @@ async def test_image_content_support():
205
212
  assert len(result.root.content) == 2
206
213
  # Check text content
207
214
  text_content = result.root.content[0]
208
- assert text_content.type == "text"
215
+ assert isinstance(text_content, TextContent)
209
216
  assert text_content.text == "Generated chart: Sales Report"
210
217
  # Check image content
211
218
  image_content = result.root.content[1]
212
- assert image_content.type == "image"
219
+ assert isinstance(image_content, ImageContent)
213
220
  assert image_content.data == png_data
214
221
  assert image_content.mimeType == "image/png"
215
222
  # Verify the tool was executed correctly
@@ -262,11 +269,11 @@ async def test_document_content_support():
262
269
  assert len(result.root.content) == 2
263
270
  # Check text content
264
271
  text_content = result.root.content[0]
265
- assert text_content.type == "text"
272
+ assert isinstance(text_content, TextContent)
266
273
  assert text_content.text == "Document: report.pdf"
267
274
  # Check document content (stored as EmbeddedResource with BlobResourceContents)
268
275
  doc_content = result.root.content[1]
269
- assert doc_content.type == "resource"
276
+ assert isinstance(doc_content, EmbeddedResource)
270
277
  assert hasattr(doc_content, "resource")
271
278
  assert str(doc_content.resource.uri) == "document://base64"
272
279
  assert doc_content.resource.mimeType == "application/pdf"
@@ -430,5 +437,131 @@ async def test_tool_annotations_in_jsonrpc():
430
437
  assert "annotations" not in tools_by_name["plain_tool"]
431
438
 
432
439
 
440
+ async def test_process_mcp_request_resources_list():
441
+ """Test that process_mcp_request routes resources/list correctly."""
442
+ from mcp.server.fastmcp import FastMCP
443
+
444
+ from clawd_code_sdk.mcp_utils import process_mcp_request
445
+
446
+ mcp = FastMCP("res_test")
447
+
448
+ @mcp.resource("test://greeting")
449
+ def get_greeting() -> str:
450
+ return "Hello from resource!"
451
+
452
+ server = mcp._mcp_server
453
+
454
+ # Test resources/list
455
+ msg = {"jsonrpc": "2.0", "id": 1, "method": "resources/list"}
456
+ resp = await process_mcp_request(msg, server)
457
+ assert resp.get("id") == 1
458
+ assert "result" in resp
459
+ resources = resp["result"]["resources"]
460
+ assert len(resources) == 1
461
+ assert resources[0]["name"] == "get_greeting"
462
+ assert str(resources[0]["uri"]) == "test://greeting"
463
+
464
+
465
+ async def test_process_mcp_request_resources_read():
466
+ """Test that process_mcp_request routes resources/read correctly."""
467
+ from mcp.server.fastmcp import FastMCP
468
+
469
+ from clawd_code_sdk.mcp_utils import process_mcp_request
470
+
471
+ mcp = FastMCP("res_test")
472
+
473
+ @mcp.resource("test://greeting")
474
+ def get_greeting() -> str:
475
+ return "Hello from resource!"
476
+
477
+ server = mcp._mcp_server
478
+
479
+ msg = JSONRPCRequest(
480
+ jsonrpc="2.0",
481
+ id=2,
482
+ method="resources/read",
483
+ params={"uri": "test://greeting"},
484
+ )
485
+ resp = await process_mcp_request(msg, server)
486
+ assert resp.get("id") == 2
487
+ assert "result" in resp
488
+ contents = resp["result"]["contents"]
489
+ assert len(contents) == 1
490
+ assert contents[0]["text"] == "Hello from resource!"
491
+ assert contents[0]["mimeType"] == "text/plain"
492
+
493
+
494
+ async def test_process_mcp_request_resource_templates_list():
495
+ """Test that process_mcp_request routes resources/templates/list correctly."""
496
+ from mcp.server.fastmcp import FastMCP
497
+
498
+ from clawd_code_sdk.mcp_utils import process_mcp_request
499
+
500
+ mcp = FastMCP("res_test")
501
+
502
+ @mcp.resource("test://data/{item_id}")
503
+ def get_data(item_id: str) -> str:
504
+ return f"Data for {item_id}"
505
+
506
+ server = mcp._mcp_server
507
+
508
+ msg = JSONRPCRequest(jsonrpc="2.0", id=3, method="resources/templates/list")
509
+ resp = await process_mcp_request(msg, server)
510
+ assert resp.get("id") == 3
511
+ assert "result" in resp
512
+ templates = resp["result"]["resourceTemplates"]
513
+ assert len(templates) == 1
514
+ assert "test://data/{item_id}" in templates[0]["uriTemplate"]
515
+
516
+
517
+ async def test_detect_capabilities():
518
+ """Test that _detect_capabilities correctly detects server capabilities."""
519
+ from mcp.server import Server
520
+ from mcp.server.fastmcp import FastMCP
521
+
522
+ from clawd_code_sdk.mcp_utils import _detect_capabilities
523
+
524
+ # Empty server
525
+ empty = Server("empty")
526
+ caps = _detect_capabilities(empty)
527
+ assert caps == {}
528
+
529
+ # Server with tools + resources + prompts
530
+ mcp = FastMCP("full")
531
+
532
+ @mcp.resource("test://r")
533
+ def res() -> str:
534
+ return "r"
535
+
536
+ @mcp.tool()
537
+ def t() -> str:
538
+ return "t"
539
+
540
+ caps = _detect_capabilities(mcp._mcp_server)
541
+ assert "tools" in caps
542
+ assert "resources" in caps
543
+ assert "prompts" in caps # FastMCP registers prompt handlers by default
544
+
545
+
546
+ async def test_process_mcp_request_initialize_advertises_capabilities():
547
+ """Test that initialize response includes detected capabilities."""
548
+ from mcp.server.fastmcp import FastMCP
549
+
550
+ from clawd_code_sdk.mcp_utils import process_mcp_request
551
+
552
+ mcp = FastMCP("cap_test")
553
+
554
+ @mcp.resource("test://r")
555
+ def res() -> str:
556
+ return "r"
557
+
558
+ server = mcp._mcp_server
559
+ msg = JSONRPCRequest(jsonrpc="2.0", id=1, method="initialize", params={})
560
+ resp = await process_mcp_request(msg, server)
561
+ caps = resp["result"]["capabilities"]
562
+ assert "resources" in caps
563
+ assert "tools" in caps
564
+
565
+
433
566
  if __name__ == "__main__":
434
567
  pytest.main(["-v", __file__])
@@ -1,39 +0,0 @@
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)
File without changes
File without changes