codebuddy-agent-sdk 0.1.11__py3-none-manylinux_2_17_x86_64.whl → 0.2.0__py3-none-manylinux_2_17_x86_64.whl
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.
Potentially problematic release.
This version of codebuddy-agent-sdk might be problematic. Click here for more details.
- codebuddy_agent_sdk/__init__.py +29 -0
- codebuddy_agent_sdk/_binary.py +2 -0
- codebuddy_agent_sdk/_errors.py +15 -0
- codebuddy_agent_sdk/_message_parser.py +10 -0
- codebuddy_agent_sdk/_version.py +1 -1
- codebuddy_agent_sdk/bin/codebuddy +0 -0
- codebuddy_agent_sdk/client.py +26 -13
- codebuddy_agent_sdk/mcp/__init__.py +35 -0
- codebuddy_agent_sdk/mcp/create_sdk_mcp_server.py +154 -0
- codebuddy_agent_sdk/mcp/sdk_control_server_transport.py +95 -0
- codebuddy_agent_sdk/mcp/types.py +300 -0
- codebuddy_agent_sdk/query.py +239 -20
- codebuddy_agent_sdk/transport/base.py +2 -0
- codebuddy_agent_sdk/transport/subprocess.py +6 -2
- codebuddy_agent_sdk/types.py +30 -3
- {codebuddy_agent_sdk-0.1.11.dist-info → codebuddy_agent_sdk-0.2.0.dist-info}/METADATA +1 -1
- codebuddy_agent_sdk-0.2.0.dist-info/RECORD +20 -0
- codebuddy_agent_sdk-0.1.11.dist-info/RECORD +0 -16
- {codebuddy_agent_sdk-0.1.11.dist-info → codebuddy_agent_sdk-0.2.0.dist-info}/WHEEL +0 -0
codebuddy_agent_sdk/__init__.py
CHANGED
|
@@ -5,10 +5,22 @@ from ._errors import (
|
|
|
5
5
|
CLIJSONDecodeError,
|
|
6
6
|
CLINotFoundError,
|
|
7
7
|
CodeBuddySDKError,
|
|
8
|
+
ExecutionError,
|
|
8
9
|
ProcessError,
|
|
9
10
|
)
|
|
10
11
|
from ._version import __version__
|
|
11
12
|
from .client import CodeBuddySDKClient
|
|
13
|
+
from .mcp import (
|
|
14
|
+
CallToolResult,
|
|
15
|
+
SdkControlServerTransport,
|
|
16
|
+
SdkMcpServerOptions,
|
|
17
|
+
SdkMcpServerResult,
|
|
18
|
+
SdkMcpToolDefinition,
|
|
19
|
+
TextContent,
|
|
20
|
+
ToolHandler,
|
|
21
|
+
create_sdk_mcp_server,
|
|
22
|
+
tool,
|
|
23
|
+
)
|
|
12
24
|
from .query import query
|
|
13
25
|
from .transport import Transport
|
|
14
26
|
from .types import (
|
|
@@ -22,12 +34,15 @@ from .types import (
|
|
|
22
34
|
CanUseToolOptions,
|
|
23
35
|
CodeBuddyAgentOptions,
|
|
24
36
|
ContentBlock,
|
|
37
|
+
ErrorMessage,
|
|
25
38
|
HookCallback,
|
|
26
39
|
HookContext,
|
|
27
40
|
HookEvent,
|
|
28
41
|
HookJSONOutput,
|
|
29
42
|
HookMatcher,
|
|
43
|
+
McpSdkServerConfig,
|
|
30
44
|
McpServerConfig,
|
|
45
|
+
McpStdioServerConfig,
|
|
31
46
|
Message,
|
|
32
47
|
PermissionMode,
|
|
33
48
|
PermissionResult,
|
|
@@ -50,6 +65,16 @@ __all__ = [
|
|
|
50
65
|
"CodeBuddySDKClient",
|
|
51
66
|
"Transport",
|
|
52
67
|
"__version__",
|
|
68
|
+
# MCP Server API
|
|
69
|
+
"create_sdk_mcp_server",
|
|
70
|
+
"tool",
|
|
71
|
+
"SdkControlServerTransport",
|
|
72
|
+
"SdkMcpServerOptions",
|
|
73
|
+
"SdkMcpServerResult",
|
|
74
|
+
"SdkMcpToolDefinition",
|
|
75
|
+
"ToolHandler",
|
|
76
|
+
"CallToolResult",
|
|
77
|
+
"TextContent",
|
|
53
78
|
# Types - Permission
|
|
54
79
|
"PermissionMode",
|
|
55
80
|
# Types - Messages
|
|
@@ -59,6 +84,7 @@ __all__ = [
|
|
|
59
84
|
"SystemMessage",
|
|
60
85
|
"ResultMessage",
|
|
61
86
|
"StreamEvent",
|
|
87
|
+
"ErrorMessage",
|
|
62
88
|
# Types - Content blocks
|
|
63
89
|
"ContentBlock",
|
|
64
90
|
"TextBlock",
|
|
@@ -88,10 +114,13 @@ __all__ = [
|
|
|
88
114
|
"HookContext",
|
|
89
115
|
# Types - MCP
|
|
90
116
|
"McpServerConfig",
|
|
117
|
+
"McpStdioServerConfig",
|
|
118
|
+
"McpSdkServerConfig",
|
|
91
119
|
# Errors
|
|
92
120
|
"CodeBuddySDKError",
|
|
93
121
|
"CLIConnectionError",
|
|
94
122
|
"CLINotFoundError",
|
|
95
123
|
"CLIJSONDecodeError",
|
|
96
124
|
"ProcessError",
|
|
125
|
+
"ExecutionError",
|
|
97
126
|
]
|
codebuddy_agent_sdk/_binary.py
CHANGED
codebuddy_agent_sdk/_errors.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"""Error definitions for CodeBuddy Agent SDK."""
|
|
2
2
|
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
3
5
|
|
|
4
6
|
class CodeBuddySDKError(Exception):
|
|
5
7
|
"""Base exception for CodeBuddy SDK errors."""
|
|
@@ -37,3 +39,16 @@ class ProcessError(CodeBuddySDKError):
|
|
|
37
39
|
"""Raised when CLI process encounters an error."""
|
|
38
40
|
|
|
39
41
|
pass
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ExecutionError(CodeBuddySDKError):
|
|
45
|
+
"""Raised when execution fails (e.g., authentication error, API error).
|
|
46
|
+
|
|
47
|
+
Contains the errors array from the ResultMessage.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(self, errors: list[str], subtype: str):
|
|
51
|
+
message = errors[0] if errors else "Execution failed"
|
|
52
|
+
super().__init__(message)
|
|
53
|
+
self.errors = errors
|
|
54
|
+
self.subtype = subtype
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
"""Message parser for CLI output."""
|
|
2
2
|
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
3
5
|
from typing import Any
|
|
4
6
|
|
|
5
7
|
from .types import (
|
|
6
8
|
AssistantMessage,
|
|
7
9
|
ContentBlock,
|
|
10
|
+
ErrorMessage,
|
|
8
11
|
Message,
|
|
9
12
|
ResultMessage,
|
|
10
13
|
StreamEvent,
|
|
@@ -99,6 +102,7 @@ def parse_message(data: dict[str, Any]) -> Message | None:
|
|
|
99
102
|
total_cost_usd=data.get("total_cost_usd"),
|
|
100
103
|
usage=data.get("usage"),
|
|
101
104
|
result=data.get("result"),
|
|
105
|
+
errors=data.get("errors"),
|
|
102
106
|
)
|
|
103
107
|
|
|
104
108
|
if msg_type == "stream_event":
|
|
@@ -109,4 +113,10 @@ def parse_message(data: dict[str, Any]) -> Message | None:
|
|
|
109
113
|
parent_tool_use_id=data.get("parent_tool_use_id"),
|
|
110
114
|
)
|
|
111
115
|
|
|
116
|
+
if msg_type == "error":
|
|
117
|
+
return ErrorMessage(
|
|
118
|
+
error=data.get("error", ""),
|
|
119
|
+
session_id=data.get("session_id"),
|
|
120
|
+
)
|
|
121
|
+
|
|
112
122
|
return None
|
codebuddy_agent_sdk/_version.py
CHANGED
|
Binary file
|
codebuddy_agent_sdk/client.py
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
"""CodeBuddy SDK Client for interactive conversations."""
|
|
2
2
|
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
3
5
|
import json
|
|
4
6
|
import os
|
|
5
7
|
from collections.abc import AsyncIterable, AsyncIterator
|
|
8
|
+
from types import TracebackType
|
|
6
9
|
from typing import Any
|
|
7
10
|
|
|
8
|
-
from ._errors import CLIConnectionError
|
|
11
|
+
from ._errors import CLIConnectionError, ExecutionError
|
|
9
12
|
from ._message_parser import parse_message
|
|
10
13
|
from .transport import SubprocessTransport, Transport
|
|
11
|
-
from .types import CanUseToolOptions, CodeBuddyAgentOptions, Message, ResultMessage
|
|
14
|
+
from .types import CanUseToolOptions, CodeBuddyAgentOptions, ErrorMessage, Message, ResultMessage
|
|
12
15
|
|
|
13
16
|
|
|
14
17
|
class CodeBuddySDKClient:
|
|
@@ -47,9 +50,7 @@ class CodeBuddySDKClient:
|
|
|
47
50
|
|
|
48
51
|
os.environ["CODEBUDDY_CODE_ENTRYPOINT"] = "sdk-py-client"
|
|
49
52
|
|
|
50
|
-
async def connect(
|
|
51
|
-
self, prompt: str | AsyncIterable[dict[str, Any]] | None = None
|
|
52
|
-
) -> None:
|
|
53
|
+
async def connect(self, prompt: str | AsyncIterable[dict[str, Any]] | None = None) -> None:
|
|
53
54
|
"""Connect to CodeBuddy with an optional initial prompt."""
|
|
54
55
|
if self._custom_transport:
|
|
55
56
|
self._transport = self._custom_transport
|
|
@@ -150,9 +151,7 @@ class CodeBuddySDKClient:
|
|
|
150
151
|
}
|
|
151
152
|
await self._transport.write(json.dumps(response))
|
|
152
153
|
|
|
153
|
-
async def _handle_permission_request(
|
|
154
|
-
self, request_id: str, request: dict[str, Any]
|
|
155
|
-
) -> None:
|
|
154
|
+
async def _handle_permission_request(self, request_id: str, request: dict[str, Any]) -> None:
|
|
156
155
|
"""Handle permission request from CLI."""
|
|
157
156
|
if not self._transport:
|
|
158
157
|
return
|
|
@@ -234,14 +233,23 @@ class CodeBuddySDKClient:
|
|
|
234
233
|
|
|
235
234
|
async def receive_response(self) -> AsyncIterator[Message]:
|
|
236
235
|
"""
|
|
237
|
-
Receive messages until and including a ResultMessage.
|
|
236
|
+
Receive messages until and including a ResultMessage or ErrorMessage.
|
|
238
237
|
|
|
239
238
|
Yields each message as it's received and terminates after
|
|
240
|
-
yielding a ResultMessage.
|
|
239
|
+
yielding a ResultMessage or ErrorMessage.
|
|
240
|
+
Raises ExecutionError if ResultMessage indicates an error.
|
|
241
241
|
"""
|
|
242
242
|
async for message in self.receive_messages():
|
|
243
|
-
|
|
243
|
+
# Check for execution error BEFORE yielding
|
|
244
244
|
if isinstance(message, ResultMessage):
|
|
245
|
+
if message.is_error and message.errors and len(message.errors) > 0:
|
|
246
|
+
raise ExecutionError(message.errors, message.subtype)
|
|
247
|
+
yield message
|
|
248
|
+
return
|
|
249
|
+
|
|
250
|
+
yield message
|
|
251
|
+
|
|
252
|
+
if isinstance(message, ErrorMessage):
|
|
245
253
|
return
|
|
246
254
|
|
|
247
255
|
async def interrupt(self) -> None:
|
|
@@ -287,12 +295,17 @@ class CodeBuddySDKClient:
|
|
|
287
295
|
self._transport = None
|
|
288
296
|
self._connected = False
|
|
289
297
|
|
|
290
|
-
async def __aenter__(self) ->
|
|
298
|
+
async def __aenter__(self) -> CodeBuddySDKClient:
|
|
291
299
|
"""Enter async context - automatically connects."""
|
|
292
300
|
await self.connect()
|
|
293
301
|
return self
|
|
294
302
|
|
|
295
|
-
async def __aexit__(
|
|
303
|
+
async def __aexit__(
|
|
304
|
+
self,
|
|
305
|
+
exc_type: type[BaseException] | None,
|
|
306
|
+
exc_val: BaseException | None,
|
|
307
|
+
exc_tb: TracebackType | None,
|
|
308
|
+
) -> bool:
|
|
296
309
|
"""Exit async context - always disconnects."""
|
|
297
310
|
await self.disconnect()
|
|
298
311
|
return False
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MCP (Model Context Protocol) Integration
|
|
3
|
+
|
|
4
|
+
This module provides utilities for creating and managing SDK MCP servers
|
|
5
|
+
that can be integrated with the CLI via the control protocol.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .create_sdk_mcp_server import (
|
|
9
|
+
create_sdk_mcp_server,
|
|
10
|
+
tool,
|
|
11
|
+
)
|
|
12
|
+
from .sdk_control_server_transport import SdkControlServerTransport
|
|
13
|
+
from .types import (
|
|
14
|
+
CallToolResult,
|
|
15
|
+
SdkMcpServerOptions,
|
|
16
|
+
SdkMcpServerResult,
|
|
17
|
+
SdkMcpToolDefinition,
|
|
18
|
+
TextContent,
|
|
19
|
+
ToolHandler,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
# Factory functions
|
|
24
|
+
"create_sdk_mcp_server",
|
|
25
|
+
"tool",
|
|
26
|
+
# Transport
|
|
27
|
+
"SdkControlServerTransport",
|
|
28
|
+
# Types
|
|
29
|
+
"SdkMcpServerOptions",
|
|
30
|
+
"SdkMcpServerResult",
|
|
31
|
+
"SdkMcpToolDefinition",
|
|
32
|
+
"ToolHandler",
|
|
33
|
+
"CallToolResult",
|
|
34
|
+
"TextContent",
|
|
35
|
+
]
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""
|
|
2
|
+
create_sdk_mcp_server - Create an SDK MCP Server
|
|
3
|
+
|
|
4
|
+
This function creates an MCP server that can be integrated into the SDK
|
|
5
|
+
and used with the CLI via the control protocol.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections.abc import Awaitable, Callable
|
|
11
|
+
from typing import Any, TypeVar
|
|
12
|
+
|
|
13
|
+
from .types import (
|
|
14
|
+
CallToolResult,
|
|
15
|
+
SdkMcpServer,
|
|
16
|
+
SdkMcpServerOptions,
|
|
17
|
+
SdkMcpServerResult,
|
|
18
|
+
SdkMcpToolDefinition,
|
|
19
|
+
ToolInputSchema,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
# Tool handler type
|
|
23
|
+
ToolHandler = Callable[[dict[str, Any]], CallToolResult | Awaitable[CallToolResult]]
|
|
24
|
+
|
|
25
|
+
# Type variable for the decorated function
|
|
26
|
+
F = TypeVar("F", bound=ToolHandler)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _python_type_to_json_schema(py_type: type) -> dict[str, Any]:
|
|
30
|
+
"""Convert Python type to JSON Schema type."""
|
|
31
|
+
type_mapping = {
|
|
32
|
+
str: {"type": "string"},
|
|
33
|
+
int: {"type": "integer"},
|
|
34
|
+
float: {"type": "number"},
|
|
35
|
+
bool: {"type": "boolean"},
|
|
36
|
+
list: {"type": "array"},
|
|
37
|
+
dict: {"type": "object"},
|
|
38
|
+
}
|
|
39
|
+
return type_mapping.get(py_type, {"type": "string"})
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _convert_schema(input_schema: dict[str, Any]) -> tuple[dict[str, dict[str, Any]], list[str]]:
|
|
43
|
+
"""
|
|
44
|
+
Convert input schema to JSON Schema format.
|
|
45
|
+
|
|
46
|
+
Supports multiple formats:
|
|
47
|
+
1. Simple type mapping: {"latitude": float, "longitude": float}
|
|
48
|
+
2. JSON Schema format: {"properties": {...}, "required": [...]}
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Tuple of (properties dict, required list)
|
|
52
|
+
"""
|
|
53
|
+
# Check if it's already in JSON Schema format
|
|
54
|
+
if "properties" in input_schema:
|
|
55
|
+
return input_schema.get("properties", {}), input_schema.get("required", [])
|
|
56
|
+
|
|
57
|
+
if "type" in input_schema and input_schema.get("type") == "object":
|
|
58
|
+
return input_schema.get("properties", {}), input_schema.get("required", [])
|
|
59
|
+
|
|
60
|
+
# Simple type mapping format: {"param_name": type}
|
|
61
|
+
properties = {}
|
|
62
|
+
required = []
|
|
63
|
+
|
|
64
|
+
for param_name, param_type in input_schema.items():
|
|
65
|
+
if isinstance(param_type, type):
|
|
66
|
+
properties[param_name] = _python_type_to_json_schema(param_type)
|
|
67
|
+
required.append(param_name)
|
|
68
|
+
elif isinstance(param_type, dict):
|
|
69
|
+
properties[param_name] = param_type
|
|
70
|
+
if "default" not in param_type:
|
|
71
|
+
required.append(param_name)
|
|
72
|
+
else:
|
|
73
|
+
properties[param_name] = {"type": "string"}
|
|
74
|
+
required.append(param_name)
|
|
75
|
+
|
|
76
|
+
return properties, required
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def tool(name: str, description: str, input_schema: dict[str, Any]) -> Callable[[F], F]:
|
|
80
|
+
"""
|
|
81
|
+
Decorator to define an MCP tool.
|
|
82
|
+
|
|
83
|
+
Example:
|
|
84
|
+
```python
|
|
85
|
+
@tool("get_weather", "Get current weather", {"latitude": float, "longitude": float})
|
|
86
|
+
async def get_weather(args: dict[str, Any]) -> dict[str, Any]:
|
|
87
|
+
return {"content": [{"type": "text", "text": f"Weather: sunny"}]}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
name: Tool name (unique within the server)
|
|
92
|
+
description: Tool description
|
|
93
|
+
input_schema: Input parameters schema. Supports:
|
|
94
|
+
- Simple types: {"latitude": float, "longitude": float}
|
|
95
|
+
- JSON Schema: {"properties": {...}, "required": [...]}
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Decorated function with tool metadata attached
|
|
99
|
+
"""
|
|
100
|
+
properties, required = _convert_schema(input_schema)
|
|
101
|
+
tool_schema = ToolInputSchema(type="object", properties=properties, required=required)
|
|
102
|
+
|
|
103
|
+
def decorator(func: F) -> F:
|
|
104
|
+
func._tool_definition = SdkMcpToolDefinition( # type: ignore[attr-defined]
|
|
105
|
+
name=name,
|
|
106
|
+
description=description,
|
|
107
|
+
input_schema=tool_schema,
|
|
108
|
+
handler=func,
|
|
109
|
+
)
|
|
110
|
+
return func
|
|
111
|
+
|
|
112
|
+
return decorator
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def create_sdk_mcp_server(
|
|
116
|
+
name: str,
|
|
117
|
+
version: str = "1.0.0",
|
|
118
|
+
tools: list[Callable[..., Any]] | None = None,
|
|
119
|
+
) -> SdkMcpServerResult:
|
|
120
|
+
"""
|
|
121
|
+
Create an SDK MCP Server.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
name: Server name (unique within the session)
|
|
125
|
+
version: Server version (defaults to "1.0.0")
|
|
126
|
+
tools: List of functions decorated with @tool
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
SDK MCP server result for use with query()
|
|
130
|
+
|
|
131
|
+
Example:
|
|
132
|
+
```python
|
|
133
|
+
from codebuddy_agent_sdk import create_sdk_mcp_server, tool, query
|
|
134
|
+
|
|
135
|
+
@tool("get_weather", "Get weather", {"location": str})
|
|
136
|
+
async def get_weather(args: dict) -> dict:
|
|
137
|
+
return {"content": [{"type": "text", "text": f"Weather: sunny"}]}
|
|
138
|
+
|
|
139
|
+
server = create_sdk_mcp_server(
|
|
140
|
+
name="my-server",
|
|
141
|
+
tools=[get_weather]
|
|
142
|
+
)
|
|
143
|
+
```
|
|
144
|
+
"""
|
|
145
|
+
tool_definitions: list[SdkMcpToolDefinition] = []
|
|
146
|
+
if tools:
|
|
147
|
+
for func in tools:
|
|
148
|
+
if not hasattr(func, "_tool_definition"):
|
|
149
|
+
raise ValueError(f"Function {func.__name__} is not decorated with @tool")
|
|
150
|
+
tool_definitions.append(func._tool_definition)
|
|
151
|
+
|
|
152
|
+
server = SdkMcpServer(SdkMcpServerOptions(name=name, version=version, tools=tool_definitions))
|
|
153
|
+
|
|
154
|
+
return SdkMcpServerResult(type="sdk", name=name, server=server)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SDK Control Server Transport
|
|
3
|
+
|
|
4
|
+
Custom transport implementation that bridges SDK MCP servers to CLI process.
|
|
5
|
+
This transport forwards MCP messages through the control protocol.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
|
|
12
|
+
from .types import JSONRPCMessage
|
|
13
|
+
|
|
14
|
+
# Callback function type for sending MCP messages to CLI
|
|
15
|
+
SendMcpMessageCallback = Callable[[JSONRPCMessage], None]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SdkControlServerTransport:
|
|
19
|
+
"""
|
|
20
|
+
SdkControlServerTransport - bridges MCP servers to CLI via control messages.
|
|
21
|
+
|
|
22
|
+
This transport implements a simple interface for forwarding MCP messages
|
|
23
|
+
between the SDK MCP server and the CLI via the control protocol.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, send_mcp_message: SendMcpMessageCallback):
|
|
27
|
+
"""
|
|
28
|
+
Create a new SDK Control Server Transport.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
send_mcp_message: Callback function to forward MCP messages to CLI
|
|
32
|
+
"""
|
|
33
|
+
self._send_mcp_message = send_mcp_message
|
|
34
|
+
self._is_closed = False
|
|
35
|
+
self._on_message: Callable[[JSONRPCMessage], None] | None = None
|
|
36
|
+
self._on_close: Callable[[], None] | None = None
|
|
37
|
+
self._on_error: Callable[[Exception], None] | None = None
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def closed(self) -> bool:
|
|
41
|
+
"""Check if the transport is closed."""
|
|
42
|
+
return self._is_closed
|
|
43
|
+
|
|
44
|
+
def set_on_message(self, callback: Callable[[JSONRPCMessage], None] | None) -> None:
|
|
45
|
+
"""Set the message callback."""
|
|
46
|
+
self._on_message = callback
|
|
47
|
+
|
|
48
|
+
def set_on_close(self, callback: Callable[[], None] | None) -> None:
|
|
49
|
+
"""Set the close callback."""
|
|
50
|
+
self._on_close = callback
|
|
51
|
+
|
|
52
|
+
def set_on_error(self, callback: Callable[[Exception], None] | None) -> None:
|
|
53
|
+
"""Set the error callback."""
|
|
54
|
+
self._on_error = callback
|
|
55
|
+
|
|
56
|
+
async def start(self) -> None:
|
|
57
|
+
"""
|
|
58
|
+
Start the transport.
|
|
59
|
+
No-op since connection is already established via stdio.
|
|
60
|
+
"""
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
async def send(self, message: JSONRPCMessage) -> None:
|
|
64
|
+
"""
|
|
65
|
+
Send a message to the CLI via control_request.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
message: The JSON-RPC message to send
|
|
69
|
+
"""
|
|
70
|
+
if self._is_closed:
|
|
71
|
+
raise RuntimeError("Transport is closed")
|
|
72
|
+
# Forward message to CLI via control_request
|
|
73
|
+
self._send_mcp_message(message)
|
|
74
|
+
|
|
75
|
+
async def close(self) -> None:
|
|
76
|
+
"""Close the transport."""
|
|
77
|
+
if self._is_closed:
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
self._is_closed = True
|
|
81
|
+
if self._on_close:
|
|
82
|
+
self._on_close()
|
|
83
|
+
|
|
84
|
+
def handle_incoming_message(self, message: JSONRPCMessage) -> None:
|
|
85
|
+
"""
|
|
86
|
+
Handle incoming message from CLI.
|
|
87
|
+
This method should be called when the CLI sends a message to this server.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
message: The JSON-RPC message from CLI
|
|
91
|
+
"""
|
|
92
|
+
if self._is_closed:
|
|
93
|
+
return
|
|
94
|
+
if self._on_message:
|
|
95
|
+
self._on_message(message)
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
"""Type definitions for SDK MCP Server."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
from collections.abc import Awaitable, Callable
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import TYPE_CHECKING, Any, Literal, TypedDict, cast
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from .sdk_control_server_transport import SdkControlServerTransport
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# ============= JSON-RPC Types =============
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class JSONRPCRequest(TypedDict, total=False):
|
|
18
|
+
"""JSON-RPC 2.0 request."""
|
|
19
|
+
|
|
20
|
+
jsonrpc: Literal["2.0"]
|
|
21
|
+
id: str | int
|
|
22
|
+
method: str
|
|
23
|
+
params: dict[str, Any] | list[Any] | None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class JSONRPCError(TypedDict, total=False):
|
|
27
|
+
"""JSON-RPC 2.0 error."""
|
|
28
|
+
|
|
29
|
+
code: int
|
|
30
|
+
message: str
|
|
31
|
+
data: Any
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class JSONRPCResponse(TypedDict, total=False):
|
|
35
|
+
"""JSON-RPC 2.0 response."""
|
|
36
|
+
|
|
37
|
+
jsonrpc: Literal["2.0"]
|
|
38
|
+
id: str | int | None
|
|
39
|
+
result: Any
|
|
40
|
+
error: JSONRPCError
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class JSONRPCNotification(TypedDict, total=False):
|
|
44
|
+
"""JSON-RPC 2.0 notification."""
|
|
45
|
+
|
|
46
|
+
jsonrpc: Literal["2.0"]
|
|
47
|
+
method: str
|
|
48
|
+
params: dict[str, Any] | list[Any] | None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
JSONRPCMessage = JSONRPCRequest | JSONRPCResponse | JSONRPCNotification
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# ============= MCP Tool Types =============
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class TextContent(TypedDict, total=False):
|
|
58
|
+
"""Text content in tool result."""
|
|
59
|
+
|
|
60
|
+
type: Literal["text"]
|
|
61
|
+
text: str
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class ImageContent(TypedDict, total=False):
|
|
65
|
+
"""Image content in tool result."""
|
|
66
|
+
|
|
67
|
+
type: Literal["image"]
|
|
68
|
+
data: str
|
|
69
|
+
mimeType: str
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class EmbeddedResource(TypedDict, total=False):
|
|
73
|
+
"""Embedded resource content in tool result."""
|
|
74
|
+
|
|
75
|
+
type: Literal["resource"]
|
|
76
|
+
resource: dict[str, Any]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
ToolResultContent = TextContent | ImageContent | EmbeddedResource
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class CallToolResult(TypedDict, total=False):
|
|
83
|
+
"""Result from calling a tool."""
|
|
84
|
+
|
|
85
|
+
content: list[ToolResultContent]
|
|
86
|
+
isError: bool
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# Tool handler type - takes arguments dict and returns CallToolResult
|
|
90
|
+
ToolHandler = Callable[[dict[str, Any]], CallToolResult | Awaitable[CallToolResult]]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass
|
|
94
|
+
class ToolInputProperty:
|
|
95
|
+
"""Property definition for tool input schema."""
|
|
96
|
+
|
|
97
|
+
type: str
|
|
98
|
+
description: str | None = None
|
|
99
|
+
enum: list[str] | None = None
|
|
100
|
+
default: Any = None
|
|
101
|
+
minimum: float | None = None
|
|
102
|
+
maximum: float | None = None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@dataclass
|
|
106
|
+
class ToolInputSchema:
|
|
107
|
+
"""JSON Schema for tool input."""
|
|
108
|
+
|
|
109
|
+
type: Literal["object"] = "object"
|
|
110
|
+
properties: dict[str, dict[str, Any]] = field(default_factory=dict)
|
|
111
|
+
required: list[str] = field(default_factory=list)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@dataclass
|
|
115
|
+
class SdkMcpToolDefinition:
|
|
116
|
+
"""
|
|
117
|
+
Tool definition for SDK MCP Server.
|
|
118
|
+
|
|
119
|
+
Example:
|
|
120
|
+
```python
|
|
121
|
+
tool_def = SdkMcpToolDefinition(
|
|
122
|
+
name="get_weather",
|
|
123
|
+
description="Get the current weather for a location",
|
|
124
|
+
input_schema=ToolInputSchema(
|
|
125
|
+
properties={
|
|
126
|
+
"location": {"type": "string", "description": "The city name"},
|
|
127
|
+
"units": {"type": "string", "enum": ["celsius", "fahrenheit"]},
|
|
128
|
+
},
|
|
129
|
+
required=["location"],
|
|
130
|
+
),
|
|
131
|
+
handler=get_weather_handler,
|
|
132
|
+
)
|
|
133
|
+
```
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
name: str
|
|
137
|
+
description: str
|
|
138
|
+
input_schema: ToolInputSchema
|
|
139
|
+
handler: ToolHandler
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@dataclass
|
|
143
|
+
class SdkMcpServerOptions:
|
|
144
|
+
"""
|
|
145
|
+
Options for creating an SDK MCP Server.
|
|
146
|
+
|
|
147
|
+
Attributes:
|
|
148
|
+
name: Server name (must be unique within the session)
|
|
149
|
+
version: Server version (defaults to "1.0.0")
|
|
150
|
+
tools: List of tool definitions to register
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
name: str
|
|
154
|
+
version: str = "1.0.0"
|
|
155
|
+
tools: list[SdkMcpToolDefinition] = field(default_factory=list)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@dataclass
|
|
159
|
+
class SdkMcpServerResult:
|
|
160
|
+
"""
|
|
161
|
+
Result type for create_sdk_mcp_server.
|
|
162
|
+
|
|
163
|
+
Attributes:
|
|
164
|
+
type: Type discriminator - always "sdk" for SDK MCP servers
|
|
165
|
+
name: Server name
|
|
166
|
+
server: The MCP server instance
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
type: Literal["sdk"]
|
|
170
|
+
name: str
|
|
171
|
+
server: SdkMcpServer
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class SdkMcpServer:
|
|
175
|
+
"""
|
|
176
|
+
SDK MCP Server implementation.
|
|
177
|
+
|
|
178
|
+
This class implements an MCP server that runs within the SDK process
|
|
179
|
+
and communicates with the CLI via the control protocol.
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
def __init__(self, options: SdkMcpServerOptions):
|
|
183
|
+
self.name = options.name
|
|
184
|
+
self.version = options.version
|
|
185
|
+
self.tools: dict[str, SdkMcpToolDefinition] = {}
|
|
186
|
+
self._transport: SdkControlServerTransport | None = None
|
|
187
|
+
|
|
188
|
+
# Register tools
|
|
189
|
+
for tool_def in options.tools:
|
|
190
|
+
self.tools[tool_def.name] = tool_def
|
|
191
|
+
|
|
192
|
+
def connect(self, transport: SdkControlServerTransport) -> None:
|
|
193
|
+
"""Connect the server to a transport."""
|
|
194
|
+
self._transport = transport
|
|
195
|
+
|
|
196
|
+
async def handle_message(self, message: JSONRPCMessage) -> JSONRPCMessage | None:
|
|
197
|
+
"""Handle an incoming JSON-RPC message."""
|
|
198
|
+
# Check if it's a request (has method and id)
|
|
199
|
+
if "method" not in message:
|
|
200
|
+
return None
|
|
201
|
+
|
|
202
|
+
method = cast(str, message.get("method", ""))
|
|
203
|
+
msg_id = cast("str | int | None", message.get("id"))
|
|
204
|
+
params = cast("dict[str, Any] | None", message.get("params", {}))
|
|
205
|
+
|
|
206
|
+
if method == "initialize":
|
|
207
|
+
return await self._handle_initialize(msg_id, params)
|
|
208
|
+
elif method == "tools/list":
|
|
209
|
+
return await self._handle_tools_list(msg_id)
|
|
210
|
+
elif method == "tools/call":
|
|
211
|
+
return await self._handle_tools_call(msg_id, params)
|
|
212
|
+
elif method == "notifications/initialized":
|
|
213
|
+
# Notification, no response needed
|
|
214
|
+
return None
|
|
215
|
+
else:
|
|
216
|
+
# Unknown method
|
|
217
|
+
if msg_id is not None:
|
|
218
|
+
return self._create_error_response(msg_id, -32601, f"Method not found: {method}")
|
|
219
|
+
return None
|
|
220
|
+
|
|
221
|
+
async def _handle_initialize(
|
|
222
|
+
self, msg_id: str | int | None, params: dict[str, Any] | None
|
|
223
|
+
) -> JSONRPCMessage:
|
|
224
|
+
"""Handle initialize request."""
|
|
225
|
+
result = {
|
|
226
|
+
"protocolVersion": "2024-11-05",
|
|
227
|
+
"capabilities": {
|
|
228
|
+
"tools": {},
|
|
229
|
+
},
|
|
230
|
+
"serverInfo": {
|
|
231
|
+
"name": self.name,
|
|
232
|
+
"version": self.version,
|
|
233
|
+
},
|
|
234
|
+
}
|
|
235
|
+
return self._create_response(msg_id, result)
|
|
236
|
+
|
|
237
|
+
async def _handle_tools_list(self, msg_id: str | int | None) -> JSONRPCMessage:
|
|
238
|
+
"""Handle tools/list request."""
|
|
239
|
+
tools_list = []
|
|
240
|
+
for tool_def in self.tools.values():
|
|
241
|
+
tools_list.append(
|
|
242
|
+
{
|
|
243
|
+
"name": tool_def.name,
|
|
244
|
+
"description": tool_def.description,
|
|
245
|
+
"inputSchema": {
|
|
246
|
+
"type": tool_def.input_schema.type,
|
|
247
|
+
"properties": tool_def.input_schema.properties,
|
|
248
|
+
"required": tool_def.input_schema.required,
|
|
249
|
+
},
|
|
250
|
+
}
|
|
251
|
+
)
|
|
252
|
+
return self._create_response(msg_id, {"tools": tools_list})
|
|
253
|
+
|
|
254
|
+
async def _handle_tools_call(
|
|
255
|
+
self, msg_id: str | int | None, params: dict[str, Any] | None
|
|
256
|
+
) -> JSONRPCMessage:
|
|
257
|
+
"""Handle tools/call request."""
|
|
258
|
+
if not isinstance(params, dict):
|
|
259
|
+
return self._create_error_response(msg_id, -32602, "Invalid params")
|
|
260
|
+
|
|
261
|
+
tool_name = params.get("name", "")
|
|
262
|
+
arguments = params.get("arguments", {})
|
|
263
|
+
|
|
264
|
+
tool_def = self.tools.get(tool_name)
|
|
265
|
+
if not tool_def:
|
|
266
|
+
return self._create_error_response(msg_id, -32602, f"Tool not found: {tool_name}")
|
|
267
|
+
|
|
268
|
+
try:
|
|
269
|
+
# Call the handler
|
|
270
|
+
result = tool_def.handler(arguments)
|
|
271
|
+
# Handle async handlers
|
|
272
|
+
if inspect.isawaitable(result):
|
|
273
|
+
result = await result
|
|
274
|
+
|
|
275
|
+
return self._create_response(msg_id, result)
|
|
276
|
+
except Exception as e:
|
|
277
|
+
# Return error as tool result
|
|
278
|
+
error_result: CallToolResult = {
|
|
279
|
+
"content": [{"type": "text", "text": str(e)}],
|
|
280
|
+
"isError": True,
|
|
281
|
+
}
|
|
282
|
+
return self._create_response(msg_id, error_result)
|
|
283
|
+
|
|
284
|
+
def _create_response(self, msg_id: str | int | None, result: Any) -> JSONRPCResponse:
|
|
285
|
+
"""Create a JSON-RPC response."""
|
|
286
|
+
return {
|
|
287
|
+
"jsonrpc": "2.0",
|
|
288
|
+
"id": msg_id,
|
|
289
|
+
"result": result,
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
def _create_error_response(
|
|
293
|
+
self, msg_id: str | int | None, code: int, message: str
|
|
294
|
+
) -> JSONRPCResponse:
|
|
295
|
+
"""Create a JSON-RPC error response."""
|
|
296
|
+
return {
|
|
297
|
+
"jsonrpc": "2.0",
|
|
298
|
+
"id": msg_id,
|
|
299
|
+
"error": {"code": code, "message": message},
|
|
300
|
+
}
|
codebuddy_agent_sdk/query.py
CHANGED
|
@@ -1,23 +1,185 @@
|
|
|
1
1
|
"""Query function for one-shot interactions with CodeBuddy."""
|
|
2
2
|
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
3
6
|
import json
|
|
4
7
|
import os
|
|
5
|
-
from collections.abc import AsyncIterable, AsyncIterator
|
|
8
|
+
from collections.abc import AsyncIterable, AsyncIterator, Callable
|
|
6
9
|
from dataclasses import asdict
|
|
7
|
-
from typing import Any
|
|
10
|
+
from typing import Any, TypeGuard
|
|
8
11
|
|
|
12
|
+
from ._errors import ExecutionError
|
|
9
13
|
from ._message_parser import parse_message
|
|
14
|
+
from .mcp.sdk_control_server_transport import SdkControlServerTransport
|
|
15
|
+
from .mcp.types import JSONRPCMessage, SdkMcpServer
|
|
10
16
|
from .transport import SubprocessTransport, Transport
|
|
11
17
|
from .types import (
|
|
12
18
|
AppendSystemPrompt,
|
|
13
19
|
CanUseToolOptions,
|
|
14
20
|
CodeBuddyAgentOptions,
|
|
21
|
+
ErrorMessage,
|
|
22
|
+
HookEvent,
|
|
15
23
|
HookMatcher,
|
|
24
|
+
McpSdkServerConfig,
|
|
25
|
+
McpServerConfig,
|
|
16
26
|
Message,
|
|
17
27
|
ResultMessage,
|
|
18
28
|
)
|
|
19
29
|
|
|
20
30
|
|
|
31
|
+
def _is_sdk_mcp_server(config: McpServerConfig) -> TypeGuard[McpSdkServerConfig]:
|
|
32
|
+
"""Type guard to check if config is an SDK MCP server."""
|
|
33
|
+
return isinstance(config, dict) and config.get("type") == "sdk"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _is_valid_hook_event(event: str) -> TypeGuard[HookEvent]:
|
|
37
|
+
"""Type guard to check if event is a valid HookEvent."""
|
|
38
|
+
return event in {
|
|
39
|
+
"PreToolUse",
|
|
40
|
+
"PostToolUse",
|
|
41
|
+
"UserPromptSubmit",
|
|
42
|
+
"Stop",
|
|
43
|
+
"SubagentStop",
|
|
44
|
+
"PreCompact",
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class QueryContext:
|
|
49
|
+
"""Context for managing query state including SDK MCP servers."""
|
|
50
|
+
|
|
51
|
+
def __init__(self, options: CodeBuddyAgentOptions):
|
|
52
|
+
self.options = options
|
|
53
|
+
# SDK MCP Server management
|
|
54
|
+
self.sdk_mcp_transports: dict[str, SdkControlServerTransport] = {}
|
|
55
|
+
self.sdk_mcp_servers: dict[str, SdkMcpServer] = {}
|
|
56
|
+
self.pending_mcp_responses: dict[str, asyncio.Future[JSONRPCMessage]] = {}
|
|
57
|
+
self.sdk_mcp_server_names: list[str] = []
|
|
58
|
+
|
|
59
|
+
def extract_mcp_servers(
|
|
60
|
+
self,
|
|
61
|
+
) -> tuple[dict[str, SdkMcpServer], dict[str, McpServerConfig] | None]:
|
|
62
|
+
"""
|
|
63
|
+
Extract SDK MCP servers from the mcp_servers config.
|
|
64
|
+
SDK servers are identified by having type: 'sdk'.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Tuple of (sdk_servers dict, regular_servers dict or None)
|
|
68
|
+
"""
|
|
69
|
+
mcp_servers = self.options.mcp_servers
|
|
70
|
+
|
|
71
|
+
if not mcp_servers or not isinstance(mcp_servers, dict):
|
|
72
|
+
return {}, None
|
|
73
|
+
|
|
74
|
+
sdk_servers: dict[str, SdkMcpServer] = {}
|
|
75
|
+
regular_servers: dict[str, McpServerConfig] = {}
|
|
76
|
+
|
|
77
|
+
for name, config in mcp_servers.items():
|
|
78
|
+
if _is_sdk_mcp_server(config):
|
|
79
|
+
# SDK MCP server
|
|
80
|
+
sdk_servers[name] = config["server"]
|
|
81
|
+
self.sdk_mcp_server_names.append(name)
|
|
82
|
+
else:
|
|
83
|
+
# Regular MCP server (stdio)
|
|
84
|
+
regular_servers[name] = config
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
sdk_servers,
|
|
88
|
+
regular_servers if regular_servers else None,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def connect_sdk_mcp_server(
|
|
92
|
+
self,
|
|
93
|
+
name: str,
|
|
94
|
+
server: SdkMcpServer,
|
|
95
|
+
send_callback: Callable[[str, JSONRPCMessage], None],
|
|
96
|
+
) -> None:
|
|
97
|
+
"""Connect an SDK MCP server."""
|
|
98
|
+
|
|
99
|
+
def _create_message_forwarder(
|
|
100
|
+
server_name: str,
|
|
101
|
+
) -> Callable[[JSONRPCMessage], None]:
|
|
102
|
+
def forwarder(msg: JSONRPCMessage) -> None:
|
|
103
|
+
send_callback(server_name, msg)
|
|
104
|
+
|
|
105
|
+
return forwarder
|
|
106
|
+
|
|
107
|
+
# Create custom transport that forwards to CLI
|
|
108
|
+
transport = SdkControlServerTransport(_create_message_forwarder(name))
|
|
109
|
+
|
|
110
|
+
# Store transport and server
|
|
111
|
+
self.sdk_mcp_transports[name] = transport
|
|
112
|
+
self.sdk_mcp_servers[name] = server
|
|
113
|
+
|
|
114
|
+
# Connect server to transport
|
|
115
|
+
server.connect(transport)
|
|
116
|
+
|
|
117
|
+
async def handle_mcp_message_request(
|
|
118
|
+
self,
|
|
119
|
+
transport: Transport,
|
|
120
|
+
request_id: str,
|
|
121
|
+
request: dict[str, Any],
|
|
122
|
+
) -> None:
|
|
123
|
+
"""Handle MCP message control request from CLI."""
|
|
124
|
+
server_name = request.get("server_name", "")
|
|
125
|
+
message: JSONRPCMessage = request.get("message", {})
|
|
126
|
+
|
|
127
|
+
server = self.sdk_mcp_servers.get(server_name)
|
|
128
|
+
|
|
129
|
+
if not server:
|
|
130
|
+
response = {
|
|
131
|
+
"type": "control_response",
|
|
132
|
+
"response": {
|
|
133
|
+
"subtype": "error",
|
|
134
|
+
"request_id": request_id,
|
|
135
|
+
"error": f"SDK MCP server not found: {server_name}",
|
|
136
|
+
},
|
|
137
|
+
}
|
|
138
|
+
await transport.write(json.dumps(response))
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
# Handle the message with the MCP server
|
|
143
|
+
mcp_response = await server.handle_message(message)
|
|
144
|
+
|
|
145
|
+
response = {
|
|
146
|
+
"type": "control_response",
|
|
147
|
+
"response": {
|
|
148
|
+
"subtype": "success",
|
|
149
|
+
"request_id": request_id,
|
|
150
|
+
"response": {
|
|
151
|
+
"mcp_response": mcp_response or {"jsonrpc": "2.0", "result": {}, "id": 0},
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
}
|
|
155
|
+
await transport.write(json.dumps(response))
|
|
156
|
+
|
|
157
|
+
except Exception as e:
|
|
158
|
+
response = {
|
|
159
|
+
"type": "control_response",
|
|
160
|
+
"response": {
|
|
161
|
+
"subtype": "error",
|
|
162
|
+
"request_id": request_id,
|
|
163
|
+
"error": str(e),
|
|
164
|
+
},
|
|
165
|
+
}
|
|
166
|
+
await transport.write(json.dumps(response))
|
|
167
|
+
|
|
168
|
+
async def cleanup(self) -> None:
|
|
169
|
+
"""Cleanup all resources."""
|
|
170
|
+
# Cancel pending MCP responses
|
|
171
|
+
for future in self.pending_mcp_responses.values():
|
|
172
|
+
if not future.done():
|
|
173
|
+
future.cancel()
|
|
174
|
+
self.pending_mcp_responses.clear()
|
|
175
|
+
|
|
176
|
+
# Close all SDK MCP transports
|
|
177
|
+
for transport in self.sdk_mcp_transports.values():
|
|
178
|
+
await transport.close()
|
|
179
|
+
self.sdk_mcp_transports.clear()
|
|
180
|
+
self.sdk_mcp_servers.clear()
|
|
181
|
+
|
|
182
|
+
|
|
21
183
|
async def query(
|
|
22
184
|
*,
|
|
23
185
|
prompt: str | AsyncIterable[dict[str, Any]],
|
|
@@ -52,13 +214,53 @@ async def query(
|
|
|
52
214
|
|
|
53
215
|
os.environ["CODEBUDDY_CODE_ENTRYPOINT"] = "sdk-py"
|
|
54
216
|
|
|
217
|
+
# Create query context for managing SDK MCP servers
|
|
218
|
+
ctx = QueryContext(options)
|
|
219
|
+
|
|
220
|
+
# Extract SDK MCP servers and regular MCP servers
|
|
221
|
+
sdk_servers, regular_servers = ctx.extract_mcp_servers()
|
|
222
|
+
|
|
223
|
+
# Create modified options with only regular MCP servers for subprocess
|
|
224
|
+
modified_options = CodeBuddyAgentOptions(
|
|
225
|
+
allowed_tools=options.allowed_tools,
|
|
226
|
+
disallowed_tools=options.disallowed_tools,
|
|
227
|
+
system_prompt=options.system_prompt,
|
|
228
|
+
mcp_servers=regular_servers or {},
|
|
229
|
+
permission_mode=options.permission_mode,
|
|
230
|
+
continue_conversation=options.continue_conversation,
|
|
231
|
+
resume=options.resume,
|
|
232
|
+
max_turns=options.max_turns,
|
|
233
|
+
model=options.model,
|
|
234
|
+
fallback_model=options.fallback_model,
|
|
235
|
+
cwd=options.cwd,
|
|
236
|
+
codebuddy_code_path=options.codebuddy_code_path,
|
|
237
|
+
env=options.env,
|
|
238
|
+
extra_args=options.extra_args,
|
|
239
|
+
stderr=options.stderr,
|
|
240
|
+
hooks=options.hooks,
|
|
241
|
+
include_partial_messages=options.include_partial_messages,
|
|
242
|
+
fork_session=options.fork_session,
|
|
243
|
+
agents=options.agents,
|
|
244
|
+
setting_sources=options.setting_sources,
|
|
245
|
+
can_use_tool=options.can_use_tool,
|
|
246
|
+
)
|
|
247
|
+
|
|
55
248
|
if transport is None:
|
|
56
|
-
transport = SubprocessTransport(options=
|
|
249
|
+
transport = SubprocessTransport(options=modified_options, prompt=prompt)
|
|
57
250
|
|
|
58
251
|
await transport.connect()
|
|
59
252
|
|
|
253
|
+
# Connect SDK MCP servers
|
|
254
|
+
def send_mcp_message(_server_name: str, _message: JSONRPCMessage) -> None:
|
|
255
|
+
# This callback sends MCP messages from server to CLI
|
|
256
|
+
# For now, SDK servers only respond to CLI requests, so this is rarely used
|
|
257
|
+
pass
|
|
258
|
+
|
|
259
|
+
for name, server in sdk_servers.items():
|
|
260
|
+
ctx.connect_sdk_mcp_server(name, server, send_mcp_message)
|
|
261
|
+
|
|
60
262
|
try:
|
|
61
|
-
await _send_initialize(transport, options)
|
|
263
|
+
await _send_initialize(transport, options, ctx.sdk_mcp_server_names)
|
|
62
264
|
await _send_prompt(transport, prompt)
|
|
63
265
|
|
|
64
266
|
async for line in transport.read():
|
|
@@ -68,35 +270,45 @@ async def query(
|
|
|
68
270
|
try:
|
|
69
271
|
data = json.loads(line)
|
|
70
272
|
|
|
71
|
-
# Handle control requests (hooks)
|
|
273
|
+
# Handle control requests (hooks, permissions, MCP messages)
|
|
72
274
|
if data.get("type") == "control_request":
|
|
73
|
-
await _handle_control_request(transport, data, options)
|
|
275
|
+
await _handle_control_request(transport, data, options, ctx)
|
|
74
276
|
continue
|
|
75
277
|
|
|
76
278
|
message = parse_message(data)
|
|
77
279
|
if message:
|
|
280
|
+
# Check for execution error BEFORE yielding
|
|
281
|
+
if isinstance(message, ResultMessage):
|
|
282
|
+
if message.is_error and message.errors and len(message.errors) > 0:
|
|
283
|
+
raise ExecutionError(message.errors, message.subtype)
|
|
284
|
+
yield message
|
|
285
|
+
break
|
|
286
|
+
|
|
78
287
|
yield message
|
|
79
288
|
|
|
80
|
-
if isinstance(message,
|
|
289
|
+
if isinstance(message, ErrorMessage):
|
|
81
290
|
break
|
|
82
291
|
|
|
83
292
|
except json.JSONDecodeError:
|
|
84
293
|
continue # Ignore non-JSON lines
|
|
85
294
|
|
|
86
295
|
finally:
|
|
296
|
+
await ctx.cleanup()
|
|
87
297
|
await transport.close()
|
|
88
298
|
|
|
89
299
|
|
|
90
|
-
async def _send_initialize(
|
|
300
|
+
async def _send_initialize(
|
|
301
|
+
transport: Transport,
|
|
302
|
+
options: CodeBuddyAgentOptions,
|
|
303
|
+
sdk_mcp_server_names: list[str],
|
|
304
|
+
) -> None:
|
|
91
305
|
"""Send initialization control request."""
|
|
92
306
|
hooks_config = _build_hooks_config(options.hooks) if options.hooks else None
|
|
93
307
|
agents_config = (
|
|
94
|
-
{name: asdict(agent) for name, agent in options.agents.items()}
|
|
95
|
-
if options.agents
|
|
96
|
-
else None
|
|
308
|
+
{name: asdict(agent) for name, agent in options.agents.items()} if options.agents else None
|
|
97
309
|
)
|
|
98
310
|
|
|
99
|
-
#
|
|
311
|
+
# Parse system_prompt config
|
|
100
312
|
system_prompt: str | None = None
|
|
101
313
|
append_system_prompt: str | None = None
|
|
102
314
|
if isinstance(options.system_prompt, str):
|
|
@@ -113,14 +325,14 @@ async def _send_initialize(transport: Transport, options: CodeBuddyAgentOptions)
|
|
|
113
325
|
"systemPrompt": system_prompt,
|
|
114
326
|
"appendSystemPrompt": append_system_prompt,
|
|
115
327
|
"agents": agents_config,
|
|
328
|
+
# Include SDK MCP server names
|
|
329
|
+
"sdkMcpServers": sdk_mcp_server_names if sdk_mcp_server_names else None,
|
|
116
330
|
},
|
|
117
331
|
}
|
|
118
332
|
await transport.write(json.dumps(request))
|
|
119
333
|
|
|
120
334
|
|
|
121
|
-
async def _send_prompt(
|
|
122
|
-
transport: Transport, prompt: str | AsyncIterable[dict[str, Any]]
|
|
123
|
-
) -> None:
|
|
335
|
+
async def _send_prompt(transport: Transport, prompt: str | AsyncIterable[dict[str, Any]]) -> None:
|
|
124
336
|
"""Send user prompt."""
|
|
125
337
|
if isinstance(prompt, str):
|
|
126
338
|
message = {
|
|
@@ -139,6 +351,7 @@ async def _handle_control_request(
|
|
|
139
351
|
transport: Transport,
|
|
140
352
|
data: dict[str, Any],
|
|
141
353
|
options: CodeBuddyAgentOptions,
|
|
354
|
+
ctx: QueryContext,
|
|
142
355
|
) -> None:
|
|
143
356
|
"""Handle control request from CLI."""
|
|
144
357
|
request_id = data.get("request_id", "")
|
|
@@ -168,6 +381,9 @@ async def _handle_control_request(
|
|
|
168
381
|
elif subtype == "can_use_tool":
|
|
169
382
|
await _handle_permission_request(transport, request_id, request, options)
|
|
170
383
|
|
|
384
|
+
elif subtype == "mcp_message":
|
|
385
|
+
await ctx.handle_mcp_message_request(transport, request_id, request)
|
|
386
|
+
|
|
171
387
|
|
|
172
388
|
async def _handle_permission_request(
|
|
173
389
|
transport: Transport,
|
|
@@ -268,14 +484,19 @@ async def _execute_hook(
|
|
|
268
484
|
return {"continue_": True}
|
|
269
485
|
|
|
270
486
|
event = parts[1]
|
|
487
|
+
|
|
488
|
+
# Validate event is a known HookEvent using TypeGuard
|
|
489
|
+
if not _is_valid_hook_event(event):
|
|
490
|
+
return {"continue_": True}
|
|
491
|
+
|
|
271
492
|
try:
|
|
272
493
|
matcher_idx = int(parts[2])
|
|
273
494
|
hook_idx = int(parts[3])
|
|
274
495
|
except ValueError:
|
|
275
496
|
return {"continue_": True}
|
|
276
497
|
|
|
277
|
-
# Find the hook
|
|
278
|
-
matchers = options.hooks.get(event)
|
|
498
|
+
# Find the hook - event is now narrowed to HookEvent by TypeGuard
|
|
499
|
+
matchers = options.hooks.get(event)
|
|
279
500
|
if not matchers or matcher_idx >= len(matchers):
|
|
280
501
|
return {"continue_": True}
|
|
281
502
|
|
|
@@ -305,9 +526,7 @@ def _build_hooks_config(
|
|
|
305
526
|
config[str(event)] = [
|
|
306
527
|
{
|
|
307
528
|
"matcher": m.matcher,
|
|
308
|
-
"hookCallbackIds": [
|
|
309
|
-
f"hook_{event}_{i}_{j}" for j, _ in enumerate(m.hooks)
|
|
310
|
-
],
|
|
529
|
+
"hookCallbackIds": [f"hook_{event}_{i}_{j}" for j, _ in enumerate(m.hooks)],
|
|
311
530
|
"timeout": m.timeout,
|
|
312
531
|
}
|
|
313
532
|
for i, m in enumerate(matchers)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"""Subprocess transport for CLI communication."""
|
|
2
2
|
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
3
5
|
import asyncio
|
|
4
6
|
import json
|
|
5
7
|
import os
|
|
@@ -81,8 +83,10 @@ class SubprocessTransport(Transport):
|
|
|
81
83
|
# When setting_sources is explicitly provided (including empty list), use it
|
|
82
84
|
# When not provided (None), default to 'none' for SDK isolation
|
|
83
85
|
if opts.setting_sources is not None:
|
|
84
|
-
|
|
85
|
-
|
|
86
|
+
setting_value = (
|
|
87
|
+
"none" if len(opts.setting_sources) == 0 else ",".join(opts.setting_sources)
|
|
88
|
+
)
|
|
89
|
+
args.extend(["--setting-sources", setting_value])
|
|
86
90
|
else:
|
|
87
91
|
# SDK default behavior: no filesystem settings loaded
|
|
88
92
|
args.extend(["--setting-sources", "none"])
|
codebuddy_agent_sdk/types.py
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
"""Type definitions for CodeBuddy Agent SDK."""
|
|
2
2
|
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
3
5
|
from collections.abc import Awaitable, Callable
|
|
4
6
|
from dataclasses import dataclass, field
|
|
5
7
|
from pathlib import Path
|
|
6
|
-
from typing import Any, Literal, NotRequired, TypedDict
|
|
8
|
+
from typing import TYPE_CHECKING, Any, Literal, NotRequired, TypedDict
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from .mcp.types import SdkMcpServer
|
|
7
12
|
|
|
8
13
|
# ============= AskUserQuestion Types =============
|
|
9
14
|
|
|
@@ -183,6 +188,7 @@ class ResultMessage:
|
|
|
183
188
|
total_cost_usd: float | None = None
|
|
184
189
|
usage: dict[str, Any] | None = None
|
|
185
190
|
result: str | None = None
|
|
191
|
+
errors: list[str] | None = None
|
|
186
192
|
|
|
187
193
|
|
|
188
194
|
@dataclass
|
|
@@ -195,7 +201,17 @@ class StreamEvent:
|
|
|
195
201
|
parent_tool_use_id: str | None = None
|
|
196
202
|
|
|
197
203
|
|
|
198
|
-
|
|
204
|
+
@dataclass
|
|
205
|
+
class ErrorMessage:
|
|
206
|
+
"""Error message from CLI."""
|
|
207
|
+
|
|
208
|
+
error: str
|
|
209
|
+
session_id: str | None = None
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
Message = (
|
|
213
|
+
UserMessage | AssistantMessage | SystemMessage | ResultMessage | StreamEvent | ErrorMessage
|
|
214
|
+
)
|
|
199
215
|
|
|
200
216
|
|
|
201
217
|
# Hook types
|
|
@@ -242,7 +258,18 @@ class McpStdioServerConfig(TypedDict):
|
|
|
242
258
|
env: NotRequired[dict[str, str]]
|
|
243
259
|
|
|
244
260
|
|
|
245
|
-
|
|
261
|
+
class McpSdkServerConfig(TypedDict):
|
|
262
|
+
"""
|
|
263
|
+
SDK MCP Server configuration - for servers running within the SDK process.
|
|
264
|
+
Created via create_sdk_mcp_server().
|
|
265
|
+
"""
|
|
266
|
+
|
|
267
|
+
type: Literal["sdk"]
|
|
268
|
+
name: str
|
|
269
|
+
server: SdkMcpServer
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
McpServerConfig = McpStdioServerConfig | McpSdkServerConfig
|
|
246
273
|
|
|
247
274
|
|
|
248
275
|
# System prompt configuration
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
codebuddy_agent_sdk/__init__.py,sha256=GKrVvH2KKyuA9ht-7uMVHJJn7eWBKOi0_DKc2Ef5RCU,2690
|
|
2
|
+
codebuddy_agent_sdk/_binary.py,sha256=rQFj2B__X7zHMxFazp_0vgXQFsqPi578f8g4jSkLUBc,4338
|
|
3
|
+
codebuddy_agent_sdk/_errors.py,sha256=bLmyebUowNlbUomXyuS1L6J6ZbwHohvzqH_1GmN4_oM,1248
|
|
4
|
+
codebuddy_agent_sdk/_message_parser.py,sha256=vtyeuwIFdl9Wz1WrWEg7G6E3OECDZT4G6bvpH15Q3VY,3504
|
|
5
|
+
codebuddy_agent_sdk/_version.py,sha256=EhZlhQ69654jiXymzw5m6O61qdW_fo3ug1b4ABdOtm4,74
|
|
6
|
+
codebuddy_agent_sdk/client.py,sha256=JfdA0wq4KOOrP4mZXfVuF4uwdrEbVHGIUrzpt4Bt_ZM,10803
|
|
7
|
+
codebuddy_agent_sdk/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
codebuddy_agent_sdk/query.py,sha256=tCfFomCNGS1Sygbk0U9j25JLsF2azWa-3AuckThYaII,17341
|
|
9
|
+
codebuddy_agent_sdk/types.py,sha256=rw5jrIg8LWtZN1Km_7TzY7SP_wFRJwKNGwPxgRcQ47A,7473
|
|
10
|
+
codebuddy_agent_sdk/bin/codebuddy,sha256=pfQZlfnGCrEocOsuQPXb8bozsVAPnWKmV62w-EhGVeE,119690167
|
|
11
|
+
codebuddy_agent_sdk/mcp/__init__.py,sha256=0aCmHN0MnMCCUU0bdTWBRlxAblnxA-vR5Og0y3BA480,764
|
|
12
|
+
codebuddy_agent_sdk/mcp/create_sdk_mcp_server.py,sha256=ig0b0ghf0T8GGjltFBRSmQSTFsaJ6fBI8S1Pjupqhps,4959
|
|
13
|
+
codebuddy_agent_sdk/mcp/sdk_control_server_transport.py,sha256=CIG3h5gULubGcQPHPVd-GG9cLswwVDfgDlLba6ZPQbs,2944
|
|
14
|
+
codebuddy_agent_sdk/mcp/types.py,sha256=CPEBUW5vCt93cBgu3HvAacXMsa5FdRWwWxPeicQ0PWw,8578
|
|
15
|
+
codebuddy_agent_sdk/transport/__init__.py,sha256=zv_8OJHgnWjCInqOiu3GOFby4XVGvDwICHpOsymOlko,166
|
|
16
|
+
codebuddy_agent_sdk/transport/base.py,sha256=XtLquCmt4yzPhTBHcVU0XFb016uG0E-12ETc-N9tzIk,662
|
|
17
|
+
codebuddy_agent_sdk/transport/subprocess.py,sha256=VHoGXoP0qm597bhIE_mTV9xyIDUM-nMk7fwZwntx94c,5701
|
|
18
|
+
codebuddy_agent_sdk-0.2.0.dist-info/METADATA,sha256=vATnYz7KEZ_IgyQqOsWBGLiVxdD-Z-_MV1RvKMK4TS0,2538
|
|
19
|
+
codebuddy_agent_sdk-0.2.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
20
|
+
codebuddy_agent_sdk-0.2.0.dist-info/RECORD,,
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
codebuddy_agent_sdk/__init__.py,sha256=MF75KZvSF3PTGk9OwjFEW5R7-vI59lexKn5e9-FY1Dw,2052
|
|
2
|
-
codebuddy_agent_sdk/_binary.py,sha256=7UBOzgooaojNAlb3tFvsN12TBAndcP3YLGD3Ic3LlmI,4302
|
|
3
|
-
codebuddy_agent_sdk/_errors.py,sha256=5ABIym6mazInI33kmt5mR94v6c325jDbi0YCgQjrysY,816
|
|
4
|
-
codebuddy_agent_sdk/_message_parser.py,sha256=U0dy1nqe2kap0_A_VWQN-KME-HEQmDhZQcCbCLVMe4s,3255
|
|
5
|
-
codebuddy_agent_sdk/_version.py,sha256=8Hkuix_8aO-_o4bmnzF6e8Q8olrmVd1BAItpI3ntq2M,75
|
|
6
|
-
codebuddy_agent_sdk/client.py,sha256=Bl6l2KFXkSs4j9usuzM7JMYsEO9Np4p6Csg9fR42LtI,10191
|
|
7
|
-
codebuddy_agent_sdk/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
-
codebuddy_agent_sdk/query.py,sha256=LNv4LIdzdg-fRunMlIicu0SAEv5AoAPpu3_w7iDGhYw,9544
|
|
9
|
-
codebuddy_agent_sdk/types.py,sha256=PYCgC6rAElM5VAcDcdDDr4MXCnIXbRcSNwiuTf__Jrk,6926
|
|
10
|
-
codebuddy_agent_sdk/bin/codebuddy,sha256=akZEOeeNA-7Rq3_zFPGe3eAXrSHl4QgY-SKQ3GK2huc,122316262
|
|
11
|
-
codebuddy_agent_sdk/transport/__init__.py,sha256=zv_8OJHgnWjCInqOiu3GOFby4XVGvDwICHpOsymOlko,166
|
|
12
|
-
codebuddy_agent_sdk/transport/base.py,sha256=ps-4jWU3jL-IiTIL5Pv70zanxdmrknCHZfpxMGBTLoA,626
|
|
13
|
-
codebuddy_agent_sdk/transport/subprocess.py,sha256=m3o-hx2q6U3yeaOPrMxqi6XetsPZdAjjAxBIDiq-vgU,5617
|
|
14
|
-
codebuddy_agent_sdk-0.1.11.dist-info/METADATA,sha256=I8bQtN9PH7QoErssvcCnx0w0-i__UEnZ81Oc7xd83tE,2539
|
|
15
|
-
codebuddy_agent_sdk-0.1.11.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
16
|
-
codebuddy_agent_sdk-0.1.11.dist-info/RECORD,,
|
|
File without changes
|