feat(#88): detect & migrate removed CLI flags into env (ssh-mcp v2)
CI / Lint (ruff) (pull_request) Successful in 11s
CI / Tests (py3.12 / windows-latest) (pull_request) Successful in 38s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 12s
CI / Catalog signature (pull_request) Successful in 8s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 12s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 12s

ssh-mcp v2 removed --password from the command line and reads
SSH_MCP_PASSWORD instead, so an old config crashes on startup. Add a
data-driven FLAG_ENV_MIGRATIONS registry plus detect_migratable_package,
migrate_removed_flags and removed_flag_warnings in bcc_core, and a
'Fix: move to environment variables' one-click action + warning in the
stdio server editor, with a matching main-window lint line. The literal
value lands in env{} (the only form Claude Desktop honours). 14 tests.
This commit is contained in:
the_og
2026-08-11 16:02:11 +00:00
committed by claude
parent 072a5cdc08
commit 7368dcdbff
3 changed files with 395 additions and 0 deletions
+147
View File
@@ -3059,3 +3059,150 @@ def test_discover_project_configs_skips_non_object_and_garbage(tmp_path):
assert str(array / ".mcp.json") not in paths
assert str(garbage / ".mcp.json") not in paths
assert str(missing / ".mcp.json") not in paths
# --------------------------------------------------------------------------- #
# Removed-flag -> env migration (ssh-mcp v2 and the general mechanism)
# --------------------------------------------------------------------------- #
def test_detect_migratable_package_via_npx_args():
data = {"command": "npx", "args": ["-y", "ssh-mcp", "--host=h", "--password=p"]}
assert c.detect_migratable_package(data) == "ssh-mcp"
def test_detect_migratable_package_direct_command_and_path_and_version():
assert c.detect_migratable_package({"command": "ssh-mcp", "args": []}) == "ssh-mcp"
assert (
c.detect_migratable_package({"command": "/usr/local/bin/ssh-mcp", "args": []}) == "ssh-mcp"
)
assert (
c.detect_migratable_package({"command": "npx", "args": ["-y", "ssh-mcp@2.0.1"]})
== "ssh-mcp"
)
def test_detect_migratable_package_unknown_returns_none():
assert c.detect_migratable_package({"command": "npx", "args": ["some-other"]}) is None
assert c.detect_migratable_package({}) is None
assert c.detect_migratable_package({"url": "https://x"}) is None
def test_migrate_removed_flags_inline_form():
data = {
"command": "npx",
"args": ["-y", "ssh-mcp", "--", "--host=1.2.3.4", "--password=hunter2"],
}
new, notes = c.migrate_removed_flags(data)
assert new["args"] == ["-y", "ssh-mcp", "--", "--host=1.2.3.4"]
assert new["env"] == {"SSH_MCP_PASSWORD": "hunter2"}
assert any("SSH_MCP_PASSWORD" in n for n in notes)
# Original untouched (pure function).
assert "env" not in data
def test_migrate_removed_flags_separate_form():
data = {
"command": "npx",
"args": ["-y", "ssh-mcp", "--user", "root", "--password", "s3cret"],
}
new, _ = c.migrate_removed_flags(data)
assert new["args"] == ["-y", "ssh-mcp", "--user", "root"]
assert new["env"] == {"SSH_MCP_PASSWORD": "s3cret"}
def test_migrate_removed_flags_preserves_existing_env_and_merges():
data = {
"command": "npx",
"args": ["ssh-mcp", "--password=p"],
"env": {"OTHER": "keep"},
}
new, _ = c.migrate_removed_flags(data)
assert new["env"] == {"OTHER": "keep", "SSH_MCP_PASSWORD": "p"}
def test_migrate_removed_flags_does_not_clobber_existing_secret():
data = {
"command": "npx",
"args": ["ssh-mcp", "--password=fromargs"],
"env": {"SSH_MCP_PASSWORD": "fromenv"},
}
new, notes = c.migrate_removed_flags(data)
# env value wins; the redundant flag is still stripped so v2 can start.
assert new["env"] == {"SSH_MCP_PASSWORD": "fromenv"}
assert "--password=fromargs" not in new["args"]
assert any("already set" in n for n in notes)
def test_migrate_removed_flags_moves_env_ref_verbatim():
data = {"command": "npx", "args": ["ssh-mcp", "--password", "${MY_PW}"]}
new, _ = c.migrate_removed_flags(data)
assert new["env"] == {"SSH_MCP_PASSWORD": "${MY_PW}"}
assert "${MY_PW}" not in new["args"]
def test_migrate_removed_flags_noop_returns_same_object():
data = {"command": "npx", "args": ["ssh-mcp", "--host=h", "--user=u"]}
new, notes = c.migrate_removed_flags(data)
assert new is data
assert notes == []
def test_migrate_removed_flags_unknown_package_noop():
data = {"command": "npx", "args": ["mystery", "--password=p"]}
new, notes = c.migrate_removed_flags(data)
assert new is data and notes == []
def test_migrate_removed_flags_only_args_left_drops_args_key():
data = {"command": "ssh-mcp", "args": ["--password=p"]}
new, _ = c.migrate_removed_flags(data)
assert "args" not in new
assert new["env"] == {"SSH_MCP_PASSWORD": "p"}
def test_removed_flag_warnings_covers_migratable_and_manual():
data = {
"command": "npx",
"args": ["ssh-mcp", "--password=p", "--sudoPassword=s", "--disableSudo"],
}
warnings = c.removed_flag_warnings(data)
text = " ".join(warnings)
assert "--password" in text
assert "sudoPassword" in text
assert "disableSudo" in text
# migrate only touches the confirmed --password mapping
new, _ = c.migrate_removed_flags(data)
assert new["env"] == {"SSH_MCP_PASSWORD": "p"}
assert "--sudoPassword=s" in new["args"]
assert "--disableSudo" in new["args"]
def test_removed_flag_warnings_empty_when_clean():
assert c.removed_flag_warnings({"command": "ssh-mcp", "args": ["--host=h"]}) == []
assert c.removed_flag_warning({"command": "ssh-mcp", "args": ["--host=h"]}) is None
def test_ssh_membermatters_end_to_end():
# The exact shape from AJ's failing server.
data = {
"command": "npx",
"args": [
"-y",
"ssh-mcp",
"--",
"--host=member.example.io",
"--user=deploy",
"--port=22",
"--password=topsecret",
],
}
new, notes = c.migrate_removed_flags(data)
assert new["args"] == [
"-y",
"ssh-mcp",
"--",
"--host=member.example.io",
"--user=deploy",
"--port=22",
]
assert new["env"] == {"SSH_MCP_PASSWORD": "topsecret"}
assert notes