Skip to content

Commit bce8619

Browse files
niechenclaude
andcommitted
test: add comprehensive non-interactive tests for AI-agent CLI commands
- Add non-interactive test coverage for mcpm edit, profile edit, profile inspect, client edit, and config set commands - Fix exit code handling in commands to properly use sys.exit() for non-interactive mode - Fix remote server environment variable validation in non_interactive.py - Update existing tests to match corrected command behavior - Ensure all AI-agent friendly CLI commands have proper test coverage 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent eb40511 commit bce8619

File tree

11 files changed

+1153
-31
lines changed

11 files changed

+1153
-31
lines changed

llm.txt

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# MCPM (Model Context Protocol Manager) - AI Agent Guide
22

3-
Generated: 2025-07-21 19:18:51 UTC
3+
Generated: 2025-07-22 11:32:59 UTC
44
Version: 2.5.0
55

66
## Overview
@@ -151,14 +151,21 @@ mcpm config <arguments>
151151

152152
Set MCPM configuration.
153153

154-
Example:
154+
Interactive by default, or use CLI parameters for automation.
155+
Use --key and --value to set configuration non-interactively.
156+
157+
Examples:
155158

156159

157-
mcpm config set
160+
mcpm config set # Interactive mode
161+
mcpm config set --key node_executable --value npx # Non-interactive mode
158162

159163

160164
**Parameters:**
161165

166+
- `--key`: Configuration key to set
167+
- `--value`: Configuration value to set
168+
- `--force`: Skip confirmation prompts (flag)
162169
- `-h`, `--help`: Show this message and exit. (flag)
163170

164171
**Examples:**

src/mcpm/commands/client.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import json
66
import os
77
import subprocess
8+
import sys
89

