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.
- acp_sdk/client/__init__.py +1 -0
- acp_sdk/client/client.py +135 -0
- acp_sdk/models.py +219 -0
- acp_sdk/server/__init__.py +2 -0
- acp_sdk/server/agent.py +32 -0
- acp_sdk/server/bundle.py +133 -0
- acp_sdk/server/context.py +6 -0
- acp_sdk/server/server.py +137 -0
- acp_sdk/server/telemetry.py +45 -0
- acp_sdk/server/utils.py +12 -0
- acp_sdk-1.0.0rc1.dist-info/METADATA +53 -0
- acp_sdk-1.0.0rc1.dist-info/RECORD +15 -0
- acp/__init__.py +0 -138
- acp/cli/__init__.py +0 -6
- acp/cli/claude.py +0 -139
- acp/cli/cli.py +0 -471
- acp/client/__main__.py +0 -79
- acp/client/session.py +0 -372
- acp/client/sse.py +0 -145
- acp/client/stdio.py +0 -153
- acp/server/__init__.py +0 -3
- acp/server/__main__.py +0 -50
- acp/server/highlevel/__init__.py +0 -9
- acp/server/highlevel/agents/__init__.py +0 -5
- acp/server/highlevel/agents/agent_manager.py +0 -110
- acp/server/highlevel/agents/base.py +0 -20
- acp/server/highlevel/agents/templates.py +0 -21
- acp/server/highlevel/context.py +0 -185
- acp/server/highlevel/exceptions.py +0 -25
- acp/server/highlevel/prompts/__init__.py +0 -4
- acp/server/highlevel/prompts/base.py +0 -167
- acp/server/highlevel/prompts/manager.py +0 -50
- acp/server/highlevel/prompts/prompt_manager.py +0 -33
- acp/server/highlevel/resources/__init__.py +0 -23
- acp/server/highlevel/resources/base.py +0 -48
- acp/server/highlevel/resources/resource_manager.py +0 -94
- acp/server/highlevel/resources/templates.py +0 -80
- acp/server/highlevel/resources/types.py +0 -185
- acp/server/highlevel/server.py +0 -705
- acp/server/highlevel/tools/__init__.py +0 -4
- acp/server/highlevel/tools/base.py +0 -83
- acp/server/highlevel/tools/tool_manager.py +0 -53
- acp/server/highlevel/utilities/__init__.py +0 -1
- acp/server/highlevel/utilities/func_metadata.py +0 -210
- acp/server/highlevel/utilities/logging.py +0 -43
- acp/server/highlevel/utilities/types.py +0 -54
- acp/server/lowlevel/__init__.py +0 -3
- acp/server/lowlevel/helper_types.py +0 -9
- acp/server/lowlevel/server.py +0 -643
- acp/server/models.py +0 -17
- acp/server/session.py +0 -315
- acp/server/sse.py +0 -175
- acp/server/stdio.py +0 -83
- acp/server/websocket.py +0 -61
- acp/shared/__init__.py +0 -0
- acp/shared/context.py +0 -14
- acp/shared/exceptions.py +0 -14
- acp/shared/memory.py +0 -87
- acp/shared/progress.py +0 -40
- acp/shared/session.py +0 -413
- acp/shared/version.py +0 -3
- acp/types.py +0 -1258
- acp_sdk-0.0.6.dist-info/METADATA +0 -46
- acp_sdk-0.0.6.dist-info/RECORD +0 -57
- acp_sdk-0.0.6.dist-info/entry_points.txt +0 -2
- acp_sdk-0.0.6.dist-info/licenses/LICENSE +0 -22
- {acp/client → acp_sdk}/__init__.py +0 -0
- {acp → acp_sdk}/py.typed +0 -0
- {acp_sdk-0.0.6.dist-info → acp_sdk-1.0.0rc1.dist-info}/WHEEL +0 -0
@@ -1,23 +0,0 @@
|
|
1
|
-
from .base import Resource
|
2
|
-
from .resource_manager import ResourceManager
|
3
|
-
from .templates import ResourceTemplate
|
4
|
-
from .types import (
|
5
|
-
BinaryResource,
|
6
|
-
DirectoryResource,
|
7
|
-
FileResource,
|
8
|
-
FunctionResource,
|
9
|
-
HttpResource,
|
10
|
-
TextResource,
|
11
|
-
)
|
12
|
-
|
13
|
-
__all__ = [
|
14
|
-
"Resource",
|
15
|
-
"TextResource",
|
16
|
-
"BinaryResource",
|
17
|
-
"FunctionResource",
|
18
|
-
"FileResource",
|
19
|
-
"HttpResource",
|
20
|
-
"DirectoryResource",
|
21
|
-
"ResourceTemplate",
|
22
|
-
"ResourceManager",
|
23
|
-
]
|
@@ -1,48 +0,0 @@
|
|
1
|
-
"""Base classes and interfaces for FastMCP resources."""
|
2
|
-
|
3
|
-
import abc
|
4
|
-
from typing import Annotated
|
5
|
-
|
6
|
-
from pydantic import (
|
7
|
-
AnyUrl,
|
8
|
-
BaseModel,
|
9
|
-
ConfigDict,
|
10
|
-
Field,
|
11
|
-
UrlConstraints,
|
12
|
-
ValidationInfo,
|
13
|
-
field_validator,
|
14
|
-
)
|
15
|
-
|
16
|
-
|
17
|
-
class Resource(BaseModel, abc.ABC):
|
18
|
-
"""Base class for all resources."""
|
19
|
-
|
20
|
-
model_config = ConfigDict(validate_default=True)
|
21
|
-
|
22
|
-
uri: Annotated[AnyUrl, UrlConstraints(host_required=False)] = Field(
|
23
|
-
default=..., description="URI of the resource"
|
24
|
-
)
|
25
|
-
name: str | None = Field(description="Name of the resource", default=None)
|
26
|
-
description: str | None = Field(
|
27
|
-
description="Description of the resource", default=None
|
28
|
-
)
|
29
|
-
mime_type: str = Field(
|
30
|
-
default="text/plain",
|
31
|
-
description="MIME type of the resource content",
|
32
|
-
pattern=r"^[a-zA-Z0-9]+/[a-zA-Z0-9\-+.]+$",
|
33
|
-
)
|
34
|
-
|
35
|
-
@field_validator("name", mode="before")
|
36
|
-
@classmethod
|
37
|
-
def set_default_name(cls, name: str | None, info: ValidationInfo) -> str:
|
38
|
-
"""Set default name from URI if not provided."""
|
39
|
-
if name:
|
40
|
-
return name
|
41
|
-
if uri := info.data.get("uri"):
|
42
|
-
return str(uri)
|
43
|
-
raise ValueError("Either name or uri must be provided")
|
44
|
-
|
45
|
-
@abc.abstractmethod
|
46
|
-
async def read(self) -> str | bytes:
|
47
|
-
"""Read the resource content."""
|
48
|
-
pass
|
@@ -1,94 +0,0 @@
|
|
1
|
-
"""Resource manager functionality."""
|
2
|
-
|
3
|
-
from typing import Callable
|
4
|
-
|
5
|
-
from pydantic import AnyUrl
|
6
|
-
|
7
|
-
from acp.server.highlevel.resources.base import Resource
|
8
|
-
from acp.server.highlevel.resources.templates import ResourceTemplate
|
9
|
-
from acp.server.highlevel.utilities.logging import get_logger
|
10
|
-
|
11
|
-
logger = get_logger(__name__)
|
12
|
-
|
13
|
-
|
14
|
-
class ResourceManager:
|
15
|
-
"""Manages FastMCP resources."""
|
16
|
-
|
17
|
-
def __init__(self, warn_on_duplicate_resources: bool = True):
|
18
|
-
self._resources: dict[str, Resource] = {}
|
19
|
-
self._templates: dict[str, ResourceTemplate] = {}
|
20
|
-
self.warn_on_duplicate_resources = warn_on_duplicate_resources
|
21
|
-
|
22
|
-
def add_resource(self, resource: Resource) -> Resource:
|
23
|
-
"""Add a resource to the manager.
|
24
|
-
|
25
|
-
Args:
|
26
|
-
resource: A Resource instance to add
|
27
|
-
|
28
|
-
Returns:
|
29
|
-
The added resource. If a resource with the same URI already exists,
|
30
|
-
returns the existing resource.
|
31
|
-
"""
|
32
|
-
logger.debug(
|
33
|
-
"Adding resource",
|
34
|
-
extra={
|
35
|
-
"uri": resource.uri,
|
36
|
-
"type": type(resource).__name__,
|
37
|
-
"resource_name": resource.name,
|
38
|
-
},
|
39
|
-
)
|
40
|
-
existing = self._resources.get(str(resource.uri))
|
41
|
-
if existing:
|
42
|
-
if self.warn_on_duplicate_resources:
|
43
|
-
logger.warning(f"Resource already exists: {resource.uri}")
|
44
|
-
return existing
|
45
|
-
self._resources[str(resource.uri)] = resource
|
46
|
-
return resource
|
47
|
-
|
48
|
-
def add_template(
|
49
|
-
self,
|
50
|
-
fn: Callable,
|
51
|
-
uri_template: str,
|
52
|
-
name: str | None = None,
|
53
|
-
description: str | None = None,
|
54
|
-
mime_type: str | None = None,
|
55
|
-
) -> ResourceTemplate:
|
56
|
-
"""Add a template from a function."""
|
57
|
-
template = ResourceTemplate.from_function(
|
58
|
-
fn,
|
59
|
-
uri_template=uri_template,
|
60
|
-
name=name,
|
61
|
-
description=description,
|
62
|
-
mime_type=mime_type,
|
63
|
-
)
|
64
|
-
self._templates[template.uri_template] = template
|
65
|
-
return template
|
66
|
-
|
67
|
-
async def get_resource(self, uri: AnyUrl | str) -> Resource | None:
|
68
|
-
"""Get resource by URI, checking concrete resources first, then templates."""
|
69
|
-
uri_str = str(uri)
|
70
|
-
logger.debug("Getting resource", extra={"uri": uri_str})
|
71
|
-
|
72
|
-
# First check concrete resources
|
73
|
-
if resource := self._resources.get(uri_str):
|
74
|
-
return resource
|
75
|
-
|
76
|
-
# Then check templates
|
77
|
-
for template in self._templates.values():
|
78
|
-
if params := template.matches(uri_str):
|
79
|
-
try:
|
80
|
-
return await template.create_resource(uri_str, params)
|
81
|
-
except Exception as e:
|
82
|
-
raise ValueError(f"Error creating resource from template: {e}")
|
83
|
-
|
84
|
-
raise ValueError(f"Unknown resource: {uri}")
|
85
|
-
|
86
|
-
def list_resources(self) -> list[Resource]:
|
87
|
-
"""List all registered resources."""
|
88
|
-
logger.debug("Listing resources", extra={"count": len(self._resources)})
|
89
|
-
return list(self._resources.values())
|
90
|
-
|
91
|
-
def list_templates(self) -> list[ResourceTemplate]:
|
92
|
-
"""List all registered templates."""
|
93
|
-
logger.debug("Listing templates", extra={"count": len(self._templates)})
|
94
|
-
return list(self._templates.values())
|
@@ -1,80 +0,0 @@
|
|
1
|
-
"""Resource template functionality."""
|
2
|
-
|
3
|
-
import inspect
|
4
|
-
import re
|
5
|
-
from typing import Any, Callable
|
6
|
-
|
7
|
-
from pydantic import BaseModel, Field, TypeAdapter, validate_call
|
8
|
-
|
9
|
-
from acp.server.highlevel.resources.types import FunctionResource, Resource
|
10
|
-
|
11
|
-
|
12
|
-
class ResourceTemplate(BaseModel):
|
13
|
-
"""A template for dynamically creating resources."""
|
14
|
-
|
15
|
-
uri_template: str = Field(
|
16
|
-
description="URI template with parameters (e.g. weather://{city}/current)"
|
17
|
-
)
|
18
|
-
name: str = Field(description="Name of the resource")
|
19
|
-
description: str | None = Field(description="Description of what the resource does")
|
20
|
-
mime_type: str = Field(
|
21
|
-
default="text/plain", description="MIME type of the resource content"
|
22
|
-
)
|
23
|
-
fn: Callable = Field(exclude=True)
|
24
|
-
parameters: dict = Field(description="JSON schema for function parameters")
|
25
|
-
|
26
|
-
@classmethod
|
27
|
-
def from_function(
|
28
|
-
cls,
|
29
|
-
fn: Callable,
|
30
|
-
uri_template: str,
|
31
|
-
name: str | None = None,
|
32
|
-
description: str | None = None,
|
33
|
-
mime_type: str | None = None,
|
34
|
-
) -> "ResourceTemplate":
|
35
|
-
"""Create a template from a function."""
|
36
|
-
func_name = name or fn.__name__
|
37
|
-
if func_name == "<lambda>":
|
38
|
-
raise ValueError("You must provide a name for lambda functions")
|
39
|
-
|
40
|
-
# Get schema from TypeAdapter - will fail if function isn't properly typed
|
41
|
-
parameters = TypeAdapter(fn).json_schema()
|
42
|
-
|
43
|
-
# ensure the arguments are properly cast
|
44
|
-
fn = validate_call(fn)
|
45
|
-
|
46
|
-
return cls(
|
47
|
-
uri_template=uri_template,
|
48
|
-
name=func_name,
|
49
|
-
description=description or fn.__doc__ or "",
|
50
|
-
mime_type=mime_type or "text/plain",
|
51
|
-
fn=fn,
|
52
|
-
parameters=parameters,
|
53
|
-
)
|
54
|
-
|
55
|
-
def matches(self, uri: str) -> dict[str, Any] | None:
|
56
|
-
"""Check if URI matches template and extract parameters."""
|
57
|
-
# Convert template to regex pattern
|
58
|
-
pattern = self.uri_template.replace("{", "(?P<").replace("}", ">[^/]+)")
|
59
|
-
match = re.match(f"^{pattern}$", uri)
|
60
|
-
if match:
|
61
|
-
return match.groupdict()
|
62
|
-
return None
|
63
|
-
|
64
|
-
async def create_resource(self, uri: str, params: dict[str, Any]) -> Resource:
|
65
|
-
"""Create a resource from the template with the given parameters."""
|
66
|
-
try:
|
67
|
-
# Call function and check if result is a coroutine
|
68
|
-
result = self.fn(**params)
|
69
|
-
if inspect.iscoroutine(result):
|
70
|
-
result = await result
|
71
|
-
|
72
|
-
return FunctionResource(
|
73
|
-
uri=uri, # type: ignore
|
74
|
-
name=self.name,
|
75
|
-
description=self.description,
|
76
|
-
mime_type=self.mime_type,
|
77
|
-
fn=lambda: result, # Capture result in closure
|
78
|
-
)
|
79
|
-
except Exception as e:
|
80
|
-
raise ValueError(f"Error creating resource from template: {e}")
|
@@ -1,185 +0,0 @@
|
|
1
|
-
"""Concrete resource implementations."""
|
2
|
-
|
3
|
-
import inspect
|
4
|
-
import json
|
5
|
-
from collections.abc import Callable
|
6
|
-
from pathlib import Path
|
7
|
-
from typing import Any
|
8
|
-
|
9
|
-
import anyio
|
10
|
-
import anyio.to_thread
|
11
|
-
import httpx
|
12
|
-
import pydantic.json
|
13
|
-
import pydantic_core
|
14
|
-
from pydantic import Field, ValidationInfo
|
15
|
-
|
16
|
-
from acp.server.highlevel.resources.base import Resource
|
17
|
-
|
18
|
-
|
19
|
-
class TextResource(Resource):
|
20
|
-
"""A resource that reads from a string."""
|
21
|
-
|
22
|
-
text: str = Field(description="Text content of the resource")
|
23
|
-
|
24
|
-
async def read(self) -> str:
|
25
|
-
"""Read the text content."""
|
26
|
-
return self.text
|
27
|
-
|
28
|
-
|
29
|
-
class BinaryResource(Resource):
|
30
|
-
"""A resource that reads from bytes."""
|
31
|
-
|
32
|
-
data: bytes = Field(description="Binary content of the resource")
|
33
|
-
|
34
|
-
async def read(self) -> bytes:
|
35
|
-
"""Read the binary content."""
|
36
|
-
return self.data
|
37
|
-
|
38
|
-
|
39
|
-
class FunctionResource(Resource):
|
40
|
-
"""A resource that defers data loading by wrapping a function.
|
41
|
-
|
42
|
-
The function is only called when the resource is read, allowing for lazy loading
|
43
|
-
of potentially expensive data. This is particularly useful when listing resources,
|
44
|
-
as the function won't be called until the resource is actually accessed.
|
45
|
-
|
46
|
-
The function can return:
|
47
|
-
- str for text content (default)
|
48
|
-
- bytes for binary content
|
49
|
-
- other types will be converted to JSON
|
50
|
-
"""
|
51
|
-
|
52
|
-
fn: Callable[[], Any] = Field(exclude=True)
|
53
|
-
|
54
|
-
async def read(self) -> str | bytes:
|
55
|
-
"""Read the resource by calling the wrapped function."""
|
56
|
-
try:
|
57
|
-
result = (
|
58
|
-
await self.fn() if inspect.iscoroutinefunction(self.fn) else self.fn()
|
59
|
-
)
|
60
|
-
if isinstance(result, Resource):
|
61
|
-
return await result.read()
|
62
|
-
if isinstance(result, bytes):
|
63
|
-
return result
|
64
|
-
if isinstance(result, str):
|
65
|
-
return result
|
66
|
-
try:
|
67
|
-
return json.dumps(pydantic_core.to_jsonable_python(result))
|
68
|
-
except (TypeError, pydantic_core.PydanticSerializationError):
|
69
|
-
# If JSON serialization fails, try str()
|
70
|
-
return str(result)
|
71
|
-
except Exception as e:
|
72
|
-
raise ValueError(f"Error reading resource {self.uri}: {e}")
|
73
|
-
|
74
|
-
|
75
|
-
class FileResource(Resource):
|
76
|
-
"""A resource that reads from a file.
|
77
|
-
|
78
|
-
Set is_binary=True to read file as binary data instead of text.
|
79
|
-
"""
|
80
|
-
|
81
|
-
path: Path = Field(description="Path to the file")
|
82
|
-
is_binary: bool = Field(
|
83
|
-
default=False,
|
84
|
-
description="Whether to read the file as binary data",
|
85
|
-
)
|
86
|
-
mime_type: str = Field(
|
87
|
-
default="text/plain",
|
88
|
-
description="MIME type of the resource content",
|
89
|
-
)
|
90
|
-
|
91
|
-
@pydantic.field_validator("path")
|
92
|
-
@classmethod
|
93
|
-
def validate_absolute_path(cls, path: Path) -> Path:
|
94
|
-
"""Ensure path is absolute."""
|
95
|
-
if not path.is_absolute():
|
96
|
-
raise ValueError("Path must be absolute")
|
97
|
-
return path
|
98
|
-
|
99
|
-
@pydantic.field_validator("is_binary")
|
100
|
-
@classmethod
|
101
|
-
def set_binary_from_mime_type(cls, is_binary: bool, info: ValidationInfo) -> bool:
|
102
|
-
"""Set is_binary based on mime_type if not explicitly set."""
|
103
|
-
if is_binary:
|
104
|
-
return True
|
105
|
-
mime_type = info.data.get("mime_type", "text/plain")
|
106
|
-
return not mime_type.startswith("text/")
|
107
|
-
|
108
|
-
async def read(self) -> str | bytes:
|
109
|
-
"""Read the file content."""
|
110
|
-
try:
|
111
|
-
if self.is_binary:
|
112
|
-
return await anyio.to_thread.run_sync(self.path.read_bytes)
|
113
|
-
return await anyio.to_thread.run_sync(self.path.read_text)
|
114
|
-
except Exception as e:
|
115
|
-
raise ValueError(f"Error reading file {self.path}: {e}")
|
116
|
-
|
117
|
-
|
118
|
-
class HttpResource(Resource):
|
119
|
-
"""A resource that reads from an HTTP endpoint."""
|
120
|
-
|
121
|
-
url: str = Field(description="URL to fetch content from")
|
122
|
-
mime_type: str = Field(
|
123
|
-
default="application/json", description="MIME type of the resource content"
|
124
|
-
)
|
125
|
-
|
126
|
-
async def read(self) -> str | bytes:
|
127
|
-
"""Read the HTTP content."""
|
128
|
-
async with httpx.AsyncClient() as client:
|
129
|
-
response = await client.get(self.url)
|
130
|
-
response.raise_for_status()
|
131
|
-
return response.text
|
132
|
-
|
133
|
-
|
134
|
-
class DirectoryResource(Resource):
|
135
|
-
"""A resource that lists files in a directory."""
|
136
|
-
|
137
|
-
path: Path = Field(description="Path to the directory")
|
138
|
-
recursive: bool = Field(
|
139
|
-
default=False, description="Whether to list files recursively"
|
140
|
-
)
|
141
|
-
pattern: str | None = Field(
|
142
|
-
default=None, description="Optional glob pattern to filter files"
|
143
|
-
)
|
144
|
-
mime_type: str = Field(
|
145
|
-
default="application/json", description="MIME type of the resource content"
|
146
|
-
)
|
147
|
-
|
148
|
-
@pydantic.field_validator("path")
|
149
|
-
@classmethod
|
150
|
-
def validate_absolute_path(cls, path: Path) -> Path:
|
151
|
-
"""Ensure path is absolute."""
|
152
|
-
if not path.is_absolute():
|
153
|
-
raise ValueError("Path must be absolute")
|
154
|
-
return path
|
155
|
-
|
156
|
-
def list_files(self) -> list[Path]:
|
157
|
-
"""List files in the directory."""
|
158
|
-
if not self.path.exists():
|
159
|
-
raise FileNotFoundError(f"Directory not found: {self.path}")
|
160
|
-
if not self.path.is_dir():
|
161
|
-
raise NotADirectoryError(f"Not a directory: {self.path}")
|
162
|
-
|
163
|
-
try:
|
164
|
-
if self.pattern:
|
165
|
-
return (
|
166
|
-
list(self.path.glob(self.pattern))
|
167
|
-
if not self.recursive
|
168
|
-
else list(self.path.rglob(self.pattern))
|
169
|
-
)
|
170
|
-
return (
|
171
|
-
list(self.path.glob("*"))
|
172
|
-
if not self.recursive
|
173
|
-
else list(self.path.rglob("*"))
|
174
|
-
)
|
175
|
-
except Exception as e:
|
176
|
-
raise ValueError(f"Error listing directory {self.path}: {e}")
|
177
|
-
|
178
|
-
async def read(self) -> str: # Always returns JSON string
|
179
|
-
"""Read the directory listing."""
|
180
|
-
try:
|
181
|
-
files = await anyio.to_thread.run_sync(self.list_files)
|
182
|
-
file_list = [str(f.relative_to(self.path)) for f in files if f.is_file()]
|
183
|
-
return json.dumps({"files": file_list}, indent=2)
|
184
|
-
except Exception as e:
|
185
|
-
raise ValueError(f"Error reading directory {self.path}: {e}")
|