agent-brain-cli 10.2.0__tar.gz → 10.3.0__tar.gz
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.
- {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/PKG-INFO +4 -3
- {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/__init__.py +1 -1
- agent_brain_cli-10.3.0/agent_brain_cli/__main__.py +14 -0
- {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/cli.py +35 -3
- {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/client/__init__.py +2 -0
- {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/client/api_client.py +20 -3
- agent_brain_cli-10.3.0/agent_brain_cli/client/protocol.py +173 -0
- agent_brain_cli-10.3.0/agent_brain_cli/client/transport.py +255 -0
- {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/commands/__init__.py +6 -0
- {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/commands/cache.py +3 -3
- {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/commands/folders.py +4 -4
- {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/commands/index.py +2 -2
- {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/commands/init.py +31 -2
- {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/commands/inject.py +2 -2
- {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/commands/jobs.py +8 -7
- agent_brain_cli-10.3.0/agent_brain_cli/commands/mcp.py +453 -0
- agent_brain_cli-10.3.0/agent_brain_cli/commands/prompt.py +191 -0
- {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/commands/query.py +2 -2
- {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/commands/reset.py +2 -2
- agent_brain_cli-10.3.0/agent_brain_cli/commands/resources.py +267 -0
- {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/commands/start.py +35 -0
- {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/commands/status.py +3 -3
- {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/config.py +153 -0
- agent_brain_cli-10.3.0/agent_brain_cli/mcp_runtime.py +273 -0
- {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/pyproject.toml +15 -3
- agent_brain_cli-10.2.0/agent_brain_cli/client/transport.py +0 -54
- {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/README.md +0 -0
- {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/commands/config.py +0 -0
- {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/commands/doctor.py +0 -0
- {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/commands/install_agent.py +0 -0
- {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/commands/list_cmd.py +0 -0
- {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/commands/stop.py +0 -0
- {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/commands/types.py +0 -0
- {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/commands/uninstall.py +0 -0
- {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/config_migrate.py +0 -0
- {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/config_schema.py +0 -0
- {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/diagnostics.py +0 -0
- {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/migration.py +0 -0
- {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/runtime/__init__.py +0 -0
- {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/runtime/claude_converter.py +0 -0
- {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/runtime/codex_converter.py +0 -0
- {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/runtime/converter_base.py +0 -0
- {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/runtime/gemini_converter.py +0 -0
- {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/runtime/opencode_converter.py +0 -0
- {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/runtime/parser.py +0 -0
- {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/runtime/skill_runtime_converter.py +0 -0
- {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/runtime/tool_maps.py +0 -0
- {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/runtime/types.py +0 -0
- {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/xdg_paths.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: agent-brain-cli
|
|
3
|
-
Version: 10.
|
|
3
|
+
Version: 10.3.0
|
|
4
4
|
Summary: Agent Brain CLI - Command-line interface for managing AI agent memory and knowledge retrieval
|
|
5
5
|
Home-page: https://github.com/SpillwaveSolutions/agent-brain
|
|
6
6
|
License: MIT
|
|
@@ -15,10 +15,11 @@ Classifier: Programming Language :: Python :: 3
|
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.10
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.11
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
-
Requires-Dist: agent-brain-rag (>=10.
|
|
19
|
-
Requires-Dist: agent-brain-uds (>=10.
|
|
18
|
+
Requires-Dist: agent-brain-rag (>=10.3.0,<11.0.0)
|
|
19
|
+
Requires-Dist: agent-brain-uds (>=10.3.0,<11.0.0)
|
|
20
20
|
Requires-Dist: click (>=8.1.0,<9.0.0)
|
|
21
21
|
Requires-Dist: httpx (>=0.28.0,<0.29.0)
|
|
22
|
+
Requires-Dist: psutil (>=6.0,<7.0)
|
|
22
23
|
Requires-Dist: pydantic (>=2.10.0,<3.0.0)
|
|
23
24
|
Requires-Dist: pyyaml (>=6.0.0,<7.0.0)
|
|
24
25
|
Requires-Dist: rich (>=13.9.0,<14.0.0)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Entry point for ``python -m agent_brain_cli``.
|
|
2
|
+
|
|
3
|
+
Defers to the Click ``cli`` group. Added in Phase 57 Plan 02 so the
|
|
4
|
+
byte-equivalence contract test can invoke the CLI without depending
|
|
5
|
+
on the ``agent-brain`` console-script being installed on ``PATH``
|
|
6
|
+
in every test environment.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from agent_brain_cli.cli import cli
|
|
12
|
+
|
|
13
|
+
if __name__ == "__main__":
|
|
14
|
+
cli()
|
|
@@ -18,8 +18,11 @@ from .commands import (
|
|
|
18
18
|
install_agent_command,
|
|
19
19
|
jobs_command,
|
|
20
20
|
list_command,
|
|
21
|
+
mcp_group,
|
|
22
|
+
prompt_command,
|
|
21
23
|
query_command,
|
|
22
24
|
reset_command,
|
|
25
|
+
resources_group,
|
|
23
26
|
start_command,
|
|
24
27
|
status_command,
|
|
25
28
|
stop_command,
|
|
@@ -33,11 +36,11 @@ from .commands import (
|
|
|
33
36
|
@click.option(
|
|
34
37
|
"--transport",
|
|
35
38
|
"transport",
|
|
36
|
-
type=click.Choice(["auto", "http", "uds"], case_sensitive=False),
|
|
39
|
+
type=click.Choice(["auto", "http", "uds", "mcp"], case_sensitive=False),
|
|
37
40
|
default=None,
|
|
38
41
|
help=(
|
|
39
|
-
"Transport
|
|
40
|
-
"
|
|
42
|
+
"Transport: auto (UDS if available, HTTP otherwise), http, uds, "
|
|
43
|
+
"or mcp (CLI talks to agent-brain-mcp). Honors AGENT_BRAIN_TRANSPORT env."
|
|
41
44
|
),
|
|
42
45
|
)
|
|
43
46
|
@click.option(
|
|
@@ -51,6 +54,28 @@ from .commands import (
|
|
|
51
54
|
default=None,
|
|
52
55
|
help="Override server base URL (only used with --transport=http|auto).",
|
|
53
56
|
)
|
|
57
|
+
@click.option(
|
|
58
|
+
"--mcp-transport",
|
|
59
|
+
"mcp_transport",
|
|
60
|
+
type=click.Choice(["stdio", "http"], case_sensitive=False),
|
|
61
|
+
default=None,
|
|
62
|
+
help=(
|
|
63
|
+
"MCP listen transport for --transport mcp: stdio (default) "
|
|
64
|
+
"or http. Honors AGENT_BRAIN_MCP_TRANSPORT env. Ignored "
|
|
65
|
+
"when --transport != mcp."
|
|
66
|
+
),
|
|
67
|
+
)
|
|
68
|
+
@click.option(
|
|
69
|
+
"--mcp-url",
|
|
70
|
+
"mcp_url",
|
|
71
|
+
default=None,
|
|
72
|
+
help=(
|
|
73
|
+
"MCP HTTP listener URL for --transport mcp --mcp-transport "
|
|
74
|
+
"http (e.g. http://127.0.0.1:9999/mcp). Honors "
|
|
75
|
+
"AGENT_BRAIN_MCP_URL env. mcp.runtime.json discovery "
|
|
76
|
+
"lands in Phase 58."
|
|
77
|
+
),
|
|
78
|
+
)
|
|
54
79
|
@click.option(
|
|
55
80
|
"--debug-transport",
|
|
56
81
|
is_flag=True,
|
|
@@ -63,6 +88,8 @@ def cli(
|
|
|
63
88
|
transport: str | None,
|
|
64
89
|
socket_path: str | None,
|
|
65
90
|
base_url: str | None,
|
|
91
|
+
mcp_transport: str | None,
|
|
92
|
+
mcp_url: str | None,
|
|
66
93
|
debug_transport: bool,
|
|
67
94
|
) -> None:
|
|
68
95
|
"""Agent Brain CLI - Manage and query the Agent Brain RAG server.
|
|
@@ -124,6 +151,8 @@ def cli(
|
|
|
124
151
|
ctx.obj["base_url_override"] = base_url
|
|
125
152
|
ctx.obj["socket_path_override"] = socket_path
|
|
126
153
|
ctx.obj["debug_transport"] = debug_transport
|
|
154
|
+
ctx.obj["mcp_transport_hint"] = mcp_transport
|
|
155
|
+
ctx.obj["mcp_url_override"] = mcp_url
|
|
127
156
|
|
|
128
157
|
|
|
129
158
|
# Register project management commands
|
|
@@ -131,6 +160,9 @@ cli.add_command(init_command, name="init")
|
|
|
131
160
|
cli.add_command(start_command, name="start")
|
|
132
161
|
cli.add_command(stop_command, name="stop")
|
|
133
162
|
cli.add_command(list_command, name="list")
|
|
163
|
+
cli.add_command(mcp_group, name="mcp")
|
|
164
|
+
cli.add_command(prompt_command, name="prompt")
|
|
165
|
+
cli.add_command(resources_group, name="resources")
|
|
134
166
|
|
|
135
167
|
# Register server interaction commands
|
|
136
168
|
cli.add_command(status_command, name="status")
|
|
@@ -159,6 +159,7 @@ class DocServeClient:
|
|
|
159
159
|
self,
|
|
160
160
|
base_url: str = "http://127.0.0.1:8000",
|
|
161
161
|
timeout: float = 30.0,
|
|
162
|
+
api_key: str | None = None,
|
|
162
163
|
):
|
|
163
164
|
"""
|
|
164
165
|
Initialize the client.
|
|
@@ -166,23 +167,37 @@ class DocServeClient:
|
|
|
166
167
|
Args:
|
|
167
168
|
base_url: Server base URL.
|
|
168
169
|
timeout: Request timeout in seconds.
|
|
170
|
+
api_key: Optional bearer token (Issues #179, #199). When supplied,
|
|
171
|
+
every outbound request carries an ``Authorization: Bearer``
|
|
172
|
+
header per RFC 6750. ``None`` means no auth header (server
|
|
173
|
+
must be in ``INSECURE_NO_AUTH=true`` mode to accept the
|
|
174
|
+
request).
|
|
169
175
|
"""
|
|
170
176
|
self.base_url = base_url.rstrip("/")
|
|
171
177
|
self.timeout = timeout
|
|
172
|
-
|
|
178
|
+
headers = {"Authorization": f"Bearer {api_key}"} if api_key else None
|
|
179
|
+
self._client = httpx.Client(timeout=timeout, headers=headers)
|
|
173
180
|
|
|
174
181
|
@classmethod
|
|
175
|
-
def from_httpx(
|
|
182
|
+
def from_httpx(
|
|
183
|
+
cls,
|
|
184
|
+
client: httpx.Client,
|
|
185
|
+
api_key: str | None = None,
|
|
186
|
+
) -> "DocServeClient":
|
|
176
187
|
"""Build a DocServeClient that uses a pre-constructed httpx.Client.
|
|
177
188
|
|
|
178
189
|
Used by the transport selector to inject a UDS-backed client
|
|
179
|
-
(see ``agent_brain_cli.client.transport.
|
|
190
|
+
(see ``agent_brain_cli.client.transport.open_backend``). The
|
|
180
191
|
inner client's ``base_url`` is preserved; this wrapper sends
|
|
181
192
|
relative paths only.
|
|
182
193
|
|
|
183
194
|
Args:
|
|
184
195
|
client: An already-configured ``httpx.Client``. The wrapper
|
|
185
196
|
takes ownership and will close it on ``__exit__``.
|
|
197
|
+
api_key: Optional bearer token to merge into the client's
|
|
198
|
+
default headers as ``Authorization: Bearer <token>``
|
|
199
|
+
(Issues #179, #199). Caller may also have set the
|
|
200
|
+
header on ``client`` directly; both paths work.
|
|
186
201
|
|
|
187
202
|
Returns:
|
|
188
203
|
A DocServeClient backed by ``client``.
|
|
@@ -191,6 +206,8 @@ class DocServeClient:
|
|
|
191
206
|
instance.base_url = "" # inner client carries the real base_url
|
|
192
207
|
timeout = client.timeout
|
|
193
208
|
instance.timeout = timeout.read or 30.0
|
|
209
|
+
if api_key:
|
|
210
|
+
client.headers["Authorization"] = f"Bearer {api_key}"
|
|
194
211
|
instance._client = client
|
|
195
212
|
return instance
|
|
196
213
|
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""BackendClient — the structural Protocol every CLI backend satisfies.
|
|
2
|
+
|
|
3
|
+
The shape lives in code so:
|
|
4
|
+
|
|
5
|
+
1. mypy strict verifies callers can swap DocServeClient (HTTP/UDS) for
|
|
6
|
+
McpStdioBackend / McpHttpBackend (v3, Phase 56 Plan 03) without
|
|
7
|
+
editing a single Click command in :mod:`agent_brain_cli.commands`.
|
|
8
|
+
2. Tests can assert ``isinstance(backend, BackendClient)`` at runtime
|
|
9
|
+
via :pep:`544`'s ``@runtime_checkable`` decorator.
|
|
10
|
+
|
|
11
|
+
Surface mirrors :class:`agent_brain_cli.client.api_client.DocServeClient`
|
|
12
|
+
verbatim — 12 public methods + 3 context-manager methods. The decision
|
|
13
|
+
to keep the Protocol sync-only (no async variant) is committed in the
|
|
14
|
+
v3 design doc at ``docs/plans/2026-06-05-mcp-v3-cli-via-mcp.md`` §3.2.
|
|
15
|
+
A future ``AsyncBackendClient`` is deferred to v10.4+ per the design
|
|
16
|
+
doc's §6.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from types import TracebackType
|
|
22
|
+
from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
# TYPE_CHECKING import avoids a runtime cycle once Phase 56 Plan 03
|
|
26
|
+
# backends start importing the Protocol from this module.
|
|
27
|
+
from .api_client import (
|
|
28
|
+
FolderInfo,
|
|
29
|
+
HealthStatus,
|
|
30
|
+
IndexingStatus,
|
|
31
|
+
IndexResponse,
|
|
32
|
+
QueryResponse,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@runtime_checkable
|
|
37
|
+
class BackendClient(Protocol):
|
|
38
|
+
"""Structural contract for CLI-side backends.
|
|
39
|
+
|
|
40
|
+
DocServeClient satisfies this Protocol without inheritance.
|
|
41
|
+
McpStdioBackend + McpHttpBackend (Plan 56-03) will declare it
|
|
42
|
+
explicitly via ``class McpStdioBackend(BackendClient): ...`` so
|
|
43
|
+
mypy emits a clean error if a method ever drifts.
|
|
44
|
+
|
|
45
|
+
All methods are synchronous. The v3 backends wrap async MCP SDK
|
|
46
|
+
calls via ``asyncio.run(...)`` or a persistent ``_loop`` attribute
|
|
47
|
+
— see the design doc §3.2. From the caller's perspective the
|
|
48
|
+
facade is identical to DocServeClient's.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
# --- Context manager surface ---------------------------------------
|
|
52
|
+
|
|
53
|
+
def __enter__(self) -> BackendClient: ...
|
|
54
|
+
|
|
55
|
+
def __exit__(
|
|
56
|
+
self,
|
|
57
|
+
exc_type: type[BaseException] | None,
|
|
58
|
+
exc_val: BaseException | None,
|
|
59
|
+
exc_tb: TracebackType | None,
|
|
60
|
+
) -> None: ...
|
|
61
|
+
|
|
62
|
+
def close(self) -> None: ...
|
|
63
|
+
|
|
64
|
+
# --- Health + status ----------------------------------------------
|
|
65
|
+
|
|
66
|
+
def health(self) -> HealthStatus: ...
|
|
67
|
+
|
|
68
|
+
def status(self) -> IndexingStatus: ...
|
|
69
|
+
|
|
70
|
+
# --- Query (maps to MCP `search_documents` tool for v3 backends) ---
|
|
71
|
+
|
|
72
|
+
def query(
|
|
73
|
+
self,
|
|
74
|
+
query_text: str,
|
|
75
|
+
top_k: int = 5,
|
|
76
|
+
similarity_threshold: float = 0.7,
|
|
77
|
+
mode: str = "hybrid",
|
|
78
|
+
alpha: float = 0.5,
|
|
79
|
+
source_types: list[str] | None = None,
|
|
80
|
+
languages: list[str] | None = None,
|
|
81
|
+
file_paths: list[str] | None = None,
|
|
82
|
+
explain: bool = False,
|
|
83
|
+
) -> QueryResponse: ...
|
|
84
|
+
|
|
85
|
+
# --- Indexing -----------------------------------------------------
|
|
86
|
+
|
|
87
|
+
def index(
|
|
88
|
+
self,
|
|
89
|
+
folder_path: str,
|
|
90
|
+
chunk_size: int = 512,
|
|
91
|
+
chunk_overlap: int = 50,
|
|
92
|
+
recursive: bool = True,
|
|
93
|
+
include_code: bool = False,
|
|
94
|
+
supported_languages: list[str] | None = None,
|
|
95
|
+
code_chunk_strategy: str = "ast_aware",
|
|
96
|
+
include_patterns: list[str] | None = None,
|
|
97
|
+
exclude_patterns: list[str] | None = None,
|
|
98
|
+
include_types: list[str] | None = None,
|
|
99
|
+
generate_summaries: bool = False,
|
|
100
|
+
force: bool = False,
|
|
101
|
+
injector_script: str | None = None,
|
|
102
|
+
folder_metadata_file: str | None = None,
|
|
103
|
+
dry_run: bool = False,
|
|
104
|
+
watch_mode: str | None = None,
|
|
105
|
+
watch_debounce_seconds: int | None = None,
|
|
106
|
+
) -> IndexResponse: ...
|
|
107
|
+
|
|
108
|
+
# --- Folder management --------------------------------------------
|
|
109
|
+
|
|
110
|
+
def list_folders(self) -> list[FolderInfo]: ...
|
|
111
|
+
|
|
112
|
+
def delete_folder(self, folder_path: str) -> dict[str, Any]: ...
|
|
113
|
+
|
|
114
|
+
def reset(self) -> IndexResponse: ...
|
|
115
|
+
|
|
116
|
+
# --- Job queue ----------------------------------------------------
|
|
117
|
+
|
|
118
|
+
def list_jobs(self, limit: int = 20) -> list[dict[str, Any]]: ...
|
|
119
|
+
|
|
120
|
+
def get_job(self, job_id: str) -> dict[str, Any]: ...
|
|
121
|
+
|
|
122
|
+
def cancel_job(self, job_id: str) -> dict[str, Any]: ...
|
|
123
|
+
|
|
124
|
+
# --- Embedding cache ----------------------------------------------
|
|
125
|
+
|
|
126
|
+
def cache_status(self) -> dict[str, Any]: ...
|
|
127
|
+
|
|
128
|
+
def clear_cache(self) -> dict[str, Any]: ...
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@runtime_checkable
|
|
132
|
+
class McpBackend(Protocol):
|
|
133
|
+
"""Structural contract for MCP-only CLI backends (Phase 59).
|
|
134
|
+
|
|
135
|
+
Exposes the ``prompts/get`` + ``prompts/list`` +
|
|
136
|
+
``resources/list`` + ``resources/templates/list`` +
|
|
137
|
+
``resources/read`` MCP surface that the v3 ``agent-brain prompt``
|
|
138
|
+
and ``agent-brain resources *`` commands speak. Intentionally
|
|
139
|
+
SEPARATE from :class:`BackendClient` — DocServeClient (UDS/HTTP)
|
|
140
|
+
does NOT satisfy this Protocol because those transports do not
|
|
141
|
+
speak MCP prompts/resources. The negative-case pinning test in
|
|
142
|
+
``agent-brain-mcp/tests/test_mcp_backend_protocol_skeleton.py``
|
|
143
|
+
(``test_doc_serve_client_does_not_satisfy_mcp_backend``) makes the
|
|
144
|
+
architectural boundary explicit and lasting.
|
|
145
|
+
|
|
146
|
+
All methods are synchronous (sync-facade Pattern A confirmed by
|
|
147
|
+
Plan 57-02; see design doc §3.2). The v3 MCP backends wrap async
|
|
148
|
+
MCP SDK calls via ``asyncio.run(...)`` per call — from the
|
|
149
|
+
caller's perspective the facade is identical to
|
|
150
|
+
:class:`BackendClient`'s.
|
|
151
|
+
|
|
152
|
+
Return shapes are deliberately ``dict[str, Any]`` /
|
|
153
|
+
``list[dict[str, Any]]`` (NOT typed dataclasses) — the MCP wire
|
|
154
|
+
payloads for ``prompts/get`` and ``resources/read`` differ enough
|
|
155
|
+
that the Phase 59 Plan 02 / Plan 03 command layer handles per-
|
|
156
|
+
method coercion (mirrors the existing
|
|
157
|
+
:func:`api_client._unwrap_payload` shape). Phase 60+ may revisit.
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
def get_prompt(
|
|
161
|
+
self, name: str, arguments: dict[str, str] | None = None
|
|
162
|
+
) -> dict[str, Any]: ...
|
|
163
|
+
|
|
164
|
+
def list_prompts(self) -> list[dict[str, Any]]: ...
|
|
165
|
+
|
|
166
|
+
def list_resources(self) -> list[dict[str, Any]]: ...
|
|
167
|
+
|
|
168
|
+
def list_resource_templates(self) -> list[dict[str, Any]]: ...
|
|
169
|
+
|
|
170
|
+
def read_resource(self, uri: str) -> dict[str, Any]: ...
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
__all__: list[str] = ["BackendClient", "McpBackend"]
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"""Transport selector — builds a BackendClient over HTTP, UDS, or MCP.
|
|
2
|
+
|
|
3
|
+
Renamed in Phase 57 (Plan 57-01) from ``open_client`` to
|
|
4
|
+
``open_backend``. Return type widened from ``DocServeClient`` to
|
|
5
|
+
:class:`agent_brain_cli.client.protocol.BackendClient` so the
|
|
6
|
+
transport-dispatching factory can return any of:
|
|
7
|
+
|
|
8
|
+
- ``DocServeClient`` (HTTP or UDS — the existing v1/v2 path)
|
|
9
|
+
- ``McpStdioBackend`` (v3 stdio MCP — agent-brain-mcp subprocess)
|
|
10
|
+
- ``McpHttpBackend`` (v3 streamable HTTP MCP — loopback listener)
|
|
11
|
+
|
|
12
|
+
The MCP backends are imported lazily inside the ``transport == "mcp"``
|
|
13
|
+
branch so HTTP/UDS-only invocations do NOT pay the MCP SDK import
|
|
14
|
+
cost AND so the CLI runs cleanly without ``agent-brain-mcp``
|
|
15
|
+
installed when the user never asks for it (CONTEXT decision: soft
|
|
16
|
+
dep on agent-brain-mcp).
|
|
17
|
+
|
|
18
|
+
Three §3.5 design-doc misuse cases surface as ``click.UsageError``
|
|
19
|
+
(exit code 2 — v10.2 HTTP-03 no-silent-fallback contract):
|
|
20
|
+
|
|
21
|
+
1. ``--transport mcp`` + ``agent-brain-mcp`` package not installed
|
|
22
|
+
→ "install agent-brain-mcp to use --transport mcp"
|
|
23
|
+
2. ``--mcp-transport http`` without ``--mcp-url`` (and no
|
|
24
|
+
``AGENT_BRAIN_MCP_URL`` env) AND no ``mcp.runtime.json``
|
|
25
|
+
discovery file → raised by ``resolve_mcp_transport`` with the
|
|
26
|
+
verbatim v3 §3.5 wording (Phase 58 CLI-MCP-08)
|
|
27
|
+
3. ``--mcp-transport stdio`` + ``agent-brain-mcp`` not reachable
|
|
28
|
+
on ``PATH`` (``shutil.which`` returns ``None``)
|
|
29
|
+
→ "agent-brain-mcp not found on PATH; install agent-brain-mcp
|
|
30
|
+
into the same Python environment"
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import shutil
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
from typing import cast
|
|
38
|
+
|
|
39
|
+
import click
|
|
40
|
+
|
|
41
|
+
from ..config import resolve_api_key, resolve_mcp_transport, resolve_transport
|
|
42
|
+
from .api_client import DocServeClient
|
|
43
|
+
from .protocol import BackendClient, McpBackend
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _resolve_state_dir_for_discovery() -> Path | None:
|
|
47
|
+
"""Resolve state_dir for mcp.runtime.json discovery (Phase 58 CLI-MCP-08).
|
|
48
|
+
|
|
49
|
+
Mirrors the chain used by ``agent-brain mcp start``. Returns None on
|
|
50
|
+
any failure — callers treat that as "no discovery possible" and
|
|
51
|
+
fall through to the explicit-url-or-error path inside
|
|
52
|
+
``resolve_mcp_transport``.
|
|
53
|
+
"""
|
|
54
|
+
try:
|
|
55
|
+
from agent_brain_cli.config import resolve_project_root
|
|
56
|
+
from agent_brain_cli.migration import resolve_state_dir_with_fallback
|
|
57
|
+
from agent_brain_cli.xdg_paths import migrate_legacy_paths
|
|
58
|
+
|
|
59
|
+
migrate_legacy_paths()
|
|
60
|
+
project_root = resolve_project_root()
|
|
61
|
+
return resolve_state_dir_with_fallback(project_root)
|
|
62
|
+
except Exception:
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def open_backend(ctx: click.Context, *, timeout: float = 30.0) -> BackendClient:
|
|
67
|
+
"""Construct a ``BackendClient`` over the transport selected by ``ctx``.
|
|
68
|
+
|
|
69
|
+
Reads from ``ctx.obj`` (set by the top-level Click group in
|
|
70
|
+
``cli.py``):
|
|
71
|
+
|
|
72
|
+
- ``transport_hint`` (``"auto"`` / ``"http"`` / ``"uds"`` /
|
|
73
|
+
``"mcp"`` / ``None``)
|
|
74
|
+
- ``base_url_override``, ``socket_path_override``
|
|
75
|
+
- ``mcp_transport_hint`` (``"stdio"`` / ``"http"`` / ``None``)
|
|
76
|
+
- ``mcp_url_override`` (URL string or ``None``)
|
|
77
|
+
- ``debug_transport`` (bool)
|
|
78
|
+
|
|
79
|
+
Dispatch (in order):
|
|
80
|
+
|
|
81
|
+
1. ``transport == "mcp"`` and ``mcp_transport == "stdio"`` →
|
|
82
|
+
:class:`McpStdioBackend(command="agent-brain-mcp")` (precheck:
|
|
83
|
+
``shutil.which("agent-brain-mcp")`` must be non-None or
|
|
84
|
+
raises with §3.5 case 3 wording)
|
|
85
|
+
2. ``transport == "mcp"`` and ``mcp_transport == "http"`` →
|
|
86
|
+
:class:`McpHttpBackend(url=resolved_mcp_url, timeout=timeout)`
|
|
87
|
+
3. ``transport == "http"`` → :class:`DocServeClient`
|
|
88
|
+
4. ``transport == "uds"`` → :class:`DocServeClient.from_httpx`
|
|
89
|
+
|
|
90
|
+
Raises:
|
|
91
|
+
click.UsageError: With exit code 2 when any of the three
|
|
92
|
+
§3.5 design-doc misuse cases hit. NO silent fallback to
|
|
93
|
+
UDS or HTTP — the operator's choice is honored or the
|
|
94
|
+
CLI exits non-zero (v10.2 HTTP-03 contract).
|
|
95
|
+
"""
|
|
96
|
+
obj = ctx.obj or {}
|
|
97
|
+
transport_hint = obj.get("transport_hint")
|
|
98
|
+
api_key = resolve_api_key()
|
|
99
|
+
|
|
100
|
+
# --- MCP branch (v3) -----------------------------------------
|
|
101
|
+
if (transport_hint or "").lower() == "mcp":
|
|
102
|
+
state_dir = _resolve_state_dir_for_discovery()
|
|
103
|
+
mcp_transport, mcp_target = resolve_mcp_transport(
|
|
104
|
+
mcp_transport_hint=obj.get("mcp_transport_hint"),
|
|
105
|
+
mcp_url_override=obj.get("mcp_url_override"),
|
|
106
|
+
state_dir=state_dir,
|
|
107
|
+
)
|
|
108
|
+
if obj.get("debug_transport"):
|
|
109
|
+
auth_marker = "with Bearer token" if api_key else "no auth"
|
|
110
|
+
target_label = mcp_target if mcp_target else "subprocess: agent-brain-mcp"
|
|
111
|
+
click.echo(
|
|
112
|
+
f"[debug-transport] mcp ({mcp_transport}) -> "
|
|
113
|
+
f"{target_label} ({auth_marker})",
|
|
114
|
+
err=True,
|
|
115
|
+
)
|
|
116
|
+
try:
|
|
117
|
+
from agent_brain_mcp.client import ( # noqa: F401
|
|
118
|
+
McpHttpBackend,
|
|
119
|
+
McpStdioBackend,
|
|
120
|
+
)
|
|
121
|
+
except ImportError as exc:
|
|
122
|
+
raise click.UsageError(
|
|
123
|
+
"install agent-brain-mcp to use --transport mcp"
|
|
124
|
+
) from exc
|
|
125
|
+
|
|
126
|
+
if mcp_transport == "stdio":
|
|
127
|
+
# §3.5 case 3 precheck — agent-brain-mcp must be on PATH
|
|
128
|
+
# before we hand off to the subprocess-spawning backend.
|
|
129
|
+
# Verbatim §3.5 wording — DO NOT paraphrase.
|
|
130
|
+
if shutil.which("agent-brain-mcp") is None:
|
|
131
|
+
raise click.UsageError(
|
|
132
|
+
"agent-brain-mcp not found on PATH; install "
|
|
133
|
+
"agent-brain-mcp into the same Python environment"
|
|
134
|
+
)
|
|
135
|
+
# cast(): agent_brain_mcp ships with ignore_missing_imports=true
|
|
136
|
+
# so mypy treats McpStdioBackend as Any. The runtime
|
|
137
|
+
# @runtime_checkable Protocol contract is pinned by the
|
|
138
|
+
# Phase 56-03 isinstance test in agent-brain-mcp/tests/.
|
|
139
|
+
return cast(BackendClient, McpStdioBackend(command="agent-brain-mcp"))
|
|
140
|
+
# mcp_transport == "http" — resolve_mcp_transport guarantees
|
|
141
|
+
# mcp_target is not None for the http branch.
|
|
142
|
+
assert mcp_target is not None # noqa: S101
|
|
143
|
+
return cast(BackendClient, McpHttpBackend(url=mcp_target, timeout=timeout))
|
|
144
|
+
|
|
145
|
+
# --- HTTP / UDS branch (existing v1/v2 path) -----------------
|
|
146
|
+
transport, target = resolve_transport(
|
|
147
|
+
transport_hint=transport_hint,
|
|
148
|
+
base_url_override=obj.get("base_url_override"),
|
|
149
|
+
socket_path_override=obj.get("socket_path_override"),
|
|
150
|
+
)
|
|
151
|
+
if obj.get("debug_transport"):
|
|
152
|
+
auth_marker = "with X-API-Key" if api_key else "no auth"
|
|
153
|
+
click.echo(
|
|
154
|
+
f"[debug-transport] {transport} -> {target} ({auth_marker})",
|
|
155
|
+
err=True,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
if transport == "http":
|
|
159
|
+
return DocServeClient(base_url=target, timeout=timeout, api_key=api_key)
|
|
160
|
+
|
|
161
|
+
# UDS: lazy import so HTTP-only invocations don't pay the cost.
|
|
162
|
+
from agent_brain_uds import make_client
|
|
163
|
+
|
|
164
|
+
inner = make_client(socket_path=Path(target), timeout=timeout)
|
|
165
|
+
return DocServeClient.from_httpx(inner, api_key=api_key)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def open_mcp_backend(ctx: click.Context, *, timeout: float = 30.0) -> McpBackend:
|
|
169
|
+
"""Construct an ``McpBackend`` for MCP-only CLI commands (Phase 59).
|
|
170
|
+
|
|
171
|
+
Sibling to :func:`open_backend`: that dispatcher returns a
|
|
172
|
+
:class:`BackendClient` (the tools surface — health/query/index/etc.)
|
|
173
|
+
over any of HTTP/UDS/MCP; this factory returns an
|
|
174
|
+
:class:`McpBackend` (the prompts + resources surface — get_prompt /
|
|
175
|
+
list_resources / read_resource) exclusively over MCP. Every Phase 59
|
|
176
|
+
MCP-only command (``agent-brain prompt``, ``agent-brain resources
|
|
177
|
+
*``) calls this instead of :func:`open_backend` so the
|
|
178
|
+
``--transport mcp`` check lives at a single point of contract.
|
|
179
|
+
|
|
180
|
+
Reads from ``ctx.obj`` (set by the top-level Click group in
|
|
181
|
+
``cli.py``):
|
|
182
|
+
|
|
183
|
+
- ``transport_hint`` — MUST be ``"mcp"`` (case-insensitive); any
|
|
184
|
+
other value raises :class:`click.UsageError` per the Phase 57
|
|
185
|
+
§3.5 no-silent-fallback contract.
|
|
186
|
+
- ``mcp_transport_hint`` (``"stdio"`` / ``"http"`` / ``None``)
|
|
187
|
+
- ``mcp_url_override`` (URL string or ``None``)
|
|
188
|
+
- ``debug_transport`` (bool)
|
|
189
|
+
|
|
190
|
+
Dispatch:
|
|
191
|
+
|
|
192
|
+
1. ``mcp_transport == "stdio"`` →
|
|
193
|
+
:class:`McpStdioBackend(command="agent-brain-mcp")` (precheck:
|
|
194
|
+
``shutil.which("agent-brain-mcp")`` must be non-None; same
|
|
195
|
+
§3.5 case-3 wording as :func:`open_backend`).
|
|
196
|
+
2. ``mcp_transport == "http"`` →
|
|
197
|
+
:class:`McpHttpBackend(url=resolved_mcp_url, timeout=timeout)`.
|
|
198
|
+
|
|
199
|
+
Raises:
|
|
200
|
+
click.UsageError: Exit code 2 when (1) ``transport_hint`` is
|
|
201
|
+
not ``"mcp"``, (2) ``agent-brain-mcp`` is not installed,
|
|
202
|
+
(3) stdio binary not on PATH. Carries the Phase 57 §3.5
|
|
203
|
+
verbatim wording — no silent fallback.
|
|
204
|
+
"""
|
|
205
|
+
obj = ctx.obj or {}
|
|
206
|
+
transport_hint = (obj.get("transport_hint") or "").lower()
|
|
207
|
+
if transport_hint != "mcp":
|
|
208
|
+
# Generic per-factory wording. Each MCP-only command may wrap
|
|
209
|
+
# this and replace the trailing ``<command>`` placeholder with
|
|
210
|
+
# its own name (``prompt``, ``resources list``, etc.) — the
|
|
211
|
+
# default is sufficient for Plan 59-01's contract test and for
|
|
212
|
+
# the rare case where a future command forgets the wrapper.
|
|
213
|
+
raise click.UsageError(
|
|
214
|
+
"This command requires --transport mcp; example: "
|
|
215
|
+
"agent-brain --transport mcp --mcp-transport stdio <command>"
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
state_dir = _resolve_state_dir_for_discovery()
|
|
219
|
+
mcp_transport, mcp_target = resolve_mcp_transport(
|
|
220
|
+
mcp_transport_hint=obj.get("mcp_transport_hint"),
|
|
221
|
+
mcp_url_override=obj.get("mcp_url_override"),
|
|
222
|
+
state_dir=state_dir,
|
|
223
|
+
)
|
|
224
|
+
if obj.get("debug_transport"):
|
|
225
|
+
target_label = mcp_target if mcp_target else "subprocess: agent-brain-mcp"
|
|
226
|
+
click.echo(
|
|
227
|
+
f"[debug-transport] mcp-only ({mcp_transport}) -> {target_label}",
|
|
228
|
+
err=True,
|
|
229
|
+
)
|
|
230
|
+
try:
|
|
231
|
+
from agent_brain_mcp.client import ( # noqa: F401
|
|
232
|
+
McpHttpBackend,
|
|
233
|
+
McpStdioBackend,
|
|
234
|
+
)
|
|
235
|
+
except ImportError as exc:
|
|
236
|
+
raise click.UsageError(
|
|
237
|
+
"install agent-brain-mcp to use --transport mcp"
|
|
238
|
+
) from exc
|
|
239
|
+
|
|
240
|
+
if mcp_transport == "stdio":
|
|
241
|
+
# §3.5 case 3 precheck — verbatim wording, same as open_backend.
|
|
242
|
+
if shutil.which("agent-brain-mcp") is None:
|
|
243
|
+
raise click.UsageError(
|
|
244
|
+
"agent-brain-mcp not found on PATH; install "
|
|
245
|
+
"agent-brain-mcp into the same Python environment"
|
|
246
|
+
)
|
|
247
|
+
# cast(): mirrors open_backend's pattern. The runtime
|
|
248
|
+
# @runtime_checkable Protocol contract is pinned by the
|
|
249
|
+
# Plan 59-01 isinstance test in agent-brain-mcp/tests/
|
|
250
|
+
# test_mcp_backend_protocol_skeleton.py.
|
|
251
|
+
return cast(McpBackend, McpStdioBackend(command="agent-brain-mcp"))
|
|
252
|
+
# mcp_transport == "http" — resolve_mcp_transport guarantees
|
|
253
|
+
# mcp_target is not None for the http branch.
|
|
254
|
+
assert mcp_target is not None # noqa: S101
|
|
255
|
+
return cast(McpBackend, McpHttpBackend(url=mcp_target, timeout=timeout))
|
|
@@ -10,8 +10,11 @@ from .inject import inject_command
|
|
|
10
10
|
from .install_agent import install_agent_command
|
|
11
11
|
from .jobs import jobs_command
|
|
12
12
|
from .list_cmd import list_command
|
|
13
|
+
from .mcp import mcp_group
|
|
14
|
+
from .prompt import prompt_command
|
|
13
15
|
from .query import query_command
|
|
14
16
|
from .reset import reset_command
|
|
17
|
+
from .resources import resources_group
|
|
15
18
|
from .start import start_command
|
|
16
19
|
from .status import status_command
|
|
17
20
|
from .stop import stop_command
|
|
@@ -29,8 +32,11 @@ __all__ = [
|
|
|
29
32
|
"install_agent_command",
|
|
30
33
|
"jobs_command",
|
|
31
34
|
"list_command",
|
|
35
|
+
"mcp_group",
|
|
36
|
+
"prompt_command",
|
|
32
37
|
"query_command",
|
|
33
38
|
"reset_command",
|
|
39
|
+
"resources_group",
|
|
34
40
|
"start_command",
|
|
35
41
|
"status_command",
|
|
36
42
|
"stop_command",
|
|
@@ -6,7 +6,7 @@ from rich.prompt import Confirm
|
|
|
6
6
|
from rich.table import Table
|
|
7
7
|
|
|
8
8
|
from ..client import ConnectionError, ServerError
|
|
9
|
-
from ..client.transport import
|
|
9
|
+
from ..client.transport import open_backend
|
|
10
10
|
|
|
11
11
|
console = Console()
|
|
12
12
|
|
|
@@ -33,7 +33,7 @@ def cache_status(ctx: click.Context, url: str | None, json_output: bool) -> None
|
|
|
33
33
|
ctx.obj["base_url_override"] = url
|
|
34
34
|
ctx.obj["transport_hint"] = "http"
|
|
35
35
|
try:
|
|
36
|
-
with
|
|
36
|
+
with open_backend(ctx) as client:
|
|
37
37
|
data = client.cache_status()
|
|
38
38
|
|
|
39
39
|
if json_output:
|
|
@@ -106,7 +106,7 @@ def cache_clear(ctx: click.Context, url: str | None, yes: bool) -> None:
|
|
|
106
106
|
ctx.obj["base_url_override"] = url
|
|
107
107
|
ctx.obj["transport_hint"] = "http"
|
|
108
108
|
try:
|
|
109
|
-
with
|
|
109
|
+
with open_backend(ctx) as client:
|
|
110
110
|
if not yes:
|
|
111
111
|
# Get current count before asking
|
|
112
112
|
try:
|