910
from InquirerPy import inquirer
1011
from InquirerPy.base.control import Choice
@@ -265,7 +266,7 @@ def edit_client(client_name, external, config_path_override, add_server, remove_
265266
force_non_interactive = is_non_interactive() or should_force_operation() or force
266267

267268
if has_cli_params or force_non_interactive:
268-
return _edit_client_non_interactive(
269+
exit_code = _edit_client_non_interactive(
269270
client_manager=client_manager,
270271
client_name=client_name,
271272
display_name=display_name,
@@ -278,6 +279,7 @@ def edit_client(client_name, external, config_path_override, add_server, remove_
278279
set_profiles=set_profiles,
279280
force=force,
280281
)
282+
sys.exit(exit_code)
281283

282284
# If external editor requested, handle that directly
283285
if external:

src/mcpm/commands/config.py

Lines changed: 91 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
"""Config command for MCPM - Manage MCPM configuration"""
22

33
import os
4+
import sys
45

56
from rich.console import Console
67
from rich.prompt import Prompt
78

89
from mcpm.utils.config import NODE_EXECUTABLES, ConfigManager
10+
from mcpm.utils.non_interactive import is_non_interactive, should_force_operation
911
from mcpm.utils.repository import RepositoryManager
1012
from mcpm.utils.rich_click_config import click
1113

@@ -24,23 +26,102 @@ def config():
2426

2527

2628
@config.command()
29+
@click.option("--key", help="Configuration key to set")
30+
@click.option("--value", help="Configuration value to set")
31+
@click.option("--force", is_flag=True, help="Skip confirmation prompts")
2732
@click.help_option("-h", "--help")
28-
def set():
33+
def set(key, value, force):
2934
"""Set MCPM configuration.
3035
31-
Example:
36+
Interactive by default, or use CLI parameters for automation.
37+
Use --key and --value to set configuration non-interactively.
38+
39+
Examples:
3240
3341
\b
34-
mcpm config set
42+
mcpm config set # Interactive mode
43+
mcpm config set --key node_executable --value npx # Non-interactive mode
3544
"""
36-
set_key = Prompt.ask("Configuration key to set", choices=["node_executable"], default="node_executable")
37-
node_executable = Prompt.ask(
38-
"Select default node executable, it will be automatically applied when adding npx server with mcpm add",
39-
choices=NODE_EXECUTABLES,
40-
)
4145
config_manager = ConfigManager()
42-
config_manager.set_config(set_key, node_executable)
43-
console.print(f"[green]Default node executable set to:[/] {node_executable}")
46+
47+
# Check if we have CLI parameters for non-interactive mode
48+
has_cli_params = key is not None and value is not None
49+
force_non_interactive = is_non_interactive() or should_force_operation() or force
50+
51+
if has_cli_params or force_non_interactive:
52+
exit_code = _set_config_non_interactive(
53+
config_manager=config_manager,
54+
key=key,
55+
value=value,
56+
force=force
57+
)
58+
sys.exit(exit_code)
59+
60+
# Interactive mode
61+
set_key = Prompt.ask("Configuration key to set", choices=["node_executable"], default="node_executable")
62+
63+
if set_key == "node_executable":
64+
node_executable = Prompt.ask(
65+
"Select default node executable, it will be automatically applied when adding npx server with mcpm add",
66+
choices=NODE_EXECUTABLES,
67+
)
68+
config_manager.set_config(set_key, node_executable)
69+
console.print(f"[green]Default node executable set to:[/] {node_executable}")
70+
else:
71+
console.print(f"[red]Error: Unknown configuration key '{set_key}'[/]")
72+
73+
74+
def _set_config_non_interactive(config_manager, key=None, value=None, force=False):
75+
"""Set configuration non-interactively."""
76+
try:
77+
# Define supported configuration keys and their valid values
78+
SUPPORTED_KEYS = {
79+
"node_executable": {
80+
"valid_values": NODE_EXECUTABLES,
81+
"description": "Default node executable for npx servers"
82+
}
83+
}
84+
85+
# Validate that both key and value are provided in non-interactive mode
86+
if not key or not value:
87+
console.print("[red]Error: Both --key and --value are required in non-interactive mode[/]")
88+
console.print("[dim]Use 'mcpm config set' for interactive mode[/]")
89+
return 1
90+
91+
# Validate the configuration key
92+
if key not in SUPPORTED_KEYS:
93+
console.print(f"[red]Error: Unknown configuration key '{key}'[/]")
94+
console.print("[yellow]Supported keys:[/]")
95+
for supported_key, info in SUPPORTED_KEYS.items():
96+
console.print(f" • [cyan]{supported_key}[/] - {info['description']}")
97+
return 1
98+
99+
# Validate the value for the specific key
100+
key_info = SUPPORTED_KEYS[key]
101+
if "valid_values" in key_info and value not in key_info["valid_values"]:
102+
console.print(f"[red]Error: Invalid value '{value}' for key '{key}'[/]")
103+
console.print(f"[yellow]Valid values for '{key}':[/]")
104+
for valid_value in key_info["valid_values"]:
105+
console.print(f" • [cyan]{valid_value}[/]")
106+
return 1
107+
108+
# Display what will be set
109+
console.print("[bold green]Setting configuration:[/]")
110+
console.print(f"Key: [cyan]{key}[/]")
111+
console.print(f"Value: [cyan]{value}[/]")
112+
113+
# Set the configuration
114+
success = config_manager.set_config(key, value)
115+
if success:
116+
console.print(f"[green]✅ Configuration '{key}' set to '{value}'[/]")
117+
return 0
118+
else:
119+
console.print(f"[red]Error: Failed to set configuration '{key}'[/]")
120+
return 1
121+
122+
except Exception as e:
123+
console.print(f"[red]Error setting configuration: {e}[/]")
124+
return 1
44125

45126

46127
@config.command()

src/mcpm/commands/new.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
New command - Create new server configurations with interactive and non-interactive modes
33
"""
44

5+
import sys
56
from typing import Optional
67

78
from rich.console import Console
@@ -50,7 +51,7 @@ def new(
5051
force_non_interactive = is_non_interactive() or should_force_operation() or force
5152

5253
if has_cli_params or force_non_interactive:
53-
return _create_new_server_non_interactive(
54+
exit_code = _create_new_server_non_interactive(
5455
server_name=server_name,
5556
server_type=server_type,
5657
command=command,
@@ -60,6 +61,7 @@ def new(
6061
headers=headers,
6162
force=force,
6263
)
64+
sys.exit(exit_code)
6365
else:
6466
# Fall back to interactive mode
6567
return _create_new_server()
@@ -120,7 +122,6 @@ def _create_new_server_non_interactive(
120122
name=config_dict["name"],
121123
url=config_dict["url"],
122124
headers=config_dict.get("headers", {}),
123-
env=config_dict.get("env", {}),
124125
)
125126

126127
# Display configuration summary
@@ -137,7 +138,7 @@ def _create_new_server_non_interactive(
137138
headers_str = ", ".join(f"{k}={v}" for k, v in server_config.headers.items())
138139
console.print(f"Headers: [cyan]{headers_str}[/]")
139140

140-
if server_config.env:
141+
if hasattr(server_config, "env") and server_config.env:
141142
env_str = ", ".join(f"{k}={v}" for k, v in server_config.env.items())
142143
console.print(f"Environment: [cyan]{env_str}[/]")
143144

src/mcpm/commands/profile/edit.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""Profile edit command."""
22

3+
import sys
4+
35
from rich.console import Console
46

57
from mcpm.global_config import GlobalConfigManager
@@ -37,14 +39,14 @@ def edit_profile(profile_name, name, servers, add_server, remove_server, set_ser
3739
console.print("[yellow]Available options:[/]")
3840
console.print(" • Run 'mcpm profile ls' to see available profiles")
3941
console.print(" • Run 'mcpm profile create {name}' to create a profile")
40-
return 1
42+
sys.exit(1)
4143

4244
# Detect if this is non-interactive mode
4345
has_cli_params = any([name, servers, add_server, remove_server, set_servers])
4446
force_non_interactive = is_non_interactive() or should_force_operation() or force
4547

4648
if has_cli_params or force_non_interactive:
47-
return _edit_profile_non_interactive(
49+
exit_code = _edit_profile_non_interactive(
4850
profile_name=profile_name,
4951
new_name=name,
5052
servers=servers,
@@ -53,6 +55,7 @@ def edit_profile(profile_name, name, servers, add_server, remove_server, set_ser
5355
set_servers=set_servers,
5456
force=force,
5557
)
58+
sys.exit(exit_code)
5659

5760
else:
5861
# Interactive mode using InquirerPy

src/mcpm/utils/non_interactive.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -238,14 +238,16 @@ def create_server_config_from_params(
238238
config["command"] = command
239239
if args:
240240
config["args"] = args.split()
241+
# Add environment variables if provided (stdio servers only)
242+
if env:
243+
config["env"] = parse_key_value_pairs(env)
241244
elif server_type == "remote":
242245
config["url"] = url
243246
if headers:
244247
config["headers"] = parse_header_pairs(headers)
245-
246-
# Add environment variables if provided
247-
if env:
248-
config["env"] = parse_key_value_pairs(env)
248+
# Remote servers don't support environment variables
249+
if env:
250+
raise ValueError("Environment variables are not supported for remote servers")
249251

250252
return config
251253

0 commit comments

Comments
 (0)