mirror of
https://github.com/langchain-ai/deepagents.git
synced 2026-07-22 01:35:28 -04:00
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user