zjcode 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. deepagents_code/__init__.py +42 -0
  2. deepagents_code/__main__.py +6 -0
  3. deepagents_code/_ask_user_types.py +90 -0
  4. deepagents_code/_cli_context.py +96 -0
  5. deepagents_code/_constants.py +41 -0
  6. deepagents_code/_debug.py +148 -0
  7. deepagents_code/_debug_buffer.py +204 -0
  8. deepagents_code/_env_vars.py +411 -0
  9. deepagents_code/_fake_models.py +66 -0
  10. deepagents_code/_git.py +521 -0
  11. deepagents_code/_paths.py +69 -0
  12. deepagents_code/_server_config.py +576 -0
  13. deepagents_code/_session_stats.py +235 -0
  14. deepagents_code/_startup_error.py +45 -0
  15. deepagents_code/_testing_models.py +305 -0
  16. deepagents_code/_textual_patches.py +420 -0
  17. deepagents_code/_tool_stream.py +694 -0
  18. deepagents_code/_version.py +46 -0
  19. deepagents_code/agent.py +1976 -0
  20. deepagents_code/app.py +19239 -0
  21. deepagents_code/app.tcss +438 -0
  22. deepagents_code/approval_mode.py +131 -0
  23. deepagents_code/ask_user.py +306 -0
  24. deepagents_code/auth_display.py +185 -0
  25. deepagents_code/auth_store.py +545 -0
  26. deepagents_code/built_in_skills/__init__.py +5 -0
  27. deepagents_code/built_in_skills/remember/SKILL.md +118 -0
  28. deepagents_code/built_in_skills/skill-creator/SKILL.md +383 -0
  29. deepagents_code/built_in_skills/skill-creator/scripts/init_skill.py +366 -0
  30. deepagents_code/built_in_skills/skill-creator/scripts/quick_validate.py +158 -0
  31. deepagents_code/client/__init__.py +1 -0
  32. deepagents_code/client/commands/__init__.py +1 -0
  33. deepagents_code/client/commands/auth.py +541 -0
  34. deepagents_code/client/commands/config.py +714 -0
  35. deepagents_code/client/commands/mcp.py +250 -0
  36. deepagents_code/client/commands/tools.py +416 -0
  37. deepagents_code/client/launch/__init__.py +1 -0
  38. deepagents_code/client/launch/server.py +978 -0
  39. deepagents_code/client/launch/server_manager.py +540 -0
  40. deepagents_code/client/non_interactive.py +1758 -0
  41. deepagents_code/client/remote_client.py +794 -0
  42. deepagents_code/clipboard.py +217 -0
  43. deepagents_code/command_registry.py +450 -0
  44. deepagents_code/config.py +4829 -0
  45. deepagents_code/config_manifest.py +1451 -0
  46. deepagents_code/configurable_model.py +577 -0
  47. deepagents_code/default_agent_prompt.md +12 -0
  48. deepagents_code/doctor.py +559 -0
  49. deepagents_code/editor.py +142 -0
  50. deepagents_code/event_bus.py +411 -0
  51. deepagents_code/extras_info.py +661 -0
  52. deepagents_code/file_ops.py +576 -0
  53. deepagents_code/formatting.py +135 -0
  54. deepagents_code/goal_rubric.py +497 -0
  55. deepagents_code/goal_tools.py +459 -0
  56. deepagents_code/hooks.py +348 -0
  57. deepagents_code/input.py +1041 -0
  58. deepagents_code/integrations/__init__.py +1 -0
  59. deepagents_code/integrations/openai_codex.py +551 -0
  60. deepagents_code/integrations/sandbox_config.py +198 -0
  61. deepagents_code/integrations/sandbox_factory.py +1124 -0
  62. deepagents_code/integrations/sandbox_provider.py +137 -0
  63. deepagents_code/integrations/sandbox_registry.py +350 -0
  64. deepagents_code/iterm_cursor_guide.py +176 -0
  65. deepagents_code/local_context.py +926 -0
  66. deepagents_code/main.py +3985 -0
  67. deepagents_code/managed_tools.py +642 -0
  68. deepagents_code/mcp_auth.py +1950 -0
  69. deepagents_code/mcp_config.py +176 -0
  70. deepagents_code/mcp_disabled.py +212 -0
  71. deepagents_code/mcp_login_service.py +281 -0
  72. deepagents_code/mcp_oauth_ui.py +199 -0
  73. deepagents_code/mcp_providers/__init__.py +23 -0
  74. deepagents_code/mcp_providers/_registry.py +39 -0
  75. deepagents_code/mcp_providers/base.py +133 -0
  76. deepagents_code/mcp_providers/github.py +102 -0
  77. deepagents_code/mcp_providers/slack.py +175 -0
  78. deepagents_code/mcp_tools.py +2427 -0
  79. deepagents_code/mcp_trust.py +207 -0
  80. deepagents_code/media_utils.py +826 -0
  81. deepagents_code/memory_guard.py +474 -0
  82. deepagents_code/model_config.py +4156 -0
  83. deepagents_code/notifications.py +247 -0
  84. deepagents_code/offload.py +402 -0
  85. deepagents_code/onboarding.py +223 -0
  86. deepagents_code/output.py +69 -0
  87. deepagents_code/paste_collapse.py +103 -0
  88. deepagents_code/project_utils.py +231 -0
  89. deepagents_code/py.typed +0 -0
  90. deepagents_code/reasoning_effort.py +641 -0
  91. deepagents_code/reliable_rubric.py +97 -0
  92. deepagents_code/resume_state.py +237 -0
  93. deepagents_code/server_graph.py +310 -0
  94. deepagents_code/session_end_summary.py +329 -0
  95. deepagents_code/sessions.py +1716 -0
  96. deepagents_code/skills/__init__.py +18 -0
  97. deepagents_code/skills/commands.py +1252 -0
  98. deepagents_code/skills/invocation.py +112 -0
  99. deepagents_code/skills/load.py +196 -0
  100. deepagents_code/skills/trust.py +547 -0
  101. deepagents_code/state_migration.py +136 -0
  102. deepagents_code/subagents.py +278 -0
  103. deepagents_code/system_prompt.md +204 -0
  104. deepagents_code/terminal_capabilities.py +115 -0
  105. deepagents_code/terminal_escape.py +287 -0
  106. deepagents_code/theme.py +891 -0
  107. deepagents_code/todo_list_prompt.md +12 -0
  108. deepagents_code/tool_catalog.py +509 -0
  109. deepagents_code/tool_display.py +367 -0
  110. deepagents_code/tools.py +516 -0
  111. deepagents_code/tui/__init__.py +1 -0
  112. deepagents_code/tui/textual_adapter.py +2553 -0
  113. deepagents_code/tui/widgets/__init__.py +9 -0
  114. deepagents_code/tui/widgets/_js_eval_display.py +139 -0
  115. deepagents_code/tui/widgets/_links.py +261 -0
  116. deepagents_code/tui/widgets/agent_selector.py +420 -0
  117. deepagents_code/tui/widgets/approval.py +602 -0
  118. deepagents_code/tui/widgets/ask_user.py +515 -0
  119. deepagents_code/tui/widgets/auth.py +1997 -0
  120. deepagents_code/tui/widgets/autocomplete.py +890 -0
  121. deepagents_code/tui/widgets/chat_input.py +3181 -0
  122. deepagents_code/tui/widgets/codex_auth.py +452 -0
  123. deepagents_code/tui/widgets/cwd_switch.py +242 -0
  124. deepagents_code/tui/widgets/debug_console.py +868 -0
  125. deepagents_code/tui/widgets/diff.py +252 -0
  126. deepagents_code/tui/widgets/effort_selector.py +189 -0
  127. deepagents_code/tui/widgets/goal_review.py +390 -0
  128. deepagents_code/tui/widgets/goal_status.py +50 -0
  129. deepagents_code/tui/widgets/history.py +194 -0
  130. deepagents_code/tui/widgets/install_confirm.py +248 -0
  131. deepagents_code/tui/widgets/launch_init.py +482 -0
  132. deepagents_code/tui/widgets/loading.py +227 -0
  133. deepagents_code/tui/widgets/mcp_login.py +539 -0
  134. deepagents_code/tui/widgets/mcp_reconnect.py +212 -0
  135. deepagents_code/tui/widgets/mcp_viewer.py +1634 -0
  136. deepagents_code/tui/widgets/message_store.py +999 -0
  137. deepagents_code/tui/widgets/messages.py +3846 -0
  138. deepagents_code/tui/widgets/model_selector.py +2343 -0
  139. deepagents_code/tui/widgets/notification_center.py +456 -0
  140. deepagents_code/tui/widgets/notification_detail.py +257 -0
  141. deepagents_code/tui/widgets/notification_settings.py +170 -0
  142. deepagents_code/tui/widgets/restart_prompt.py +158 -0
  143. deepagents_code/tui/widgets/skill_trust.py +131 -0
  144. deepagents_code/tui/widgets/startup_tip.py +91 -0
  145. deepagents_code/tui/widgets/status.py +781 -0
  146. deepagents_code/tui/widgets/subagent_panel.py +969 -0
  147. deepagents_code/tui/widgets/theme_selector.py +401 -0
  148. deepagents_code/tui/widgets/thread_selector.py +2564 -0
  149. deepagents_code/tui/widgets/tool_renderers.py +190 -0
  150. deepagents_code/tui/widgets/tool_widgets.py +304 -0
  151. deepagents_code/tui/widgets/update_available.py +311 -0
  152. deepagents_code/tui/widgets/update_confirm.py +184 -0
  153. deepagents_code/tui/widgets/update_progress.py +327 -0
  154. deepagents_code/tui/widgets/welcome.py +508 -0
  155. deepagents_code/turn_end_summary.py +522 -0
  156. deepagents_code/ui.py +858 -0
  157. deepagents_code/unicode_security.py +563 -0
  158. deepagents_code/update_check.py +3124 -0
  159. zjcode-0.0.1.data/data/deepagents_code/default_agent_prompt.md +12 -0
  160. zjcode-0.0.1.dist-info/METADATA +220 -0
  161. zjcode-0.0.1.dist-info/RECORD +163 -0
  162. zjcode-0.0.1.dist-info/WHEEL +4 -0
  163. zjcode-0.0.1.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,12 @@
