afd 0.1.0__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.
- afd/__init__.py +134 -0
- afd/cli/__init__.py +31 -0
- afd/cli/main.py +447 -0
- afd/cli/output.py +287 -0
- afd/core/__init__.py +111 -0
- afd/core/commands.py +325 -0
- afd/core/errors.py +347 -0
- afd/core/handoff.py +434 -0
- afd/core/metadata.py +307 -0
- afd/core/pipeline.py +900 -0
- afd/core/result.py +263 -0
- afd/py.typed +1 -0
- afd/server/__init__.py +61 -0
- afd/server/bootstrap/__init__.py +29 -0
- afd/server/bootstrap/afd_docs.py +168 -0
- afd/server/bootstrap/afd_help.py +164 -0
- afd/server/bootstrap/afd_schema.py +176 -0
- afd/server/bootstrap/registry.py +52 -0
- afd/server/decorators.py +242 -0
- afd/server/factory.py +438 -0
- afd/testing/__init__.py +52 -0
- afd/testing/assertions.py +397 -0
- afd/testing/fixtures.py +257 -0
- afd/transports/__init__.py +39 -0
- afd/transports/base.py +149 -0
- afd/transports/fastmcp.py +182 -0
- afd/transports/mock.py +250 -0
- afd-0.1.0.dist-info/METADATA +200 -0
- afd-0.1.0.dist-info/RECORD +31 -0
- afd-0.1.0.dist-info/WHEEL +4 -0
- afd-0.1.0.dist-info/entry_points.txt +2 -0
afd/__init__.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Agent-First Development (AFD) toolkit for Python.
|
|
2
|
+
|
|
3
|
+
AFD is a software development methodology where AI agents are treated as
|
|
4
|
+
first-class users from day one.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
>>> from afd import CommandResult, success, error
|
|
8
|
+
>>> result = success(data={"id": "123"}, reasoning="Created successfully")
|
|
9
|
+
>>> result.success
|
|
10
|
+
True
|
|
11
|
+
|
|
12
|
+
Server Example:
|
|
13
|
+
>>> from afd.server import create_server
|
|
14
|
+
>>> from afd import success
|
|
15
|
+
>>> from pydantic import BaseModel
|
|
16
|
+
>>>
|
|
17
|
+
>>> server = create_server("my-app")
|
|
18
|
+
>>>
|
|
19
|
+
>>> class GreetInput(BaseModel):
|
|
20
|
+
... name: str
|
|
21
|
+
>>>
|
|
22
|
+
>>> @server.command(name="greet", description="Greet someone")
|
|
23
|
+
... async def greet(input: GreetInput):
|
|
24
|
+
... return success({"message": f"Hello, {input.name}!"})
|
|
25
|
+
|
|
26
|
+
Handoff Example:
|
|
27
|
+
>>> from afd import create_handoff, is_handoff, success
|
|
28
|
+
>>> from afd.server import define_command
|
|
29
|
+
>>>
|
|
30
|
+
>>> @define_command(
|
|
31
|
+
... name="chat.connect",
|
|
32
|
+
... description="Connect to a chat room",
|
|
33
|
+
... handoff=True,
|
|
34
|
+
... handoff_protocol="websocket",
|
|
35
|
+
... )
|
|
36
|
+
... async def connect_chat(input):
|
|
37
|
+
... return success(create_handoff(
|
|
38
|
+
... protocol="websocket",
|
|
39
|
+
... endpoint="wss://chat.example.com/room/123",
|
|
40
|
+
... token="auth-token",
|
|
41
|
+
... ))
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
from afd.core.result import (
|
|
45
|
+
CommandResult,
|
|
46
|
+
ResultMetadata,
|
|
47
|
+
success,
|
|
48
|
+
failure,
|
|
49
|
+
error,
|
|
50
|
+
is_success,
|
|
51
|
+
is_failure,
|
|
52
|
+
)
|
|
53
|
+
from afd.core.errors import (
|
|
54
|
+
CommandError,
|
|
55
|
+
ErrorCodes,
|
|
56
|
+
create_error,
|
|
57
|
+
validation_error,
|
|
58
|
+
not_found_error,
|
|
59
|
+
rate_limit_error,
|
|
60
|
+
timeout_error,
|
|
61
|
+
internal_error,
|
|
62
|
+
wrap_error,
|
|
63
|
+
is_command_error,
|
|
64
|
+
)
|
|
65
|
+
from afd.core.metadata import (
|
|
66
|
+
Source,
|
|
67
|
+
PlanStep,
|
|
68
|
+
PlanStepStatus,
|
|
69
|
+
Alternative,
|
|
70
|
+
Warning,
|
|
71
|
+
create_source,
|
|
72
|
+
create_step,
|
|
73
|
+
update_step_status,
|
|
74
|
+
create_warning,
|
|
75
|
+
)
|
|
76
|
+
from afd.core.handoff import (
|
|
77
|
+
HandoffProtocol,
|
|
78
|
+
HandoffCredentials,
|
|
79
|
+
HandoffMetadata,
|
|
80
|
+
HandoffResult,
|
|
81
|
+
ReconnectPolicy,
|
|
82
|
+
is_handoff,
|
|
83
|
+
is_handoff_protocol,
|
|
84
|
+
is_handoff_command,
|
|
85
|
+
get_handoff_protocol,
|
|
86
|
+
create_handoff,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
__version__ = "0.1.0"
|
|
90
|
+
|
|
91
|
+
__all__ = [
|
|
92
|
+
# Version
|
|
93
|
+
"__version__",
|
|
94
|
+
# Result types
|
|
95
|
+
"CommandResult",
|
|
96
|
+
"ResultMetadata",
|
|
97
|
+
"success",
|
|
98
|
+
"failure",
|
|
99
|
+
"error",
|
|
100
|
+
"is_success",
|
|
101
|
+
"is_failure",
|
|
102
|
+
# Error types
|
|
103
|
+
"CommandError",
|
|
104
|
+
"ErrorCodes",
|
|
105
|
+
"create_error",
|
|
106
|
+
"validation_error",
|
|
107
|
+
"not_found_error",
|
|
108
|
+
"rate_limit_error",
|
|
109
|
+
"timeout_error",
|
|
110
|
+
"internal_error",
|
|
111
|
+
"wrap_error",
|
|
112
|
+
"is_command_error",
|
|
113
|
+
# Metadata types
|
|
114
|
+
"Source",
|
|
115
|
+
"PlanStep",
|
|
116
|
+
"PlanStepStatus",
|
|
117
|
+
"Alternative",
|
|
118
|
+
"Warning",
|
|
119
|
+
"create_source",
|
|
120
|
+
"create_step",
|
|
121
|
+
"update_step_status",
|
|
122
|
+
"create_warning",
|
|
123
|
+
# Handoff types
|
|
124
|
+
"HandoffProtocol",
|
|
125
|
+
"HandoffCredentials",
|
|
126
|
+
"HandoffMetadata",
|
|
127
|
+
"HandoffResult",
|
|
128
|
+
"ReconnectPolicy",
|
|
129
|
+
"is_handoff",
|
|
130
|
+
"is_handoff_protocol",
|
|
131
|
+
"is_handoff_command",
|
|
132
|
+
"get_handoff_protocol",
|
|
133
|
+
"create_handoff",
|
|
134
|
+
]
|
afd/cli/__init__.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AFD CLI Module.
|
|
3
|
+
|
|
4
|
+
A command-line interface for interacting with AFD/MCP servers.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
$ afd connect my-server
|
|
8
|
+
$ afd tools
|
|
9
|
+
$ afd call user.create '{"name": "Alice"}'
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from afd.cli.main import cli, main
|
|
13
|
+
from afd.cli.output import (
|
|
14
|
+
print_error,
|
|
15
|
+
print_result,
|
|
16
|
+
print_success,
|
|
17
|
+
print_tools,
|
|
18
|
+
print_warning,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
# Main CLI
|
|
23
|
+
"cli",
|
|
24
|
+
"main",
|
|
25
|
+
# Output helpers
|
|
26
|
+
"print_result",
|
|
27
|
+
"print_error",
|
|
28
|
+
"print_success",
|
|
29
|
+
"print_warning",
|
|
30
|
+
"print_tools",
|
|
31
|
+
]
|
afd/cli/main.py
ADDED
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AFD CLI main module.
|
|
3
|
+
|
|
4
|
+
Provides the Click-based command-line interface.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import json
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
import click
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
|
|
16
|
+
from afd.cli.output import (
|
|
17
|
+
console,
|
|
18
|
+
error_console,
|
|
19
|
+
print_connecting,
|
|
20
|
+
print_disconnecting,
|
|
21
|
+
print_error,
|
|
22
|
+
print_info,
|
|
23
|
+
print_result,
|
|
24
|
+
print_success,
|
|
25
|
+
print_tool_detail,
|
|
26
|
+
print_tools,
|
|
27
|
+
)
|
|
28
|
+
from afd.transports import FastMCPTransport, MockTransport, Transport, TransportConfig
|
|
29
|
+
|
|
30
|
+
# State file for persistent connection info
|
|
31
|
+
STATE_FILE = Path.home() / ".afd" / "state.json"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _load_state() -> dict[str, Any]:
|
|
35
|
+
"""Load CLI state from disk."""
|
|
36
|
+
if STATE_FILE.exists():
|
|
37
|
+
try:
|
|
38
|
+
return json.loads(STATE_FILE.read_text())
|
|
39
|
+
except (json.JSONDecodeError, OSError):
|
|
40
|
+
pass
|
|
41
|
+
return {}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _save_state(state: dict[str, Any]) -> None:
|
|
45
|
+
"""Save CLI state to disk."""
|
|
46
|
+
STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
47
|
+
STATE_FILE.write_text(json.dumps(state, indent=2))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _get_transport(server: str | None = None) -> Transport:
|
|
51
|
+
"""
|
|
52
|
+
Get a transport instance.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
server: Server name or URL. Special values:
|
|
56
|
+
- "mock" or "mock:" prefix for MockTransport
|
|
57
|
+
- Otherwise uses FastMCPTransport
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Configured transport instance
|
|
61
|
+
"""
|
|
62
|
+
if server is None:
|
|
63
|
+
# Load from state
|
|
64
|
+
state = _load_state()
|
|
65
|
+
server = state.get("server")
|
|
66
|
+
if not server:
|
|
67
|
+
raise click.ClickException(
|
|
68
|
+
"No server connected. Use 'afd connect <server>' first."
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Check for mock transport
|
|
72
|
+
if server == "mock" or server.startswith("mock:"):
|
|
73
|
+
return MockTransport()
|
|
74
|
+
|
|
75
|
+
# Default to FastMCP transport
|
|
76
|
+
return FastMCPTransport(server_name=server)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@click.group()
|
|
80
|
+
@click.version_option(package_name="afd")
|
|
81
|
+
@click.option("--json", "json_output", is_flag=True, help="Output raw JSON")
|
|
82
|
+
@click.option("--quiet", "-q", is_flag=True, help="Suppress non-essential output")
|
|
83
|
+
@click.pass_context
|
|
84
|
+
def cli(ctx: click.Context, json_output: bool, quiet: bool) -> None:
|
|
85
|
+
"""
|
|
86
|
+
AFD - Agent-First Development CLI.
|
|
87
|
+
|
|
88
|
+
Interact with MCP servers using the AFD command pattern.
|
|
89
|
+
|
|
90
|
+
\b
|
|
91
|
+
Examples:
|
|
92
|
+
afd connect my-server
|
|
93
|
+
afd tools
|
|
94
|
+
afd call user.create '{"name": "Alice"}'
|
|
95
|
+
"""
|
|
96
|
+
ctx.ensure_object(dict)
|
|
97
|
+
ctx.obj["json_output"] = json_output
|
|
98
|
+
ctx.obj["quiet"] = quiet
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@cli.command()
|
|
102
|
+
@click.argument("server")
|
|
103
|
+
@click.option("--timeout", "-t", default=30.0, help="Connection timeout in seconds")
|
|
104
|
+
@click.pass_context
|
|
105
|
+
def connect(ctx: click.Context, server: str, timeout: float) -> None:
|
|
106
|
+
"""
|
|
107
|
+
Connect to an MCP server.
|
|
108
|
+
|
|
109
|
+
SERVER can be a server name, URL, or special value:
|
|
110
|
+
\b
|
|
111
|
+
- mock Use mock transport for testing
|
|
112
|
+
- <name> Connect to named server
|
|
113
|
+
|
|
114
|
+
\b
|
|
115
|
+
Examples:
|
|
116
|
+
afd connect my-server
|
|
117
|
+
afd connect mock
|
|
118
|
+
"""
|
|
119
|
+
quiet = ctx.obj.get("quiet", False)
|
|
120
|
+
|
|
121
|
+
if not quiet:
|
|
122
|
+
print_connecting(server)
|
|
123
|
+
|
|
124
|
+
async def _connect() -> None:
|
|
125
|
+
transport = _get_transport(server)
|
|
126
|
+
try:
|
|
127
|
+
await transport.connect()
|
|
128
|
+
|
|
129
|
+
# Save connection state
|
|
130
|
+
_save_state({"server": server})
|
|
131
|
+
|
|
132
|
+
if not quiet:
|
|
133
|
+
print_success(f"Connected to {server}")
|
|
134
|
+
|
|
135
|
+
# List available tools
|
|
136
|
+
tools = await transport.list_tools()
|
|
137
|
+
if tools:
|
|
138
|
+
print_info(f"Available tools: {len(tools)}")
|
|
139
|
+
finally:
|
|
140
|
+
await transport.disconnect()
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
asyncio.run(_connect())
|
|
144
|
+
except Exception as e:
|
|
145
|
+
print_error(str(e))
|
|
146
|
+
sys.exit(1)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@cli.command()
|
|
150
|
+
@click.pass_context
|
|
151
|
+
def disconnect(ctx: click.Context) -> None:
|
|
152
|
+
"""
|
|
153
|
+
Disconnect from the current server.
|
|
154
|
+
|
|
155
|
+
Clears the saved connection state.
|
|
156
|
+
"""
|
|
157
|
+
quiet = ctx.obj.get("quiet", False)
|
|
158
|
+
|
|
159
|
+
state = _load_state()
|
|
160
|
+
server = state.get("server")
|
|
161
|
+
|
|
162
|
+
if not server:
|
|
163
|
+
if not quiet:
|
|
164
|
+
print_info("No active connection")
|
|
165
|
+
return
|
|
166
|
+
|
|
167
|
+
if not quiet:
|
|
168
|
+
print_disconnecting()
|
|
169
|
+
|
|
170
|
+
# Clear state
|
|
171
|
+
_save_state({})
|
|
172
|
+
|
|
173
|
+
if not quiet:
|
|
174
|
+
print_success(f"Disconnected from {server}")
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@cli.command()
|
|
178
|
+
@click.option("--server", "-s", help="Server to query (uses saved connection if omitted)")
|
|
179
|
+
@click.option("--filter", "-f", "pattern", help="Filter tools by name pattern")
|
|
180
|
+
@click.option("--detail", "-d", "tool_name", help="Show detailed info for a specific tool")
|
|
181
|
+
@click.pass_context
|
|
182
|
+
def tools(ctx: click.Context, server: str | None, pattern: str | None, tool_name: str | None) -> None:
|
|
183
|
+
"""
|
|
184
|
+
List available tools from the connected server.
|
|
185
|
+
|
|
186
|
+
\b
|
|
187
|
+
Examples:
|
|
188
|
+
afd tools
|
|
189
|
+
afd tools --filter user
|
|
190
|
+
afd tools --detail user.create
|
|
191
|
+
"""
|
|
192
|
+
json_output = ctx.obj.get("json_output", False)
|
|
193
|
+
|
|
194
|
+
async def _list_tools() -> None:
|
|
195
|
+
transport = _get_transport(server)
|
|
196
|
+
try:
|
|
197
|
+
await transport.connect()
|
|
198
|
+
tool_list = await transport.list_tools()
|
|
199
|
+
|
|
200
|
+
# Filter if pattern provided
|
|
201
|
+
if pattern:
|
|
202
|
+
tool_list = [t for t in tool_list if pattern.lower() in t.name.lower()]
|
|
203
|
+
|
|
204
|
+
# Show detail for specific tool
|
|
205
|
+
if tool_name:
|
|
206
|
+
matching = [t for t in tool_list if t.name == tool_name]
|
|
207
|
+
if not matching:
|
|
208
|
+
raise click.ClickException(f"Tool '{tool_name}' not found")
|
|
209
|
+
|
|
210
|
+
if json_output:
|
|
211
|
+
import json as json_module
|
|
212
|
+
console.print(json_module.dumps({
|
|
213
|
+
"name": matching[0].name,
|
|
214
|
+
"description": matching[0].description,
|
|
215
|
+
"input_schema": matching[0].input_schema,
|
|
216
|
+
}, indent=2))
|
|
217
|
+
else:
|
|
218
|
+
print_tool_detail(matching[0])
|
|
219
|
+
return
|
|
220
|
+
|
|
221
|
+
# List all tools
|
|
222
|
+
if json_output:
|
|
223
|
+
import json as json_module
|
|
224
|
+
console.print(json_module.dumps([
|
|
225
|
+
{"name": t.name, "description": t.description}
|
|
226
|
+
for t in tool_list
|
|
227
|
+
], indent=2))
|
|
228
|
+
else:
|
|
229
|
+
print_tools(tool_list)
|
|
230
|
+
finally:
|
|
231
|
+
await transport.disconnect()
|
|
232
|
+
|
|
233
|
+
try:
|
|
234
|
+
asyncio.run(_list_tools())
|
|
235
|
+
except click.ClickException:
|
|
236
|
+
raise
|
|
237
|
+
except Exception as e:
|
|
238
|
+
print_error(str(e))
|
|
239
|
+
sys.exit(1)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
@cli.command()
|
|
243
|
+
@click.argument("tool")
|
|
244
|
+
@click.argument("args", required=False, default="{}")
|
|
245
|
+
@click.option("--server", "-s", help="Server to use (uses saved connection if omitted)")
|
|
246
|
+
@click.pass_context
|
|
247
|
+
def call(ctx: click.Context, tool: str, args: str, server: str | None) -> None:
|
|
248
|
+
"""
|
|
249
|
+
Call a tool on the connected server.
|
|
250
|
+
|
|
251
|
+
TOOL is the name of the tool to call.
|
|
252
|
+
ARGS is a JSON string of arguments (default: {}).
|
|
253
|
+
|
|
254
|
+
\b
|
|
255
|
+
Examples:
|
|
256
|
+
afd call user.list
|
|
257
|
+
afd call user.create '{"name": "Alice", "email": "alice@example.com"}'
|
|
258
|
+
afd call doc.get '{"id": "123"}'
|
|
259
|
+
"""
|
|
260
|
+
json_output = ctx.obj.get("json_output", False)
|
|
261
|
+
|
|
262
|
+
# Parse args
|
|
263
|
+
try:
|
|
264
|
+
parsed_args = json.loads(args)
|
|
265
|
+
except json.JSONDecodeError as e:
|
|
266
|
+
print_error(f"Invalid JSON arguments: {e}")
|
|
267
|
+
sys.exit(1)
|
|
268
|
+
|
|
269
|
+
async def _call_tool() -> None:
|
|
270
|
+
transport = _get_transport(server)
|
|
271
|
+
try:
|
|
272
|
+
await transport.connect()
|
|
273
|
+
result = await transport.call_tool(tool, parsed_args)
|
|
274
|
+
print_result(result, json_output=json_output)
|
|
275
|
+
finally:
|
|
276
|
+
await transport.disconnect()
|
|
277
|
+
|
|
278
|
+
try:
|
|
279
|
+
asyncio.run(_call_tool())
|
|
280
|
+
except Exception as e:
|
|
281
|
+
print_error(str(e))
|
|
282
|
+
sys.exit(1)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
@cli.command()
|
|
286
|
+
@click.pass_context
|
|
287
|
+
def status(ctx: click.Context) -> None:
|
|
288
|
+
"""
|
|
289
|
+
Show current connection status.
|
|
290
|
+
|
|
291
|
+
Displays the currently connected server and connection state.
|
|
292
|
+
"""
|
|
293
|
+
json_output = ctx.obj.get("json_output", False)
|
|
294
|
+
|
|
295
|
+
state = _load_state()
|
|
296
|
+
server = state.get("server")
|
|
297
|
+
|
|
298
|
+
if json_output:
|
|
299
|
+
console.print(json.dumps({
|
|
300
|
+
"connected": server is not None,
|
|
301
|
+
"server": server,
|
|
302
|
+
}, indent=2))
|
|
303
|
+
return
|
|
304
|
+
|
|
305
|
+
if server:
|
|
306
|
+
print_success(f"Connected to: {server}")
|
|
307
|
+
else:
|
|
308
|
+
print_info("Not connected to any server")
|
|
309
|
+
print_info("Use 'afd connect <server>' to connect")
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
@cli.command()
|
|
313
|
+
@click.option("--server", "-s", help="Server to validate")
|
|
314
|
+
@click.pass_context
|
|
315
|
+
def validate(ctx: click.Context, server: str | None) -> None:
|
|
316
|
+
"""
|
|
317
|
+
Validate server connection and tools.
|
|
318
|
+
|
|
319
|
+
Connects to the server, lists tools, and verifies each can be inspected.
|
|
320
|
+
"""
|
|
321
|
+
quiet = ctx.obj.get("quiet", False)
|
|
322
|
+
json_output = ctx.obj.get("json_output", False)
|
|
323
|
+
|
|
324
|
+
async def _validate() -> None:
|
|
325
|
+
transport = _get_transport(server)
|
|
326
|
+
results: dict[str, Any] = {
|
|
327
|
+
"connection": False,
|
|
328
|
+
"tools_listed": False,
|
|
329
|
+
"tool_count": 0,
|
|
330
|
+
"errors": [],
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
try:
|
|
334
|
+
await transport.connect()
|
|
335
|
+
results["connection"] = True
|
|
336
|
+
|
|
337
|
+
tools = await transport.list_tools()
|
|
338
|
+
results["tools_listed"] = True
|
|
339
|
+
results["tool_count"] = len(tools)
|
|
340
|
+
|
|
341
|
+
if json_output:
|
|
342
|
+
console.print(json.dumps(results, indent=2))
|
|
343
|
+
else:
|
|
344
|
+
print_success("Connection: OK")
|
|
345
|
+
print_success(f"Tools listed: {len(tools)}")
|
|
346
|
+
print_success("Validation passed!")
|
|
347
|
+
|
|
348
|
+
except Exception as e:
|
|
349
|
+
results["errors"].append(str(e))
|
|
350
|
+
if json_output:
|
|
351
|
+
console.print(json.dumps(results, indent=2))
|
|
352
|
+
else:
|
|
353
|
+
print_error(f"Validation failed: {e}")
|
|
354
|
+
sys.exit(1)
|
|
355
|
+
finally:
|
|
356
|
+
await transport.disconnect()
|
|
357
|
+
|
|
358
|
+
try:
|
|
359
|
+
asyncio.run(_validate())
|
|
360
|
+
except Exception as e:
|
|
361
|
+
print_error(str(e))
|
|
362
|
+
sys.exit(1)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
@cli.command()
|
|
366
|
+
@click.pass_context
|
|
367
|
+
def shell(ctx: click.Context) -> None:
|
|
368
|
+
"""
|
|
369
|
+
Start an interactive shell.
|
|
370
|
+
|
|
371
|
+
Provides a REPL for calling tools interactively.
|
|
372
|
+
Use 'exit' or Ctrl+D to quit.
|
|
373
|
+
"""
|
|
374
|
+
from rich.prompt import Prompt
|
|
375
|
+
|
|
376
|
+
state = _load_state()
|
|
377
|
+
server = state.get("server")
|
|
378
|
+
|
|
379
|
+
if not server:
|
|
380
|
+
print_error("No server connected. Use 'afd connect <server>' first.")
|
|
381
|
+
sys.exit(1)
|
|
382
|
+
|
|
383
|
+
console.print(f"[bold]AFD Shell[/bold] - Connected to [cyan]{server}[/cyan]")
|
|
384
|
+
console.print("[dim]Type 'help' for commands, 'exit' to quit[/dim]\n")
|
|
385
|
+
|
|
386
|
+
async def _shell() -> None:
|
|
387
|
+
transport = _get_transport(server)
|
|
388
|
+
await transport.connect()
|
|
389
|
+
|
|
390
|
+
try:
|
|
391
|
+
while True:
|
|
392
|
+
try:
|
|
393
|
+
line = Prompt.ask("[cyan]afd[/cyan]")
|
|
394
|
+
except EOFError:
|
|
395
|
+
break
|
|
396
|
+
|
|
397
|
+
line = line.strip()
|
|
398
|
+
if not line:
|
|
399
|
+
continue
|
|
400
|
+
|
|
401
|
+
if line in ("exit", "quit"):
|
|
402
|
+
break
|
|
403
|
+
|
|
404
|
+
if line == "help":
|
|
405
|
+
console.print("""
|
|
406
|
+
[bold]Commands:[/bold]
|
|
407
|
+
tools List available tools
|
|
408
|
+
call <tool> [args] Call a tool with optional JSON args
|
|
409
|
+
help Show this help
|
|
410
|
+
exit Exit the shell
|
|
411
|
+
""")
|
|
412
|
+
continue
|
|
413
|
+
|
|
414
|
+
if line == "tools":
|
|
415
|
+
tool_list = await transport.list_tools()
|
|
416
|
+
print_tools(tool_list)
|
|
417
|
+
continue
|
|
418
|
+
|
|
419
|
+
if line.startswith("call "):
|
|
420
|
+
parts = line[5:].strip().split(maxsplit=1)
|
|
421
|
+
tool_name = parts[0]
|
|
422
|
+
tool_args = json.loads(parts[1]) if len(parts) > 1 else {}
|
|
423
|
+
|
|
424
|
+
result = await transport.call_tool(tool_name, tool_args)
|
|
425
|
+
print_result(result)
|
|
426
|
+
continue
|
|
427
|
+
|
|
428
|
+
console.print(f"[yellow]Unknown command: {line}[/yellow]")
|
|
429
|
+
|
|
430
|
+
finally:
|
|
431
|
+
await transport.disconnect()
|
|
432
|
+
|
|
433
|
+
try:
|
|
434
|
+
asyncio.run(_shell())
|
|
435
|
+
except KeyboardInterrupt:
|
|
436
|
+
pass
|
|
437
|
+
|
|
438
|
+
console.print("\n[dim]Goodbye![/dim]")
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def main() -> None:
|
|
442
|
+
"""Entry point for the CLI."""
|
|
443
|
+
cli()
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
if __name__ == "__main__":
|
|
447
|
+
main()
|