codebuddy-agent-sdk 0.1.23__py3-none-musllinux_1_1_x86_64.whl → 0.2.2__py3-none-musllinux_1_1_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 +36 -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 +93 -16
- 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 +261 -59
- codebuddy_agent_sdk/transport/base.py +2 -0
- codebuddy_agent_sdk/transport/subprocess.py +6 -2
- codebuddy_agent_sdk/types.py +92 -3
- {codebuddy_agent_sdk-0.1.23.dist-info → codebuddy_agent_sdk-0.2.2.dist-info}/METADATA +1 -1
- codebuddy_agent_sdk-0.2.2.dist-info/RECORD +20 -0
- codebuddy_agent_sdk-0.1.23.dist-info/RECORD +0 -16
- {codebuddy_agent_sdk-0.1.23.dist-info → codebuddy_agent_sdk-0.2.2.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 (
|
|
@@ -20,14 +32,20 @@ from .types import (
|
|
|
20
32
|
AssistantMessage,
|
|
21
33
|
CanUseTool,
|
|
22
34
|
CanUseToolOptions,
|
|
35
|
+
Checkpoint,
|
|
36
|
+
CheckpointFileChangeStats,
|
|
23
37
|
CodeBuddyAgentOptions,
|
|
24
38
|
ContentBlock,
|
|
39
|
+
ErrorMessage,
|
|
40
|
+
FileVersion,
|
|
25
41
|
HookCallback,
|
|
26
42
|
HookContext,
|
|
27
43
|
HookEvent,
|
|
28
44
|
HookJSONOutput,
|
|
29
45
|
HookMatcher,
|
|
46
|
+
McpSdkServerConfig,
|
|
30
47
|
McpServerConfig,
|
|
48
|
+
McpStdioServerConfig,
|
|
31
49
|
Message,
|
|
32
50
|
PermissionMode,
|
|
33
51
|
PermissionResult,
|
|
@@ -50,6 +68,16 @@ __all__ = [
|
|
|
50
68
|
"CodeBuddySDKClient",
|
|
51
69
|
"Transport",
|
|
52
70
|
"__version__",
|
|
71
|
+
# MCP Server API
|
|
72
|
+
"create_sdk_mcp_server",
|
|
73
|
+
"tool",
|
|
74
|
+
"SdkControlServerTransport",
|
|
75
|
+
"SdkMcpServerOptions",
|
|
76
|
+
"SdkMcpServerResult",
|
|
77
|
+
"SdkMcpToolDefinition",
|
|
78
|
+
"ToolHandler",
|
|
79
|
+
"CallToolResult",
|
|
80
|
+
"TextContent",
|
|
53
81
|
# Types - Permission
|
|
54
82
|
"PermissionMode",
|
|
55
83
|
# Types - Messages
|
|
@@ -59,6 +87,7 @@ __all__ = [
|
|
|
59
87
|
"SystemMessage",
|
|
60
88
|
"ResultMessage",
|
|
61
89
|
"StreamEvent",
|
|
90
|
+
"ErrorMessage",
|
|
62
91
|
# Types - Content blocks
|
|
63
92
|
"ContentBlock",
|
|
64
93
|
"TextBlock",
|
|
@@ -86,12 +115,19 @@ __all__ = [
|
|
|
86
115
|
"HookMatcher",
|
|
87
116
|
"HookJSONOutput",
|
|
88
117
|
"HookContext",
|
|
118
|
+
# Types - Checkpoint
|
|
119
|
+
"Checkpoint",
|
|
120
|
+
"CheckpointFileChangeStats",
|
|
121
|
+
"FileVersion",
|
|
89
122
|
# Types - MCP
|
|
90
123
|
"McpServerConfig",
|
|
124
|
+
"McpStdioServerConfig",
|
|
125
|
+
"McpSdkServerConfig",
|
|
91
126
|
# Errors
|
|
92
127
|
"CodeBuddySDKError",
|
|
93
128
|
"CLIConnectionError",
|
|
94
129
|
"CLINotFoundError",
|
|
95
130
|
"CLIJSONDecodeError",
|
|
96
131
|
"ProcessError",
|
|
132
|
+
"ExecutionError",
|
|
97
133
|
]
|
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, HookMatcher, Message, ResultMessage
|
|
12
15
|
|
|
13
16
|
|
|
14
17
|
class CodeBuddySDKClient:
|
|
@@ -44,12 +47,12 @@ class CodeBuddySDKClient:
|
|
|
44
47
|
self._custom_transport = transport
|
|
45
48
|
self._transport: Transport | None = None
|
|
46
49
|
self._connected = False
|
|
50
|
+
# Hook callback registry: callback_id -> hook function
|
|
51
|
+
self._hook_callbacks: dict[str, Any] = {}
|
|
47
52
|
|
|
48
53
|
os.environ["CODEBUDDY_CODE_ENTRYPOINT"] = "sdk-py-client"
|
|
49
54
|
|
|
50
|
-
async def connect(
|
|
51
|
-
self, prompt: str | AsyncIterable[dict[str, Any]] | None = None
|
|
52
|
-
) -> None:
|
|
55
|
+
async def connect(self, prompt: str | AsyncIterable[dict[str, Any]] | None = None) -> None:
|
|
53
56
|
"""Connect to CodeBuddy with an optional initial prompt."""
|
|
54
57
|
if self._custom_transport:
|
|
55
58
|
self._transport = self._custom_transport
|
|
@@ -68,13 +71,52 @@ class CodeBuddySDKClient:
|
|
|
68
71
|
if not self._transport:
|
|
69
72
|
return
|
|
70
73
|
|
|
74
|
+
hooks_config, self._hook_callbacks = self._build_hooks_config()
|
|
75
|
+
|
|
71
76
|
request = {
|
|
72
77
|
"type": "control_request",
|
|
73
78
|
"request_id": f"init_{id(self)}",
|
|
74
|
-
"request": {
|
|
79
|
+
"request": {
|
|
80
|
+
"subtype": "initialize",
|
|
81
|
+
"hooks": hooks_config,
|
|
82
|
+
},
|
|
75
83
|
}
|
|
76
84
|
await self._transport.write(json.dumps(request))
|
|
77
85
|
|
|
86
|
+
def _build_hooks_config(self) -> tuple[dict[str, list[dict[str, Any]]] | None, dict[str, Any]]:
|
|
87
|
+
"""Build hooks configuration for CLI and callback registry.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
Tuple of (config for CLI, callback_id -> hook function mapping)
|
|
91
|
+
"""
|
|
92
|
+
callbacks: dict[str, Any] = {}
|
|
93
|
+
|
|
94
|
+
if not self.options.hooks:
|
|
95
|
+
return None, callbacks
|
|
96
|
+
|
|
97
|
+
config: dict[str, list[dict[str, Any]]] = {}
|
|
98
|
+
|
|
99
|
+
for event, matchers in self.options.hooks.items():
|
|
100
|
+
event_str = str(event)
|
|
101
|
+
matcher_configs = []
|
|
102
|
+
|
|
103
|
+
for i, m in enumerate(matchers):
|
|
104
|
+
callback_ids = []
|
|
105
|
+
for j, hook in enumerate(m.hooks):
|
|
106
|
+
callback_id = f"hook_{event_str}_{i}_{j}"
|
|
107
|
+
callback_ids.append(callback_id)
|
|
108
|
+
callbacks[callback_id] = hook
|
|
109
|
+
|
|
110
|
+
matcher_configs.append({
|
|
111
|
+
"matcher": m.matcher,
|
|
112
|
+
"hookCallbackIds": callback_ids,
|
|
113
|
+
"timeout": m.timeout,
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
config[event_str] = matcher_configs
|
|
117
|
+
|
|
118
|
+
return (config if config else None), callbacks
|
|
119
|
+
|
|
78
120
|
async def query(
|
|
79
121
|
self,
|
|
80
122
|
prompt: str | AsyncIterable[dict[str, Any]],
|
|
@@ -139,20 +181,24 @@ class CodeBuddySDKClient:
|
|
|
139
181
|
if subtype == "can_use_tool":
|
|
140
182
|
await self._handle_permission_request(request_id, request)
|
|
141
183
|
elif subtype == "hook_callback":
|
|
142
|
-
|
|
184
|
+
callback_id = request.get("callback_id", "")
|
|
185
|
+
hook_input = request.get("input", {})
|
|
186
|
+
tool_use_id = request.get("tool_use_id")
|
|
187
|
+
|
|
188
|
+
# Execute the hook
|
|
189
|
+
hook_response = await self._execute_hook(callback_id, hook_input, tool_use_id)
|
|
190
|
+
|
|
143
191
|
response = {
|
|
144
192
|
"type": "control_response",
|
|
145
193
|
"response": {
|
|
146
194
|
"subtype": "success",
|
|
147
195
|
"request_id": request_id,
|
|
148
|
-
"response":
|
|
196
|
+
"response": hook_response,
|
|
149
197
|
},
|
|
150
198
|
}
|
|
151
199
|
await self._transport.write(json.dumps(response))
|
|
152
200
|
|
|
153
|
-
async def _handle_permission_request(
|
|
154
|
-
self, request_id: str, request: dict[str, Any]
|
|
155
|
-
) -> None:
|
|
201
|
+
async def _handle_permission_request(self, request_id: str, request: dict[str, Any]) -> None:
|
|
156
202
|
"""Handle permission request from CLI."""
|
|
157
203
|
if not self._transport:
|
|
158
204
|
return
|
|
@@ -232,16 +278,42 @@ class CodeBuddySDKClient:
|
|
|
232
278
|
}
|
|
233
279
|
await self._transport.write(json.dumps(response))
|
|
234
280
|
|
|
281
|
+
async def _execute_hook(
|
|
282
|
+
self,
|
|
283
|
+
callback_id: str,
|
|
284
|
+
hook_input: dict[str, Any],
|
|
285
|
+
tool_use_id: str | None,
|
|
286
|
+
) -> dict[str, Any]:
|
|
287
|
+
"""Execute a hook callback by looking up in the callback registry."""
|
|
288
|
+
hook = self._hook_callbacks.get(callback_id)
|
|
289
|
+
if not hook:
|
|
290
|
+
return {"continue": True}
|
|
291
|
+
|
|
292
|
+
try:
|
|
293
|
+
result = await hook(hook_input, tool_use_id, {"signal": None})
|
|
294
|
+
return dict(result)
|
|
295
|
+
except Exception as e:
|
|
296
|
+
return {"continue": False, "stopReason": str(e)}
|
|
297
|
+
|
|
235
298
|
async def receive_response(self) -> AsyncIterator[Message]:
|
|
236
299
|
"""
|
|
237
|
-
Receive messages until and including a ResultMessage.
|
|
300
|
+
Receive messages until and including a ResultMessage or ErrorMessage.
|
|
238
301
|
|
|
239
302
|
Yields each message as it's received and terminates after
|
|
240
|
-
yielding a ResultMessage.
|
|
303
|
+
yielding a ResultMessage or ErrorMessage.
|
|
304
|
+
Raises ExecutionError if ResultMessage indicates an error.
|
|
241
305
|
"""
|
|
242
306
|
async for message in self.receive_messages():
|
|
243
|
-
|
|
307
|
+
# Check for execution error BEFORE yielding
|
|
244
308
|
if isinstance(message, ResultMessage):
|
|
309
|
+
if message.is_error and message.errors and len(message.errors) > 0:
|
|
310
|
+
raise ExecutionError(message.errors, message.subtype)
|
|
311
|
+
yield message
|
|
312
|
+
return
|
|
313
|
+
|
|
314
|
+
yield message
|
|
315
|
+
|
|
316
|
+
if isinstance(message, ErrorMessage):
|
|
245
317
|
return
|
|
246
318
|
|
|
247
319
|
async def interrupt(self) -> None:
|
|
@@ -287,12 +359,17 @@ class CodeBuddySDKClient:
|
|
|
287
359
|
self._transport = None
|
|
288
360
|
self._connected = False
|
|
289
361
|
|
|
290
|
-
async def __aenter__(self) ->
|
|
362
|
+
async def __aenter__(self) -> CodeBuddySDKClient:
|
|
291
363
|
"""Enter async context - automatically connects."""
|
|
292
364
|
await self.connect()
|
|
293
365
|
return self
|
|
294
366
|
|
|
295
|
-
async def __aexit__(
|
|
367
|
+
async def __aexit__(
|
|
368
|
+
self,
|
|
369
|
+
exc_type: type[BaseException] | None,
|
|
370
|
+
exc_val: BaseException | None,
|
|
371
|
+
exc_tb: TracebackType | None,
|
|
372
|
+
) -> bool:
|
|
296
373
|
"""Exit async context - always disconnects."""
|
|
297
374
|
await self.disconnect()
|
|
298
375
|
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)
|