1
+ ### Todo List Management
2
+
3
+ When using the write_todos tool:
4
+
5
+ 1. Use todos for any task with 2+ steps — they give the user visibility
6
+ 2. Mark tasks `in_progress` before starting, `completed` immediately after
7
+ 3. Don't batch completions — mark each item done as you finish it
8
+ 4. If a task reveals sub-tasks, add them right away
9
+ 5. For simple 1-step tasks, just do them directly
10
+ {todo_guidance}
11
+
12
+ The todo list is a planning tool - use it judiciously to avoid overwhelming the user with excessive task tracking.
@@ -0,0 +1,509 @@
1
+ """Enumerate the tools available to the agent.
2
+
3
+ Backs two entry points: the `dcode tools list` CLI command (`_run_tools_list`)
4
+ and the interactive `/tools` slash command (`app._handle_tools_command`).
5
+
6
+ The tool set is read from the *real* tool objects the agent binds rather than a
7
+ hand-maintained catalog, so names and descriptions never drift from what the
8
+ model actually sees. Built-in tools are collected by compiling the agent with a
9
+ throwaway offline chat model (no credentials, no network) and reading the bound
10
+ tool node; MCP tools are discovered via the same path the app and server use.
11
+
12
+ The collection functions here lazily import the heavy agent stack (agent
13
+ compilation, MCP discovery) inside their bodies. Only the fake-model base is
14
+ imported at module top, so importing this module is cheap relative to the agent
15
+ stack — and this module is itself imported lazily by both entry points
16
+ (`_run_tools_list` and `_handle_tools_command`), never on the startup hot path.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import asyncio
22
+ import logging
23
+ from collections.abc import Mapping
24
+ from dataclasses import dataclass
25
+ from pathlib import Path
26
+ from typing import TYPE_CHECKING, Any, Literal, cast
27
+
28
+ from deepagents_code._fake_models import _ToolBindingFakeModel
29
+
30
+ if TYPE_CHECKING:
31
+ from collections.abc import Sequence
32
+
33
+ from langgraph.prebuilt.tool_node import ToolNode
34
+
35
+ from deepagents_code.mcp_tools import MCPServerInfo, MCPServerStatus
36
+
37
+ logger = logging.getLogger(__name__)
38
+
39
+ ToolSource = Literal["built-in", "mcp"]
40
+ """Stable source token identifying where a tool group comes from.
41
+
42
+ Emitted verbatim in the `--json` output, so it is a public contract; keep it a
43
+ `Literal` of stable tokens (not a bare `str`), following the same convention as
44
+ `mcp_tools.MCPServerStatus`.
45
+ """
46
+
47
+ BUILT_IN_GROUP = "Built-in"
48
+ """Display label for the group of tools bundled with `deepagents-code`."""
49
+
50
+
51
+ @dataclass(frozen=True, slots=True)
52
+ class ToolEntry:
53
+ """A single tool's display metadata."""
54
+
55
+ name: str
56
+ """Tool name as bound on the agent (e.g. `read_file`)."""
57
+
58
+ description: str
59
+ """First non-empty line of the tool's description, whitespace-collapsed."""
60
+
61
+
62
+ @dataclass(frozen=True, slots=True)
63
+ class ToolGroup:
64
+ """A named group of tools sharing a source."""
65
+
66
+ label: str
67
+ """Group heading (`Built-in`, or the MCP server name)."""
68
+
69
+ source: ToolSource
70
+ """Stable source token: `built-in` or `mcp`."""
71
+
72
+ tools: tuple[ToolEntry, ...]
73
+ """Tools in this group, in bind order."""
74
+
75
+
76
+ @dataclass(frozen=True, slots=True)
77
+ class UnavailableServer:
78
+ """An MCP server that was discovered but currently exposes no tools."""
79
+
80
+ name: str
81
+ """Server name from the MCP configuration."""
82
+
83
+ status: MCPServerStatus
84
+ """Load status token — any non-`ok` `mcp_tools.MCPServerStatus`.
85
+
86
+ Reuses `MCPServerStatus` (rather than a bare `str`) so the same closed value
87
+ set governs this field and its `--json` output, and so the token list has a
88
+ single source of truth in `mcp_tools`.
89
+ """
90
+
91
+ detail: str
92
+ """Human-readable reason from discovery, or `""` when none was given.
93
+
94
+ For config-load failures this is discovery's own reason string, which may
95
+ include the local config file path (e.g. `~/.deepagents/mcp.json: ...`) —
96
+ the same text the interactive `/mcp` viewer shows. See `collect_mcp_catalog`.
97
+ """
98
+
99
+ def __post_init__(self) -> None:
100
+ """Enforce that an unavailable server is never `ok`.
101
+
102
+ An `ok` server exposes tools and belongs in a `ToolGroup`, never here;
103
+ rejecting it at construction keeps the documented non-`ok` invariant
104
+ from being silently violated by a future producer.
105
+
106
+ Raises:
107
+ ValueError: If `status` is `"ok"`.
108
+ """
109
+ if self.status == "ok":
110
+ msg = "UnavailableServer.status must be a non-'ok' MCPServerStatus"
111
+ raise ValueError(msg)
112
+
113
+
114
+ @dataclass(frozen=True, slots=True)
115
+ class ToolCatalog:
116
+ """Everything `dcode tools list` needs to render, in display order."""
117
+
118
+ groups: tuple[ToolGroup, ...]
119
+ """Built-in group first, then one group per MCP server that exposes tools."""
120
+
121
+ unavailable: tuple[UnavailableServer, ...] = ()
122
+ """MCP servers discovered with no tools (errored, needing login, or disabled).
123
+
124
+ Surfaced rather than dropped so a user debugging a missing tool can see why
125
+ it is absent.
126
+ """
127
+
128
+ mcp_error: str | None = None
129
+ """Generic notice set when MCP discovery itself failed; `None` on success.
130
+
131
+ Raw exception detail is logged at debug level, never embedded here, so no
132
+ file paths or stack traces leak into CLI/JSON output.
133
+ """
134
+
135
+
136
+ def unavailable_server_display(server: UnavailableServer) -> tuple[str, str]:
137
+ """Return the `(status_label, detail)` display pair for an unavailable server.
138
+
139
+ Shared by the CLI (`client.commands.tools._print_unavailable_servers`) and
140
+ TUI (`app._render_tool_catalog`) renderers so both describe a server the same
141
+ way. A disabled server shows its reconnect guidance if present, else the
142
+ generic "disabled by user", with no separate detail; other statuses show the
143
+ status token plus discovery's reason string when present.
144
+
145
+ Args:
146
+ server: A server that loaded with no usable tools.
147
+
148
+ Returns:
149
+ `(status_label, detail)`: the primary status text and any secondary
150
+ detail (`""` when none). Each renderer lays these out itself, e.g. as
151
+ `status_label: detail`.
152
+ """
153
+ if server.status == "disabled":
154
+ return (server.detail or "disabled by user", "")
155
+ return (server.status, server.detail)
156
+
157
+
158
+ class _CatalogModel(_ToolBindingFakeModel):
159
+ """Offline placeholder model used only to compile the agent for enumeration.
160
+
161
+ Compiling the agent binds every tool but never calls the model, so this
162
+ never issues a request — enumeration only reads the bound tool node. It
163
+ exists so tool enumeration works without credentials or network access.
164
+ Inherits the `bind_tools` passthrough and minimal `profile` the agent
165
+ runtime reads during setup from `_ToolBindingFakeModel`.
166
+ """
167
+
168
+ model: str = "catalog"
169
+
170
+
171
+ def _first_line(text: str | None) -> str:
172
+ """Return the first non-empty line of `text`, whitespace-collapsed."""
173
+ if not text:
174
+ return ""
175
+ for line in text.splitlines():
176
+ stripped = line.strip()
177
+ if stripped:
178
+ return " ".join(stripped.split())
179
+ return ""
180
+
181
+
182
+ def collect_built_in_tools(
183
+ *, assistant_id: str = "agent", enable_interpreter: bool = False
184
+ ) -> list[ToolEntry]:
185
+ """Enumerate the built-in tools the agent binds by default.
186
+
187
+ Compiles the agent with an offline placeholder model and reads the bound
188
+ tool node. Memory and skills are disabled because they contribute no tools
189
+ (they only augment the system prompt). The selected assistant id is still
190
+ forwarded so agent-specific subagents are loaded from the same directory the
191
+ normal launch path uses. The custom CLI tools are included the same way
192
+ `server_graph._build_tools` adds them, so `web_search` appears only when
193
+ Tavily is configured.
194
+
195
+ Args:
196
+ assistant_id: Resolved dcode agent identifier to compile.
197
+ enable_interpreter: Wire the JS interpreter middleware so `js_eval`
198
+ appears when the default agent would bind it. Callers should pass
199
+ the resolved runtime setting (see `_resolve_enable_interpreter`) so
200
+ the list matches the tools the agent actually binds.
201
+
202
+ Returns:
203
+ Built-in tools in bind order.
204
+
205
+ Raises:
206
+ RuntimeError: If the compiled graph does not expose its bound tools.
207
+ """
208
+ from deepagents_code.agent import create_cli_agent
209
+ from deepagents_code.config import settings
210
+ from deepagents_code.tools import fetch_url, get_current_thread_id, web_search
211
+
212
+ # Keep in sync with `server_graph._build_tools`: web_search is bound only
213
+ # when Tavily is configured, so it appears here only under the same gate.
214
+ custom_tools: list[Any] = [fetch_url, get_current_thread_id]
215
+ if settings.has_tavily:
216
+ custom_tools.append(web_search)
217
+
218
+ agent, _backend = create_cli_agent(
219
+ _CatalogModel(),
220
+ assistant_id=assistant_id,
221
+ tools=custom_tools,
222
+ enable_memory=False,
223
+ enable_skills=False,
224
+ enable_shell=True,
225
+ enable_interpreter=enable_interpreter,
226
+ )
227
+ tools = collect_tools_from_agent(agent)
228
+ if tools is None:
229
+ msg = "Compiled agent does not expose a LangGraph tool node"
230
+ raise RuntimeError(msg)
231
+ return tools
232
+
233
+
234
+ def collect_tools_from_agent(agent: object) -> list[ToolEntry] | None:
235
+ """Read tools from a local compiled agent when its graph is inspectable.
236
+
237
+ LangGraph does not expose a public tool-enumeration API, so this reaches
238
+ through the compiled graph's conventional `nodes["tools"].bound` shape.
239
+ Returning `None` distinguishes an uninspectable graph (a remote agent, or a
240
+ local graph whose internals no longer match that convention) from a local
241
+ graph that validly binds zero tools (`[]`).
242
+
243
+ Args:
244
+ agent: Active local or remote agent object.
245
+
246
+ Returns:
247
+ Bound tools in graph order; `[]` for an inspectable local graph with no
248
+ tools; or `None` when the agent cannot be inspected locally.
249
+ """
250
+ nodes = getattr(agent, "nodes", None)
251
+ if not isinstance(nodes, Mapping):
252
+ # No conventional node map: a remote agent or a non-graph object. Expected
253
+ # for remote agents, so debug rather than warning.
254
+ logger.debug("Agent %r has no inspectable node map", type(agent))
255
+ return None
256
+ if "tools" not in nodes:
257
+ # LangChain omits the tool node when an otherwise valid local agent
258
+ # binds no tools. The graph is still inspectable; its tool set is empty.
259
+ return []
260
+ node = nodes.get("tools")
261
+ tool_node = cast("ToolNode | None", getattr(node, "bound", None))
262
+ tools_by_name = getattr(tool_node, "tools_by_name", None)
263
+ if not isinstance(tools_by_name, Mapping):
264
+ # A "tools" node exists but does not expose the expected
265
+ # `bound.tools_by_name` mapping — a LangGraph internal-shape change, not
266
+ # a remote agent. Warn so this drift is visible in logs even though the
267
+ # user-facing notice attributes it to an uninspectable agent.
268
+ logger.warning(
269
+ "Agent 'tools' node is not introspectable (bound=%r); "
270
+ "LangGraph internals may have changed",
271
+ type(tool_node),
272
+ )
273
+ return None
274
+ tools: list[ToolEntry] = []
275
+ for name, tool in tools_by_name.items():
276
+ if not isinstance(name, str):
277
+ continue
278
+ description = getattr(tool, "description", None)
279
+ tools.append(
280
+ ToolEntry(
281
+ name=name,
282
+ description=_first_line(
283
+ description if isinstance(description, str) else None
284
+ ),
285
+ )
286
+ )
287
+ return tools
288
+
289
+
290
+ def collect_mcp_catalog(
291
+ *,
292
+ mcp_config_path: str | None = None,
293
+ trust_project_mcp: bool | None = None,
294
+ ) -> tuple[list[ToolGroup], list[UnavailableServer], str | None]:
295
+ """Discover MCP servers, split into tool groups and unavailable servers.
296
+
297
+ Best-effort: if discovery itself raises (no config, offline, load error),
298
+ the technical detail is logged and a generic `mcp_error` message is
299
+ returned so `dcode tools list` still renders the built-in tools while
300
+ telling the user discovery failed. Servers that loaded but expose no tools
301
+ are reported as `UnavailableServer`s (errored, needing login, or disabled)
302
+ rather than silently dropped — surfacing exactly what a user running this
303
+ command to debug a missing tool needs to see.
304
+
305
+ Args:
306
+ mcp_config_path: Explicit MCP config path (`--mcp-config`), or `None`
307
+ to rely on auto-discovery.
308
+ trust_project_mcp: Project-level stdio trust decision
309
+ (`--trust-project-mcp`), forwarded to discovery unchanged.
310
+
311
+ Returns:
312
+ `(groups, unavailable, mcp_error)`: per-server tool groups, discovered
313
+ servers exposing no tools, and a generic discovery-failure message
314
+ (`None` when discovery succeeded).
315
+ """
316
+ try:
317
+ server_info = asyncio.run(
318
+ _load_mcp_server_info(
319
+ mcp_config_path=mcp_config_path,
320
+ trust_project_mcp=trust_project_mcp,
321
+ )
322
+ )
323
+ except Exception:
324
+ # Log the real cause for debugging, but return a generic message so no
325
+ # file path or stack trace leaks into CLI/JSON output.
326
+ logger.warning("MCP tool discovery failed for `tools list`", exc_info=True)
327
+ return [], [], "MCP discovery failed; showing built-in tools only."
328
+
329
+ groups, unavailable = split_mcp_server_info(server_info)
330
+ return groups, unavailable, None
331
+
332
+
333
+ def split_mcp_server_info(
334
+ server_info: Sequence[MCPServerInfo],
335
+ ) -> tuple[list[ToolGroup], list[UnavailableServer]]:
336
+ """Split loaded MCP server metadata into tool groups and unavailable servers.
337
+
338
+ Pure function shared by the CLI discovery path (`collect_mcp_catalog`) and
339
+ the interactive `/tools` command, which passes the app's already-loaded
340
+ `MCPServerInfo` list rather than re-discovering (Textual's running event
341
+ loop forbids the `asyncio.run` discovery path).
342
+
343
+ Servers that loaded but expose no tools are reported as `UnavailableServer`s
344
+ (errored, needing login, or disabled) rather than silently dropped —
345
+ surfacing exactly what a user debugging a missing tool needs to see.
346
+
347
+ Args:
348
+ server_info: Loaded MCP server metadata.
349
+
350
+ Returns:
351
+ `(groups, unavailable)`: per-server tool groups (only servers exposing
352
+ tools) and servers discovered with no tools and a non-`ok` status.
353
+ """
354
+ groups: list[ToolGroup] = []
355
+ unavailable: list[UnavailableServer] = []
356
+ for server in server_info:
357
+ if server.tools:
358
+ entries = tuple(
359
+ ToolEntry(name=tool.name, description=_first_line(tool.description))
360
+ for tool in server.tools
361
+ )
362
+ groups.append(ToolGroup(label=server.name, source="mcp", tools=entries))
363
+ elif server.status != "ok":
364
+ # A server that loaded but has no tools *and* is not "ok" is broken,
365
+ # unauthenticated, or disabled — report it so the omission is
366
+ # explained. A plainly-disabled server drops discovery's reason so
367
+ # the renderers show the generic "disabled by user" label; a
368
+ # just-re-enabled one (`pending_reconnect`) keeps its reconnect
369
+ # guidance so the renderer can distinguish it from a server the user
370
+ # left disabled. Other statuses retain discovery's reason string —
371
+ # not a stack trace, but config-load failures can include the local
372
+ # config file path — see `UnavailableServer.detail`.
373
+ detail = server.error or ""
374
+ if server.status == "disabled" and not server.pending_reconnect:
375
+ detail = ""
376
+ unavailable.append(
377
+ UnavailableServer(
378
+ name=server.name,
379
+ status=server.status,
380
+ detail=detail,
381
+ )
382
+ )
383
+ return groups, unavailable
384
+
385
+
386
+ def build_catalog_from_server_info(
387
+ built_in: Sequence[ToolEntry],
388
+ server_info: Sequence[MCPServerInfo],
389
+ ) -> ToolCatalog:
390
+ """Assemble a `ToolCatalog` from pre-collected built-in tools and live MCP info.
391
+
392
+ The interactive `/tools` command entry point: it avoids the `asyncio.run`
393
+ MCP discovery reached via `collect_catalog` (the `asyncio.run` call itself
394
+ lives in `collect_mcp_catalog`), which cannot run inside Textual's running
395
+ event loop, by reusing the MCP metadata the app already loaded. `mcp_error`
396
+ is always `None` here because discovery is not attempted — any load failures
397
+ are already reflected per-server in `server_info` as non-`ok` `MCPServerInfo`
398
+ entries, which `split_mcp_server_info` surfaces as `UnavailableServer`s.
399
+
400
+ Args:
401
+ built_in: Built-in tools in bind order (from `collect_built_in_tools`).
402
+ server_info: The app's already-loaded MCP server metadata.
403
+
404
+ Returns:
405
+ A `ToolCatalog` with the built-in group first, then any MCP groups, plus
406
+ unavailable servers.
407
+ """
408
+ groups: list[ToolGroup] = [
409
+ ToolGroup(label=BUILT_IN_GROUP, source="built-in", tools=tuple(built_in))
410
+ ]
411
+ mcp_groups, unavailable = split_mcp_server_info(server_info)
412
+ groups.extend(mcp_groups)
413
+ return ToolCatalog(groups=tuple(groups), unavailable=tuple(unavailable))
414
+
415
+
416
+ async def _load_mcp_server_info(
417
+ *,
418
+ mcp_config_path: str | None,
419
+ trust_project_mcp: bool | None,
420
+ ) -> list[Any]:
421
+ """Load MCP server metadata, cleaning up any temporary sessions.
422
+
423
+ Args:
424
+ mcp_config_path: Explicit MCP config path, or `None` for auto-discovery.
425
+ trust_project_mcp: Project-level stdio trust decision.
426
+
427
+ Returns:
428
+ Discovered MCP server metadata, or an empty list when none load.
429
+ """
430
+ from deepagents_code.mcp_tools import resolve_and_load_mcp_tools
431
+ from deepagents_code.project_utils import ProjectContext
432
+
433
+ try:
434
+ project_context = ProjectContext.from_user_cwd(Path.cwd())
435
+ except (OSError, RuntimeError):
436
+ # `Path.cwd()`/`.resolve()` raise OSError for a missing cwd and
437
+ # RuntimeError on a symlink loop (3.11-3.12); match the codebase's own
438
+ # convention in `project_utils` and fall back to no project context.
439
+ logger.warning("Could not determine working directory for MCP discovery")
440
+ project_context = None
441
+
442
+ session_manager = None
443
+ try:
444
+ _tools, session_manager, server_info = await resolve_and_load_mcp_tools(
445
+ explicit_config_path=mcp_config_path,
446
+ no_mcp=False,
447
+ trust_project_mcp=trust_project_mcp,
448
+ project_context=project_context,
449
+ )
450
+ return server_info or []
451
+ finally:
452
+ if session_manager is not None:
453
+ try:
454
+ await session_manager.cleanup()
455
+ except Exception:
456
+ logger.warning("MCP discovery cleanup failed", exc_info=True)
457
+
458
+
459
+ def collect_catalog(
460
+ *,
461
+ assistant_id: str = "agent",
462
+ enable_interpreter: bool = False,
463
+ include_mcp: bool = True,
464
+ mcp_config_path: str | None = None,
465
+ trust_project_mcp: bool | None = None,
466
+ ) -> ToolCatalog:
467
+ """Collect everything `dcode tools list` renders.
468
+
469
+ Args:
470
+ assistant_id: Resolved dcode agent identifier to compile for built-in
471
+ tools, including any agent-specific subagents.
472
+ enable_interpreter: Whether the default agent binds `js_eval`; forwarded
473
+ to `collect_built_in_tools`.
474
+ include_mcp: When `True`, discover MCP servers and append their groups
475
+ after the built-in group (best-effort). Pass `False` to mirror
476
+ `--no-mcp`.
477
+ mcp_config_path: Explicit MCP config path (`--mcp-config`).
478
+ trust_project_mcp: Project-level stdio trust decision
479
+ (`--trust-project-mcp`).
480
+
481
+ Returns:
482
+ A `ToolCatalog` with the built-in group first, then any MCP groups,
483
+ plus unavailable servers and any discovery-failure notice.
484
+ """
485
+ groups: list[ToolGroup] = [
486
+ ToolGroup(
487
+ label=BUILT_IN_GROUP,
488
+ source="built-in",
489
+ tools=tuple(
490
+ collect_built_in_tools(
491
+ assistant_id=assistant_id,
492
+ enable_interpreter=enable_interpreter,
493
+ )
494
+ ),
495
+ )
496
+ ]
497
+ unavailable: list[UnavailableServer] = []
498
+ mcp_error: str | None = None
499
+ if include_mcp:
500
+ mcp_groups, unavailable, mcp_error = collect_mcp_catalog(
501
+ mcp_config_path=mcp_config_path,
502
+ trust_project_mcp=trust_project_mcp,
503
+ )
504
+ groups.extend(mcp_groups)
505
+ return ToolCatalog(
506
+ groups=tuple(groups),
507
+ unavailable=tuple(unavailable),
508
+ mcp_error=mcp_error,
509
+ )