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
@@ -1,83 +0,0 @@
1
- import inspect
2
- from typing import TYPE_CHECKING, Any, Callable
3
-
4
- from pydantic import BaseModel, Field
5
-
6
- import acp.server.highlevel
7
- from acp.server.highlevel.exceptions import ToolError
8
- from acp.server.highlevel.utilities.func_metadata import FuncMetadata, func_metadata
9
-
10
- if TYPE_CHECKING:
11
- from acp.server.highlevel.server import Context
12
-
13
-
14
- class Tool(BaseModel):
15
- """Internal tool registration info."""
16
-
17
- fn: Callable = Field(exclude=True)
18
- name: str = Field(description="Name of the tool")
19
- description: str = Field(description="Description of what the tool does")
20
- parameters: dict = Field(description="JSON schema for tool parameters")
21
- fn_metadata: FuncMetadata = Field(
22
- description="Metadata about the function including a pydantic model for tool"
23
- " arguments"
24
- )
25
- is_async: bool = Field(description="Whether the tool is async")
26
- context_kwarg: str | None = Field(
27
- None, description="Name of the kwarg that should receive context"
28
- )
29
-
30
- @classmethod
31
- def from_function(
32
- cls,
33
- fn: Callable,
34
- name: str | None = None,
35
- description: str | None = None,
36
- context_kwarg: str | None = None,
37
- ) -> "Tool":
38
- """Create a Tool from a function."""
39
- func_name = name or fn.__name__
40
-
41
- if func_name == "<lambda>":
42
- raise ValueError("You must provide a name for lambda functions")
43
-
44
- func_doc = description or fn.__doc__ or ""
45
- is_async = inspect.iscoroutinefunction(fn)
46
-
47
- # Find context parameter if it exists
48
- if context_kwarg is None:
49
- sig = inspect.signature(fn)
50
- for param_name, param in sig.parameters.items():
51
- if param.annotation is acp.server.highlevel.Context:
52
- context_kwarg = param_name
53
- break
54
-
55
- func_arg_metadata = func_metadata(
56
- fn,
57
- skip_names=[context_kwarg] if context_kwarg is not None else [],
58
- )
59
- parameters = func_arg_metadata.arg_model.model_json_schema()
60
-
61
- return cls(
62
- fn=fn,
63
- name=func_name,
64
- description=func_doc,
65
- parameters=parameters,
66
- fn_metadata=func_arg_metadata,
67
- is_async=is_async,
68
- context_kwarg=context_kwarg,
69
- )
70
-
71
- async def run(self, arguments: dict, context: "Context | None" = None) -> Any:
72
- """Run the tool with arguments."""
73
- try:
74
- return await self.fn_metadata.call_fn_with_arg_validation(
75
- self.fn,
76
- self.is_async,
77
- arguments,
78
- {self.context_kwarg: context}
79
- if self.context_kwarg is not None
80
- else None,
81
- )
82
- except Exception as e:
83
- raise ToolError(f"Error executing tool {self.name}: {e}") from e
@@ -1,53 +0,0 @@
1
- from collections.abc import Callable
2
- from typing import TYPE_CHECKING, Any
3
-
4
- from acp.server.highlevel.exceptions import ToolError
5
- from acp.server.highlevel.tools.base import Tool
6
- from acp.server.highlevel.utilities.logging import get_logger
7
-
8
- if TYPE_CHECKING:
9
- from acp.server.highlevel.server import Context
10
-
11
- logger = get_logger(__name__)
12
-
13
-
14
- class ToolManager:
15
- """Manages FastMCP tools."""
16
-
17
- def __init__(self, warn_on_duplicate_tools: bool = True):
18
- self._tools: dict[str, Tool] = {}
19
- self.warn_on_duplicate_tools = warn_on_duplicate_tools
20
-
21
- def get_tool(self, name: str) -> Tool | None:
22
- """Get tool by name."""
23
- return self._tools.get(name)
24
-
25
- def list_tools(self) -> list[Tool]:
26
- """List all registered tools."""
27
- return list(self._tools.values())
28
-
29
- def add_tool(
30
- self,
31
- fn: Callable,
32
- name: str | None = None,
33
- description: str | None = None,
34
- ) -> Tool:
35
- """Add a tool to the server."""
36
- tool = Tool.from_function(fn, name=name, description=description)
37
- existing = self._tools.get(tool.name)
38
- if existing:
39
- if self.warn_on_duplicate_tools:
40
- logger.warning(f"Tool already exists: {tool.name}")
41
- return existing
42
- self._tools[tool.name] = tool
43
- return tool
44
-
45
- async def call_tool(
46
- self, name: str, arguments: dict, context: "Context | None" = None
47
- ) -> Any:
48
- """Call a tool by name with arguments."""
49
- tool = self.get_tool(name)
50
- if not tool:
51
- raise ToolError(f"Unknown tool: {name}")
52
-
53
- return await tool.run(arguments, context=context)
@@ -1 +0,0 @@
1
- """FastMCP utility modules."""
@@ -1,210 +0,0 @@
1
- import inspect
2
- import json
3
- from collections.abc import Awaitable, Callable, Sequence
4
- from typing import (
5
- Annotated,
6
- Any,
7
- ForwardRef,
8
- )
9
-
10
- from pydantic import BaseModel, ConfigDict, Field, WithJsonSchema, create_model
11
- from pydantic._internal._typing_extra import eval_type_backport
12
- from pydantic.fields import FieldInfo
13
- from pydantic_core import PydanticUndefined
14
-
15
- from acp.server.highlevel.exceptions import InvalidSignature
16
- from acp.server.highlevel.utilities.logging import get_logger
17
-
18
- logger = get_logger(__name__)
19
-
20
-
21
- class ArgModelBase(BaseModel):
22
- """A model representing the arguments to a function."""
23
-
24
- def model_dump_one_level(self) -> dict[str, Any]:
25
- """Return a dict of the model's fields, one level deep.
26
-
27
- That is, sub-models etc are not dumped - they are kept as pydantic models.
28
- """
29
- kwargs: dict[str, Any] = {}
30
- for field_name in self.model_fields.keys():
31
- kwargs[field_name] = getattr(self, field_name)
32
- return kwargs
33
-
34
- model_config = ConfigDict(
35
- arbitrary_types_allowed=True,
36
- )
37
-
38
-
39
- class FuncMetadata(BaseModel):
40
- arg_model: Annotated[type[ArgModelBase], WithJsonSchema(None)]
41
- # We can add things in the future like
42
- # - Maybe some args are excluded from attempting to parse from JSON
43
- # - Maybe some args are special (like context) for dependency injection
44
-
45
- async def call_fn_with_arg_validation(
46
- self,
47
- fn: Callable[..., Any] | Awaitable[Any],
48
- fn_is_async: bool,
49
- arguments_to_validate: dict[str, Any],
50
- arguments_to_pass_directly: dict[str, Any] | None,
51
- ) -> Any:
52
- """Call the given function with arguments validated and injected.
53
-
54
- Arguments are first attempted to be parsed from JSON, then validated against
55
- the argument model, before being passed to the function.
56
- """
57
- arguments_pre_parsed = self.pre_parse_json(arguments_to_validate)
58
- arguments_parsed_model = self.arg_model.model_validate(arguments_pre_parsed)
59
- arguments_parsed_dict = arguments_parsed_model.model_dump_one_level()
60
-
61
- arguments_parsed_dict |= arguments_to_pass_directly or {}
62
-
63
- if fn_is_async:
64
- if isinstance(fn, Awaitable):
65
- return await fn
66
- return await fn(**arguments_parsed_dict)
67
- if isinstance(fn, Callable):
68
- return fn(**arguments_parsed_dict)
69
- raise TypeError("fn must be either Callable or Awaitable")
70
-
71
- def pre_parse_json(self, data: dict[str, Any]) -> dict[str, Any]:
72
- """Pre-parse data from JSON.
73
-
74
- Return a dict with same keys as input but with values parsed from JSON
75
- if appropriate.
76
-
77
- This is to handle cases like `["a", "b", "c"]` being passed in as JSON inside
78
- a string rather than an actual list. Claude desktop is prone to this - in fact
79
- it seems incapable of NOT doing this. For sub-models, it tends to pass
80
- dicts (JSON objects) as JSON strings, which can be pre-parsed here.
81
- """
82
- new_data = data.copy() # Shallow copy
83
- for field_name, field_info in self.arg_model.model_fields.items():
84
- if field_name not in data.keys():
85
- continue
86
- if isinstance(data[field_name], str):
87
- try:
88
- pre_parsed = json.loads(data[field_name])
89
- except json.JSONDecodeError:
90
- continue # Not JSON - skip
91
- if isinstance(pre_parsed, str):
92
- # This is likely that the raw value is e.g. `"hello"` which we
93
- # Should really be parsed as '"hello"' in Python - but if we parse
94
- # it as JSON it'll turn into just 'hello'. So we skip it.
95
- continue
96
- new_data[field_name] = pre_parsed
97
- assert new_data.keys() == data.keys()
98
- return new_data
99
-
100
- model_config = ConfigDict(
101
- arbitrary_types_allowed=True,
102
- )
103
-
104
-
105
- def func_metadata(func: Callable, skip_names: Sequence[str] = ()) -> FuncMetadata:
106
- """Given a function, return metadata including a pydantic model representing its
107
- signature.
108
-
109
- The use case for this is
110
- ```
111
- meta = func_to_pyd(func)
112
- validated_args = meta.arg_model.model_validate(some_raw_data_dict)
113
- return func(**validated_args.model_dump_one_level())
114
- ```
115
-
116
- **critically** it also provides pre-parse helper to attempt to parse things from
117
- JSON.
118
-
119
- Args:
120
- func: The function to convert to a pydantic model
121
- skip_names: A list of parameter names to skip. These will not be included in
122
- the model.
123
- Returns:
124
- A pydantic model representing the function's signature.
125
- """
126
- sig = _get_typed_signature(func)
127
- params = sig.parameters
128
- dynamic_pydantic_model_params: dict[str, Any] = {}
129
- globalns = getattr(func, "__globals__", {})
130
- for param in params.values():
131
- if param.name.startswith("_"):
132
- raise InvalidSignature(
133
- f"Parameter {param.name} of {func.__name__} cannot start with '_'"
134
- )
135
- if param.name in skip_names:
136
- continue
137
- annotation = param.annotation
138
-
139
- # `x: None` / `x: None = None`
140
- if annotation is None:
141
- annotation = Annotated[
142
- None,
143
- Field(
144
- default=param.default
145
- if param.default is not inspect.Parameter.empty
146
- else PydanticUndefined
147
- ),
148
- ]
149
-
150
- # Untyped field
151
- if annotation is inspect.Parameter.empty:
152
- annotation = Annotated[
153
- Any,
154
- Field(),
155
- # 🤷
156
- WithJsonSchema({"title": param.name, "type": "string"}),
157
- ]
158
-
159
- field_info = FieldInfo.from_annotated_attribute(
160
- _get_typed_annotation(annotation, globalns),
161
- param.default
162
- if param.default is not inspect.Parameter.empty
163
- else PydanticUndefined,
164
- )
165
- dynamic_pydantic_model_params[param.name] = (field_info.annotation, field_info)
166
- continue
167
-
168
- arguments_model = create_model(
169
- f"{func.__name__}Arguments",
170
- **dynamic_pydantic_model_params,
171
- __base__=ArgModelBase,
172
- )
173
- resp = FuncMetadata(arg_model=arguments_model)
174
- return resp
175
-
176
-
177
- def _get_typed_annotation(annotation: Any, globalns: dict[str, Any]) -> Any:
178
- def try_eval_type(value, globalns, localns):
179
- try:
180
- return eval_type_backport(value, globalns, localns), True
181
- except NameError:
182
- return value, False
183
-
184
- if isinstance(annotation, str):
185
- annotation = ForwardRef(annotation)
186
- annotation, status = try_eval_type(annotation, globalns, globalns)
187
-
188
- # This check and raise could perhaps be skipped, and we (FastMCP) just call
189
- # model_rebuild right before using it 🤷
190
- if status is False:
191
- raise InvalidSignature(f"Unable to evaluate type annotation {annotation}")
192
-
193
- return annotation
194
-
195
-
196
- def _get_typed_signature(call: Callable[..., Any]) -> inspect.Signature:
197
- """Get function signature while evaluating forward references"""
198
- signature = inspect.signature(call)
199
- globalns = getattr(call, "__globals__", {})
200
- typed_params = [
201
- inspect.Parameter(
202
- name=param.name,
203
- kind=param.kind,
204
- default=param.default,
205
- annotation=_get_typed_annotation(param.annotation, globalns),
206
- )
207
- for param in signature.parameters.values()
208
- ]
209
- typed_signature = inspect.Signature(typed_params)
210
- return typed_signature
@@ -1,43 +0,0 @@
1
- """Logging utilities for FastMCP."""
2
-
3
- import logging
4
- from typing import Literal
5
-
6
-
7
- def get_logger(name: str) -> logging.Logger:
8
- """Get a logger nested under MCPnamespace.
9
-
10
- Args:
11
- name: the name of the logger, which will be prefixed with 'FastMCP.'
12
-
13
- Returns:
14
- a configured logger instance
15
- """
16
- return logging.getLogger(name)
17
-
18
-
19
- def configure_logging(
20
- level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO",
21
- ) -> None:
22
- """Configure logging for MCP.
23
-
24
- Args:
25
- level: the log level to use
26
- """
27
- handlers = []
28
- try:
29
- from rich.console import Console
30
- from rich.logging import RichHandler
31
-
32
- handlers.append(RichHandler(console=Console(stderr=True), rich_tracebacks=True))
33
- except ImportError:
34
- pass
35
-
36
- if not handlers:
37
- handlers.append(logging.StreamHandler())
38
-
39
- logging.basicConfig(
40
- level=level,
41
- format="%(message)s",
42
- handlers=handlers,
43
- )
@@ -1,54 +0,0 @@
1
- """Common types used across FastMCP."""
2
-
3
- import base64
4
- from pathlib import Path
5
-
6
- from acp.types import ImageContent
7
-
8
-
9
- class Image:
10
- """Helper class for returning images from tools."""
11
-
12
- def __init__(
13
- self,
14
- path: str | Path | None = None,
15
- data: bytes | None = None,
16
- format: str | None = None,
17
- ):
18
- if path is None and data is None:
19
- raise ValueError("Either path or data must be provided")
20
- if path is not None and data is not None:
21
- raise ValueError("Only one of path or data can be provided")
22
-
23
- self.path = Path(path) if path else None
24
- self.data = data
25
- self._format = format
26
- self._mime_type = self._get_mime_type()
27
-
28
- def _get_mime_type(self) -> str:
29
- """Get MIME type from format or guess from file extension."""
30
- if self._format:
31
- return f"image/{self._format.lower()}"
32
-
33
- if self.path:
34
- suffix = self.path.suffix.lower()
35
- return {
36
- ".png": "image/png",
37
- ".jpg": "image/jpeg",
38
- ".jpeg": "image/jpeg",
39
- ".gif": "image/gif",
40
- ".webp": "image/webp",
41
- }.get(suffix, "application/octet-stream")
42
- return "image/png" # default for raw binary data
43
-
44
- def to_image_content(self) -> ImageContent:
45
- """Convert to MCP ImageContent."""
46
- if self.path:
47
- with open(self.path, "rb") as f:
48
- data = base64.b64encode(f.read()).decode()
49
- elif self.data is not None:
50
- data = base64.b64encode(self.data).decode()
51
- else:
52
- raise ValueError("No image data available")
53
-
54
- return ImageContent(type="image", data=data, mimeType=self._mime_type)
@@ -1,3 +0,0 @@
1
- from .server import NotificationOptions, Server
2
-
3
- __all__ = ["Server", "NotificationOptions"]
@@ -1,9 +0,0 @@
1
- from dataclasses import dataclass
2
-
3
-
4
- @dataclass
5
- class ReadResourceContents:
6
- """Contents returned from a read_resource call."""
7
-
8
- content: str | bytes
9
- mime_type: str | None = None