acp-sdk 0.0.6__py3-none-any.whl → 1.0.0rc1__py3-none-any.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.
Files changed (69) hide show
  1. acp_sdk/client/__init__.py +1 -0
  2. acp_sdk/client/client.py +135 -0
  3. acp_sdk/models.py +219 -0
  4. acp_sdk/server/__init__.py +2 -0
  5. acp_sdk/server/agent.py +32 -0
  6. acp_sdk/server/bundle.py +133 -0
  7. acp_sdk/server/context.py +6 -0
  8. acp_sdk/server/server.py +137 -0
  9. acp_sdk/server/telemetry.py +45 -0
  10. acp_sdk/server/utils.py +12 -0
  11. acp_sdk-1.0.0rc1.dist-info/METADATA +53 -0
  12. acp_sdk-1.0.0rc1.dist-info/RECORD +15 -0
  13. acp/__init__.py +0 -138
  14. acp/cli/__init__.py +0 -6
  15. acp/cli/claude.py +0 -139
  16. acp/cli/cli.py +0 -471
  17. acp/client/__main__.py +0 -79
  18. acp/client/session.py +0 -372
  19. acp/client/sse.py +0 -145
  20. acp/client/stdio.py +0 -153
  21. acp/server/__init__.py +0 -3
  22. acp/server/__main__.py +0 -50
  23. acp/server/highlevel/__init__.py +0 -9
  24. acp/server/highlevel/agents/__init__.py +0 -5
  25. acp/server/highlevel/agents/agent_manager.py +0 -110
  26. acp/server/highlevel/agents/base.py +0 -20
  27. acp/server/highlevel/agents/templates.py +0 -21
  28. acp/server/highlevel/context.py +0 -185
  29. acp/server/highlevel/exceptions.py +0 -25
  30. acp/server/highlevel/prompts/__init__.py +0 -4
  31. acp/server/highlevel/prompts/base.py +0 -167
  32. acp/server/highlevel/prompts/manager.py +0 -50
  33. acp/server/highlevel/prompts/prompt_manager.py +0 -33
  34. acp/server/highlevel/resources/__init__.py +0 -23
  35. acp/server/highlevel/resources/base.py +0 -48
  36. acp/server/highlevel/resources/resource_manager.py +0 -94
  37. acp/server/highlevel/resources/templates.py +0 -80
  38. acp/server/highlevel/resources/types.py +0 -185
  39. acp/server/highlevel/server.py +0 -705
  40. acp/server/highlevel/tools/__init__.py +0 -4
  41. acp/server/highlevel/tools/base.py +0 -83
  42. acp/server/highlevel/tools/tool_manager.py +0 -53
  43. acp/server/highlevel/utilities/__init__.py +0 -1
  44. acp/server/highlevel/utilities/func_metadata.py +0 -210
  45. acp/server/highlevel/utilities/logging.py +0 -43
  46. acp/server/highlevel/utilities/types.py +0 -54
  47. acp/server/lowlevel/__init__.py +0 -3
  48. acp/server/lowlevel/helper_types.py +0 -9
  49. acp/server/lowlevel/server.py +0 -643
  50. acp/server/models.py +0 -17
  51. acp/server/session.py +0 -315
  52. acp/server/sse.py +0 -175
  53. acp/server/stdio.py +0 -83
  54. acp/server/websocket.py +0 -61
  55. acp/shared/__init__.py +0 -0
  56. acp/shared/context.py +0 -14
  57. acp/shared/exceptions.py +0 -14
  58. acp/shared/memory.py +0 -87
  59. acp/shared/progress.py +0 -40
  60. acp/shared/session.py +0 -413
  61. acp/shared/version.py +0 -3
  62. acp/types.py +0 -1258
  63. acp_sdk-0.0.6.dist-info/METADATA +0 -46
  64. acp_sdk-0.0.6.dist-info/RECORD +0 -57
  65. acp_sdk-0.0.6.dist-info/entry_points.txt +0 -2
  66. acp_sdk-0.0.6.dist-info/licenses/LICENSE +0 -22
  67. {acp/client → acp_sdk}/__init__.py +0 -0
  68. {acp → acp_sdk}/py.typed +0 -0
  69. {acp_sdk-0.0.6.dist-info → acp_sdk-1.0.0rc1.dist-info}/WHEEL +0 -0
