feat(cli): add docs link to project MCP approval prompt (#3341)

Closes #3308

---

The `Project MCP servers require approval` prompt didn't explain what
project-level trust actually means or how to revoke it once granted.
Adds a `Learn more:` line that links to the project-level-trust section
of the MCP tools docs, so users have a one-click jump to the security
model, the trust store, and the `--trust-project-mcp` flag before
deciding.

The link is rendered with Rich's `[link=...]` markup (same pattern used
by `show_help` and the auth provider hints), so terminals that support
OSC 8 make it clickable while plain terminals still see the URL.

---------

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
Co-authored-by: Mason Daugherty <61371264+mdrxy@users.noreply.github.com>
Co-authored-by: Mason Daugherty <github@mdrxy.com>
Co-authored-by: Mason Daugherty <mason@langchain.dev>
This commit is contained in:
open-swe[bot]
2026-05-11 13:39:59 -07:00
committed by GitHub
parent d69e1431e3
commit b5c12280e4
3 changed files with 161 additions and 3 deletions
+13
View File
@@ -47,6 +47,19 @@ as enabled, and `0`, `false`, `no`, `off`, empty string, or unset as disabled.""
DEBUG_FILE = "DEEPAGENTS_CLI_DEBUG_FILE"
"""Path for the debug log file (default: `/tmp/deepagents_debug.log`)."""
DEBUG_MCP_PROJECT_TRUST = "DEEPAGENTS_CLI_DEBUG_MCP_PROJECT_TRUST"
"""Force the project MCP approval prompt for manual UI testing.
Set to a truthy value when launching the interactive CLI to render the
project-level MCP trust prompt without relying on an untrusted config state. If
project MCP servers are discovered, the prompt shows those real servers;
otherwise it shows a sample server. The CLI exits after the prompt response so
the debug run does not continue into TUI or server startup, and it does not
persist trust decisions.
Parsed by `is_env_truthy`: accepts `1`, `true`, `yes`, `on` as enabled.
"""
DEBUG_NOTIFICATIONS = "DEEPAGENTS_CLI_DEBUG_NOTIFICATIONS"
"""Inject sample missing-dependency notifications at launch so the notification
center UI can be exercised without waiting for real conditions. Does not
+33 -3
View File
@@ -1432,6 +1432,13 @@ def _print_session_stats(stats: Any, console: Any) -> None: # noqa: ANN401
print_usage_table(stats, stats.wall_time_seconds, console)
def _debug_mcp_project_trust_enabled() -> bool:
"""Return whether the project MCP approval prompt debug path is enabled."""
from deepagents_cli._env_vars import DEBUG_MCP_PROJECT_TRUST, is_env_truthy
return is_env_truthy(DEBUG_MCP_PROJECT_TRUST)
def _check_mcp_project_trust(*, trust_flag: bool = False) -> bool | None:
"""Check whether project-level MCP servers should be trusted.
@@ -1461,6 +1468,8 @@ def _check_mcp_project_trust(*, trust_flag: bool = False) -> bool | None:
)
from deepagents_cli.project_utils import ProjectContext
debug_prompt = _debug_mcp_project_trust_enabled()
try:
project_context = ProjectContext.from_user_cwd(Path.cwd())
config_paths = discover_mcp_configs(project_context=project_context)
@@ -1468,7 +1477,7 @@ def _check_mcp_project_trust(*, trust_flag: bool = False) -> bool | None:
return None
_, project_configs = classify_discovered_configs(config_paths)
if not project_configs:
if not project_configs and not debug_prompt:
return None
# Merge configs by server name (last wins, matching the loader) so that
@@ -1483,6 +1492,15 @@ def _check_mcp_project_trust(*, trust_flag: bool = False) -> bool | None:
merged_config = merge_mcp_configs(loaded_configs)
all_servers = extract_project_server_summaries(merged_config)
if not all_servers and debug_prompt:
all_servers = [
(
"debug-project-mcp",
"stdio",
"uvx deepagents-debug-mcp --sample-project-server",
)
]
if not all_servers:
return None
@@ -1501,12 +1519,16 @@ def _check_mcp_project_trust(*, trust_flag: bool = False) -> bool | None:
)
fingerprint = compute_config_fingerprint(project_configs)
if is_project_mcp_trusted(project_root, fingerprint):
if not debug_prompt and is_project_mcp_trusted(project_root, fingerprint):
return True
# Interactive prompt
from rich.console import Console as _Console
docs_url = (
"https://docs.langchain.com/oss/python/deepagents/cli/"
"mcp-tools#project-level-trust"
)
prompt_console = _Console(stderr=True)
prompt_console.print()
prompt_console.print(
@@ -1515,6 +1537,11 @@ def _check_mcp_project_trust(*, trust_flag: bool = False) -> bool | None:
for name, kind, summary in all_servers:
prompt_console.print(f' [bold]"{name}"[/bold] ({kind}): {summary}')
prompt_console.print()
prompt_console.print(
f"[dim]Learn more: [link={docs_url}]{docs_url}[/link][/dim]",
highlight=False,
)
prompt_console.print()
try:
answer = input("Allow? [y/N]: ").strip().lower()
@@ -1522,7 +1549,8 @@ def _check_mcp_project_trust(*, trust_flag: bool = False) -> bool | None:
answer = ""
if answer == "y":
trust_project_mcp(project_root, fingerprint)
if not debug_prompt:
trust_project_mcp(project_root, fingerprint)
return True
return False
@@ -2115,6 +2143,8 @@ def cli_main() -> None:
mcp_trust_decision = _check_mcp_project_trust(
trust_flag=getattr(args, "trust_project_mcp", False),
)
if _debug_mcp_project_trust_enabled():
sys.exit(0)
# Run Textual CLI
return_code = 0
+115
View File
@@ -923,6 +923,121 @@ class TestThreadsListCwdArgparse:
assert ns.cwd == "/some/path"
class TestCheckMcpProjectTrustPrompt:
"""The project MCP approval prompt should surface a docs link."""
def test_debug_env_helper_uses_truthy_parsing(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
"""The debug helper treats common falsy strings as disabled."""
from deepagents_cli._env_vars import DEBUG_MCP_PROJECT_TRUST
from deepagents_cli.main import _debug_mcp_project_trust_enabled
monkeypatch.setenv(DEBUG_MCP_PROJECT_TRUST, "0")
assert _debug_mcp_project_trust_enabled() is False
monkeypatch.setenv(DEBUG_MCP_PROJECT_TRUST, "1")
assert _debug_mcp_project_trust_enabled() is True
def test_debug_env_forces_prompt_without_project_config(
self,
capsys: pytest.CaptureFixture[str],
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""The debug env var shows a sample prompt without requiring config files."""
from deepagents_cli._env_vars import DEBUG_MCP_PROJECT_TRUST
from deepagents_cli.main import _check_mcp_project_trust
project_context = SimpleNamespace(project_root=tmp_path, user_cwd=tmp_path)
monkeypatch.setenv(DEBUG_MCP_PROJECT_TRUST, "1")
with (
patch(
"deepagents_cli.project_utils.ProjectContext.from_user_cwd",
return_value=project_context,
),
patch(
"deepagents_cli.mcp_tools.discover_mcp_configs",
return_value=[],
),
patch(
"deepagents_cli.mcp_tools.classify_discovered_configs",
return_value=([], []),
),
patch(
"deepagents_cli.mcp_trust.is_project_mcp_trusted",
return_value=True,
),
patch("deepagents_cli.mcp_trust.trust_project_mcp") as trust_project_mcp,
patch("builtins.input", return_value="y"),
):
decision = _check_mcp_project_trust(trust_flag=False)
assert decision is True
trust_project_mcp.assert_not_called()
captured = capsys.readouterr()
assert "debug-project-mcp" in captured.err
assert "Learn more:" in captured.err
def test_prompt_includes_docs_link(
self, capsys: pytest.CaptureFixture[str], tmp_path: Path
) -> None:
"""When the prompt fires, it should print the project-level-trust docs URL."""
from deepagents_cli.main import _check_mcp_project_trust
project_root = tmp_path / "proj"
project_root.mkdir()
project_cfg = project_root / ".mcp.json"
project_cfg.write_text("{}")
project_context = SimpleNamespace(
project_root=project_root, user_cwd=project_root
)
with (
patch(
"deepagents_cli.project_utils.ProjectContext.from_user_cwd",
return_value=project_context,
),
patch(
"deepagents_cli.mcp_tools.discover_mcp_configs",
return_value=[project_cfg],
),
patch(
"deepagents_cli.mcp_tools.classify_discovered_configs",
return_value=([], [project_cfg]),
),
patch(
"deepagents_cli.mcp_tools.load_mcp_config_lenient",
return_value={
"mcpServers": {"fs": {"command": "node", "args": ["server.js"]}}
},
),
patch(
"deepagents_cli.mcp_tools.extract_project_server_summaries",
return_value=[("fs", "stdio", "node server.js")],
),
patch(
"deepagents_cli.mcp_trust.is_project_mcp_trusted",
return_value=False,
),
patch("builtins.input", return_value="n"),
):
decision = _check_mcp_project_trust(trust_flag=False)
assert decision is False
captured = capsys.readouterr()
flattened = captured.err.replace("\n", "")
assert (
"https://docs.langchain.com/oss/python/deepagents/cli/"
"mcp-tools#project-level-trust" in flattened
)
assert "Learn more:" in captured.err
class TestCheckMcpProjectTrustDedupe:
"""Regression tests for the project MCP approval prompt deduplication.