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.
Files changed (49) hide show
  1. {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/PKG-INFO +4 -3
  2. {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/__init__.py +1 -1
  3. agent_brain_cli-10.3.0/agent_brain_cli/__main__.py +14 -0
  4. {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/cli.py +35 -3
  5. {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/client/__init__.py +2 -0
  6. {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/client/api_client.py +20 -3
  7. agent_brain_cli-10.3.0/agent_brain_cli/client/protocol.py +173 -0
  8. agent_brain_cli-10.3.0/agent_brain_cli/client/transport.py +255 -0
  9. {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/commands/__init__.py +6 -0
  10. {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/commands/cache.py +3 -3
  11. {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/commands/folders.py +4 -4
  12. {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/commands/index.py +2 -2
  13. {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/commands/init.py +31 -2
  14. {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/commands/inject.py +2 -2
  15. {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/commands/jobs.py +8 -7
  16. agent_brain_cli-10.3.0/agent_brain_cli/commands/mcp.py +453 -0
  17. agent_brain_cli-10.3.0/agent_brain_cli/commands/prompt.py +191 -0
  18. {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/commands/query.py +2 -2
  19. {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/commands/reset.py +2 -2
  20. agent_brain_cli-10.3.0/agent_brain_cli/commands/resources.py +267 -0
  21. {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/commands/start.py +35 -0
  22. {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/commands/status.py +3 -3
  23. {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/config.py +153 -0
  24. agent_brain_cli-10.3.0/agent_brain_cli/mcp_runtime.py +273 -0
  25. {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/pyproject.toml +15 -3
  26. agent_brain_cli-10.2.0/agent_brain_cli/client/transport.py +0 -54
  27. {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/README.md +0 -0
  28. {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/commands/config.py +0 -0
  29. {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/commands/doctor.py +0 -0
  30. {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/commands/install_agent.py +0 -0
  31. {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/commands/list_cmd.py +0 -0
  32. {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/commands/stop.py +0 -0
  33. {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/commands/types.py +0 -0
  34. {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/commands/uninstall.py +0 -0
  35. {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/config_migrate.py +0 -0
  36. {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/config_schema.py +0 -0
  37. {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/diagnostics.py +0 -0
  38. {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/migration.py +0 -0
  39. {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/runtime/__init__.py +0 -0
  40. {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/runtime/claude_converter.py +0 -0
  41. {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/runtime/codex_converter.py +0 -0
  42. {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/runtime/converter_base.py +0 -0
  43. {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/runtime/gemini_converter.py +0 -0
  44. {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/runtime/opencode_converter.py +0 -0
  45. {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/runtime/parser.py +0 -0
  46. {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/runtime/skill_runtime_converter.py +0 -0
  47. {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/runtime/tool_maps.py +0 -0
  48. {agent_brain_cli-10.2.0 → agent_brain_cli-10.3.0}/agent_brain_cli/runtime/types.py +0 -0
  49. {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.2.0
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.2.0,<11.0.0)
19
- Requires-Dist: agent-brain-uds (>=10.2.0,<11.0.0)
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)
@@ -1,3 +1,3 @@
1
1
  """Doc-Serve CLI - Command-line interface for managing Doc-Serve server."""
2
2
 
3
- __version__ = "10.2.0"
3
+ __version__ = "10.3.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 to use: auto (default — UDS if available, HTTP "
40
- "otherwise), http, or uds. Honors AGENT_BRAIN_TRANSPORT env."
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")
@@ -7,8 +7,10 @@ from .api_client import (
7
7
  FolderInfo,
8
8
  ServerError,
9
9
  )
10
+ from .protocol import BackendClient
10
11
 
11
12
  __all__ = [
13
+ "BackendClient",
12
14
  "DocServeClient",
13
15
  "DocServeError",
14
16
  "ConnectionError",
@@ -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
- self._client = httpx.Client(timeout=timeout)
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(cls, client: httpx.Client) -> "DocServeClient":
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.open_client``). The
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 open_client
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 open_client(ctx) as client:
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 open_client(ctx) as client:
109
+ with open_backend(ctx) as client:
110
110
  if not yes:
111
111
  # Get current count before asking
112
112
  try: