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.
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/PKG-INFO +1 -1
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/pyproject.toml +1 -1
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/_internal/transport/subprocess_cli.py +2 -2
- clawd_code_sdk-1.0.6/src/clawd_code_sdk/anthropic_types.py +116 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/client.py +7 -4
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/mcp_utils.py +107 -4
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/models/content_blocks.py +1 -5
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/models/options.py +43 -77
- clawd_code_sdk-1.0.6/tests/e2e/test_mcp_resources.py +72 -0
- clawd_code_sdk-1.0.6/tests/e2e/test_sdk_mcp_resources.py +78 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/tests/e2e/test_tool_permissions.py +1 -1
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/tests/mcp_server.py +12 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/tests/test_sdk_mcp_integration.py +138 -5
- clawd_code_sdk-1.0.4/src/clawd_code_sdk/anthropic_types.py +0 -39
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/.gitignore +0 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/LICENSE +0 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/README.md +0 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/__init__.py +0 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/_bundled/.gitignore +0 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/_errors.py +0 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/_internal/__init__.py +0 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/_internal/message_parser.py +0 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/_internal/query.py +0 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/_internal/transport/__init__.py +0 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/_version.py +0 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/list_sessions.py +0 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/models/__init__.py +0 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/models/agents.py +0 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/models/base.py +0 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/models/control.py +0 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/models/hooks.py +0 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/models/input_types.py +0 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/models/mcp.py +0 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/models/messages.py +0 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/models/output_types.py +0 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/models/permissions.py +0 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/models/prompt_requests.py +0 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/models/prompts.py +0 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/models/server_info.py +0 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/models/session.py +0 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/models/settings.py +0 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/models/system_messages.py +0 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/models/thinking.py +0 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/py.typed +0 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/query.py +0 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/session.py +0 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/storage/ARCHITECTURE.md +0 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/storage/__init__.py +0 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/storage/helpers.py +0 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/storage/models.py +0 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/storage/replay.py +0 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/usage.py +0 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/tests/__init__.py +0 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/tests/conftest.py +0 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/tests/e2e/__init__.py +0 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/tests/e2e/test_agents_and_settings.py +0 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/tests/e2e/test_dynamic_control.py +0 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/tests/e2e/test_hook_events.py +0 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/tests/e2e/test_hooks.py +0 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/tests/e2e/test_include_partial_messages.py +0 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/tests/e2e/test_mcp_tools.py +0 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/tests/e2e/test_sdk_mcp_tools.py +0 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/tests/e2e/test_slash_commands.py +0 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/tests/e2e/test_stderr_callback.py +0 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/tests/e2e/test_storage_parsing.py +0 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/tests/e2e/test_structured_output.py +0 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/tests/e2e/test_subagent_invocation.py +0 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/tests/mock_claude_server.py +0 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/tests/test_changelog.py +0 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/tests/test_client.py +0 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/tests/test_errors.py +0 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/tests/test_image.png +0 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/tests/test_integration.py +0 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/tests/test_message_parser.py +0 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/tests/test_session.py +0 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/tests/test_streaming_client.py +0 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/tests/test_subprocess_buffering.py +0 -0
- {clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/tests/test_tool_callbacks.py +0 -0
- {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.
|
|
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
|
|
@@ -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,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
|
-
|
|
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,
|
|
@@ -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)
|
|
@@ -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,
|
|
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."""
|
|
@@ -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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/_internal/message_parser.py
RENAMED
|
File without changes
|
|
File without changes
|
{clawd_code_sdk-1.0.4 → clawd_code_sdk-1.0.6}/src/clawd_code_sdk/_internal/transport/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|