acp/shared/memory.py DELETED
@@ -1,87 +0,0 @@
1
- """
2
- In-memory transports
3
- """
4
-
5
- from contextlib import asynccontextmanager
6
- from datetime import timedelta
7
- from typing import AsyncGenerator
8
-
9
- import anyio
10
- from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
11
-
12
- from acp.client.session import ClientSession
13
- from acp.server import Server
14
- from acp.types import JSONRPCMessage
15
-
16
- MessageStream = tuple[
17
- MemoryObjectReceiveStream[JSONRPCMessage | Exception],
18
- MemoryObjectSendStream[JSONRPCMessage],
19
- ]
20
-
21
-
22
- @asynccontextmanager
23
- async def create_client_server_memory_streams() -> AsyncGenerator[
24
- tuple[MessageStream, MessageStream], None
25
- ]:
26
- """
27
- Creates a pair of bidirectional memory streams for client-server communication.
28
-
29
- Returns:
30
- A tuple of (client_streams, server_streams) where each is a tuple of
31
- (read_stream, write_stream)
32
- """
33
- # Create streams for both directions
34
- server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[
35
- JSONRPCMessage | Exception
36
- ](1)
37
- client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[
38
- JSONRPCMessage | Exception
39
- ](1)
40
-
41
- client_streams = (server_to_client_receive, client_to_server_send)
42
- server_streams = (client_to_server_receive, server_to_client_send)
43
-
44
- async with (
45
- server_to_client_receive,
46
- client_to_server_send,
47
- client_to_server_receive,
48
- server_to_client_send,
49
- ):
50
- yield client_streams, server_streams
51
-
52
-
53
- @asynccontextmanager
54
- async def create_connected_server_and_client_session(
55
- server: Server,
56
- read_timeout_seconds: timedelta | None = None,
57
- raise_exceptions: bool = False,
58
- ) -> AsyncGenerator[ClientSession, None]:
59
- """Creates a ClientSession that is connected to a running MCP server."""
60
- async with create_client_server_memory_streams() as (
61
- client_streams,
62
- server_streams,
63
- ):
64
- client_read, client_write = client_streams
65
- server_read, server_write = server_streams
66
-
67
- # Create a cancel scope for the server task
68
- async with anyio.create_task_group() as tg:
69
- tg.start_soon(
70
- lambda: server.run(
71
- server_read,
72
- server_write,
73
- server.create_initialization_options(),
74
- raise_exceptions=raise_exceptions,
75
- )
76
- )
77
-
78
- try:
79
- async with ClientSession(
80
- read_stream=client_read,
81
- write_stream=client_write,
82
- read_timeout_seconds=read_timeout_seconds,
83
- ) as client_session:
84
- await client_session.initialize()
85
- yield client_session
86
- finally:
87
- tg.cancel_scope.cancel()
acp/shared/progress.py DELETED
@@ -1,40 +0,0 @@
1
- from contextlib import contextmanager
2
- from dataclasses import dataclass, field
3
-
4
- from pydantic import BaseModel
5
-
6
- from acp.shared.context import RequestContext
7
- from acp.shared.session import BaseSession
8
- from acp.types import ProgressToken
9
-
10
-
11
- class Progress(BaseModel):
12
- progress: float
13
- total: float | None
14
-
15
-
16
- @dataclass
17
- class ProgressContext:
18
- session: BaseSession
19
- progress_token: ProgressToken
20
- total: float | None
21
- current: float = field(default=0.0, init=False)
22
-
23
- async def progress(self, amount: float) -> None:
24
- self.current += amount
25
-
26
- await self.session.send_progress_notification(
27
- self.progress_token, self.current, total=self.total
28
- )
29
-
30
-
31
- @contextmanager
32
- def progress(ctx: RequestContext, total: float | None = None):
33
- if ctx.meta is None or ctx.meta.progressToken is None:
34
- raise ValueError("No progress token provided")
35
-
36
- progress_ctx = ProgressContext(ctx.session, ctx.meta.progressToken, total)
37
- try:
38
- yield progress_ctx
39
- finally:
40
- pass
acp/shared/session.py DELETED
@@ -1,413 +0,0 @@
1
- import logging
2
- from contextlib import AbstractAsyncContextManager
3
- from datetime import timedelta
4
- from typing import Any, Callable, Generic, TypeVar
5
-
6
- import anyio
7
- import anyio.lowlevel
8
- import httpx
9
- from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
10
- from opentelemetry import trace
11
- from opentelemetry.baggage.propagation import W3CBaggagePropagator
12
- from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
13
- from pydantic import BaseModel
14
-
15
- from acp.shared.exceptions import McpError
16
- from acp.types import (
17
- CancelledNotification,
18
- ClientNotification,
19
- ClientRequest,
20
- ClientResult,
21
- ErrorData,
22
- JSONRPCError,
23
- JSONRPCMessage,
24
- JSONRPCNotification,
25
- JSONRPCRequest,
26
- JSONRPCResponse,
27
- RequestParams,
28
- ServerNotification,
29
- ServerRequest,
30
- ServerResult,
31
- )
32
-
33
- SendRequestT = TypeVar("SendRequestT", ClientRequest, ServerRequest)
34
- SendResultT = TypeVar("SendResultT", ClientResult, ServerResult)
35
- SendNotificationT = TypeVar("SendNotificationT", ClientNotification, ServerNotification)
36
- ReceiveRequestT = TypeVar("ReceiveRequestT", ClientRequest, ServerRequest)
37
- ReceiveResultT = TypeVar("ReceiveResultT", bound=BaseModel)
38
- ReceiveNotificationT = TypeVar(
39
- "ReceiveNotificationT", ClientNotification, ServerNotification
40
- )
41
-
42
- RequestId = str | int
43
-
44
-
45
- class RequestResponder(Generic[ReceiveRequestT, SendResultT]):
46
- """Handles responding to MCP requests and manages request lifecycle.
47
-
48
- This class MUST be used as a context manager to ensure proper cleanup and
49
- cancellation handling:
50
-
51
- Example:
52
- with request_responder as resp:
53
- await resp.respond(result)
54
-
55
- The context manager ensures:
56
- 1. Proper cancellation scope setup and cleanup
57
- 2. Request completion tracking
58
- 3. Cleanup of in-flight requests
59
- """
60
-
61
- def __init__(
62
- self,
63
- request_id: RequestId,
64
- request_meta: RequestParams.Meta | None,
65
- request: ReceiveRequestT,
66
- session: "BaseSession",
67
- on_complete: Callable[["RequestResponder[ReceiveRequestT, SendResultT]"], Any],
68
- ) -> None:
69
- self.request_id = request_id
70
- self.request_meta = request_meta
71
- self.request = request
72
- self._session = session
73
- self._completed = False
74
- self.task_group = anyio.create_task_group()
75
- self._on_complete = on_complete
76
- self._entered = False # Track if we're in a context manager
77
-
78
- async def __aenter__(self) -> "RequestResponder[ReceiveRequestT, SendResultT]":
79
- """Enter the context manager, enabling request cancellation tracking."""
80
- self._entered = True
81
- self.task_group = anyio.create_task_group()
82
- await self.task_group.__aenter__()
83
- return self
84
-
85
- async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
86
- """Exit the context manager, performing cleanup and notifying completion."""
87
- try:
88
- if not self.task_group:
89
- raise RuntimeError("No active cancel scope")
90
- await self.task_group.__aexit__(exc_type, exc_val, exc_tb)
91
- if self._completed:
92
- self._on_complete(self)
93
- finally:
94
- self._entered = False
95
-
96
- async def respond(self, response: SendResultT | ErrorData) -> None:
97
- """Send a response for this request.
98
-
99
- Must be called within a context manager block.
100
- Raises:
101
- RuntimeError: If not used within a context manager
102
- AssertionError: If request was already responded to
103
- """
104
- if not self._entered:
105
- raise RuntimeError("RequestResponder must be used as a context manager")
106
- assert not self._completed, "Request already responded to"
107
-
108
- if not self.cancelled:
109
- self._completed = True
110
-
111
- await self._session._send_response(
112
- request_id=self.request_id, response=response
113
- )
114
-
115
- async def cancel(self) -> None:
116
- """Cancel this request and mark it as completed."""
117
- if not self._entered:
118
- raise RuntimeError("RequestResponder must be used as a context manager")
119
- if not self.task_group:
120
- raise RuntimeError("No active cancel scope")
121
-
122
- self.task_group.cancel_scope.cancel()
123
- self._completed = True # Mark as completed so it's removed from in_flight
124
- # Send an error response to indicate cancellation
125
- await self._session._send_response(
126
- request_id=self.request_id,
127
- response=ErrorData(code=0, message="Request cancelled", data=None),
128
- )
129
-
130
- @property
131
- def in_flight(self) -> bool:
132
- return not self._completed and not self.cancelled
133
-
134
- @property
135
- def cancelled(self) -> bool:
136
- return (
137
- self.task_group is not None and self.task_group.cancel_scope.cancel_called
138
- )
139
-
140
-
141
- class BaseSession(
142
- AbstractAsyncContextManager,
143
- Generic[
144
- SendRequestT,
145
- SendNotificationT,
146
- SendResultT,
147
- ReceiveRequestT,
148
- ReceiveNotificationT,
149
- ],
150
- ):
151
- """
152
- Implements an MCP "session" on top of read/write streams, including features
153
- like request/response linking, notifications, and progress.
154
-
155
- This class is an async context manager that automatically starts processing
156
- messages when entered.
157
- """
158
-
159
- _response_streams: dict[
160
- RequestId, MemoryObjectSendStream[JSONRPCResponse | JSONRPCError]
161
- ]
162
- _request_id: int
163
- _in_flight: dict[RequestId, RequestResponder[ReceiveRequestT, SendResultT]]
164
-
165
- def __init__(
166
- self,
167
- read_stream: MemoryObjectReceiveStream[JSONRPCMessage | Exception],
168
- write_stream: MemoryObjectSendStream[JSONRPCMessage],
169
- receive_request_type: type[ReceiveRequestT],
170
- receive_notification_type: type[ReceiveNotificationT],
171
- # If none, reading will never time out
172
- read_timeout_seconds: timedelta | None = None,
173
- ) -> None:
174
- self._read_stream = read_stream
175
- self._write_stream = write_stream
176
- self._response_streams = {}
177
- self._request_id = 0
178
- self._receive_request_type = receive_request_type
179
- self._receive_notification_type = receive_notification_type
180
- self._read_timeout_seconds = read_timeout_seconds
181
- self._in_flight = {}
182
-
183
- self._incoming_message_stream_writer, self._incoming_message_stream_reader = (
184
- anyio.create_memory_object_stream[
185
- RequestResponder[ReceiveRequestT, SendResultT]
186
- | ReceiveNotificationT
187
- | Exception
188
- ]()
189
- )
190
-
191
- async def __aenter__(self):
192
- self._task_group = anyio.create_task_group()
193
- await self._task_group.__aenter__()
194
- self._task_group.start_soon(self._receive_loop)
195
- return self
196
-
197
- async def __aexit__(self, exc_type, exc_val, exc_tb):
198
- # Using BaseSession as a context manager should not block on exit (this
199
- # would be very surprising behavior), so make sure to cancel the tasks
200
- # in the task group.
201
- self._task_group.cancel_scope.cancel()
202
- return await self._task_group.__aexit__(exc_type, exc_val, exc_tb)
203
-
204
- async def send_request(
205
- self,
206
- request: SendRequestT,
207
- result_type: type[ReceiveResultT],
208
- request_id: RequestId | None = None,
209
- ) -> ReceiveResultT:
210
- """
211
- Sends a request and wait for a response. Raises an McpError if the
212
- response contains an error.
213
-
214
- Do not use this method to emit notifications! Use send_notification()
215
- instead.
216
- """
217
-
218
- if request_id is None:
219
- request_id = self._request_id
220
- self._request_id = request_id + 1
221
-
222
- response_stream, response_stream_reader = anyio.create_memory_object_stream[
223
- JSONRPCResponse | JSONRPCError
224
- ](1)
225
- self._response_streams[request_id] = response_stream
226
-
227
- jsonrpc_request = JSONRPCRequest(
228
- jsonrpc="2.0",
229
- id=request_id,
230
- **request.model_dump(by_alias=True, mode="json", exclude_none=True),
231
- )
232
-
233
- with trace.get_tracer(__name__).start_as_current_span(jsonrpc_request.method):
234
- meta = (
235
- jsonrpc_request.params.get("_meta", {})
236
- if jsonrpc_request.params is not None
237
- else {}
238
- )
239
- W3CBaggagePropagator().inject(meta)
240
- TraceContextTextMapPropagator().inject(meta)
241
- if jsonrpc_request.params is None:
242
- jsonrpc_request.params = {}
243
- jsonrpc_request.params["_meta"] = meta
244
-
245
- # TODO: Support progress callbacks
246
-
247
- await self._write_stream.send(JSONRPCMessage(jsonrpc_request))
248
-
249
- try:
250
- with anyio.fail_after(
251
- None
252
- if self._read_timeout_seconds is None
253
- else self._read_timeout_seconds.total_seconds()
254
- ):
255
- response_or_error = await response_stream_reader.receive()
256
- except TimeoutError:
257
- raise McpError(
258
- ErrorData(
259
- code=httpx.codes.REQUEST_TIMEOUT,
260
- message=(
261
- f"Timed out while waiting for response to "
262
- f"{request.__class__.__name__}. Waited "
263
- f"{self._read_timeout_seconds} seconds."
264
- ),
265
- )
266
- )
267
-
268
- if isinstance(response_or_error, JSONRPCError):
269
- raise McpError(response_or_error.error)
270
- else:
271
- return result_type.model_validate(response_or_error.result)
272
-
273
- async def send_notification(self, notification: SendNotificationT) -> None:
274
- """
275
- Emits a notification, which is a one-way message that does not expect
276
- a response.
277
- """
278
- jsonrpc_notification = JSONRPCNotification(
279
- jsonrpc="2.0",
280
- **notification.model_dump(by_alias=True, mode="json", exclude_none=True),
281
- )
282
-
283
- await self._write_stream.send(JSONRPCMessage(jsonrpc_notification))
284
-
285
- async def _send_response(
286
- self, request_id: RequestId, response: SendResultT | ErrorData
287
- ) -> None:
288
- if isinstance(response, ErrorData):
289
- jsonrpc_error = JSONRPCError(jsonrpc="2.0", id=request_id, error=response)
290
- await self._write_stream.send(JSONRPCMessage(jsonrpc_error))
291
- else:
292
- jsonrpc_response = JSONRPCResponse(
293
- jsonrpc="2.0",
294
- id=request_id,
295
- result=response.model_dump(
296
- by_alias=True, mode="json", exclude_none=True
297
- ),
298
- )
299
- await self._write_stream.send(JSONRPCMessage(jsonrpc_response))
300
-
301
- async def _receive_loop(self) -> None:
302
- async with (
303
- self._read_stream,
304
- self._write_stream,
305
- self._incoming_message_stream_writer,
306
- ):
307
- async for message in self._read_stream:
308
- if isinstance(message, Exception):
309
- await self._incoming_message_stream_writer.send(message)
310
- elif isinstance(message.root, JSONRPCRequest):
311
- validated_request = self._receive_request_type.model_validate(
312
- message.root.model_dump(
313
- by_alias=True, mode="json", exclude_none=True
314
- )
315
- )
316
-
317
- responder = RequestResponder(
318
- request_id=message.root.id,
319
- request_meta=validated_request.root.params.meta
320
- if validated_request.root.params
321
- else None,
322
- request=validated_request,
323
- session=self,
324
- on_complete=lambda r: self._in_flight.pop(r.request_id, None),
325
- )
326
-
327
- meta = (
328
- responder.request_meta.model_dump()
329
- if responder.request_meta
330
- else {}
331
- )
332
- ctx = TraceContextTextMapPropagator().extract(carrier=meta)
333
- ctx = W3CBaggagePropagator().extract(carrier=meta, context=ctx)
334
- with trace.get_tracer(__name__).start_span(
335
- validated_request.root.method,
336
- context=ctx,
337
- kind=trace.SpanKind.SERVER,
338
- ):
339
- self._in_flight[responder.request_id] = responder
340
- await self._received_request(responder)
341
-
342
- if not responder._completed:
343
- await self._incoming_message_stream_writer.send(responder)
344
-
345
- elif isinstance(message.root, JSONRPCNotification):
346
- try:
347
- notification = self._receive_notification_type.model_validate(
348
- message.root.model_dump(
349
- by_alias=True, mode="json", exclude_none=True
350
- )
351
- )
352
- # Handle cancellation notifications
353
- if isinstance(notification.root, CancelledNotification):
354
- cancelled_id = notification.root.params.requestId
355
- if cancelled_id in self._in_flight:
356
- await self._in_flight[cancelled_id].cancel()
357
- else:
358
- await self._received_notification(notification)
359
- await self._incoming_message_stream_writer.send(
360
- notification
361
- )
362
- except Exception as e:
363
- # For other validation errors, log and continue
364
- logging.warning(
365
- f"Failed to validate notification: {e}. "
366
- f"Message was: {message.root}"
367
- )
368
- else: # Response or error
369
- stream = self._response_streams.pop(message.root.id, None)
370
- if stream:
371
- await stream.send(message.root)
372
- else:
373
- await self._incoming_message_stream_writer.send(
374
- RuntimeError(
375
- "Received response with an unknown "
376
- f"request ID: {message}"
377
- )
378
- )
379
-
380
- async def _received_request(
381
- self, responder: RequestResponder[ReceiveRequestT, SendResultT]
382
- ) -> None:
383
- """
384
- Can be overridden by subclasses to handle a request without needing to
385
- listen on the message stream.
386
-
387
- If the request is responded to within this method, it will not be
388
- forwarded on to the message stream.
389
- """
390
-
391
- async def _received_notification(self, notification: ReceiveNotificationT) -> None:
392
- """
393
- Can be overridden by subclasses to handle a notification without needing
394
- to listen on the message stream.
395
- """
396
-
397
- async def send_progress_notification(
398
- self, progress_token: str | int, progress: float, total: float | None = None
399
- ) -> None:
400
- """
401
- Sends a progress notification for a request that is currently being
402
- processed.
403
- """
404
-
405
- @property
406
- def incoming_messages(
407
- self,
408
- ) -> MemoryObjectReceiveStream[
409
- RequestResponder[ReceiveRequestT, SendResultT]
410
- | ReceiveNotificationT
411
- | Exception
412
- ]:
413
- return self._incoming_message_stream_reader
acp/shared/version.py DELETED
@@ -1,3 +0,0 @@
1
- from acp.types import LATEST_PROTOCOL_VERSION
2
-
3
- SUPPORTED_PROTOCOL_VERSIONS = [1, LATEST_PROTOCOL_VERSION]