Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 12 additions & 13 deletions src/mcpm/commands/edit.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"""

import os
import shlex
import subprocess
import sys
from typing import Any, Dict, Optional
Expand Down Expand Up @@ -80,7 +81,7 @@ def edit(server_name, new, editor):

if isinstance(server_config, STDIOServerConfig):
table.add_row("Command", server_config.command)
table.add_row("Arguments", ", ".join(server_config.args) if server_config.args else "[dim]None[/]")
table.add_row("Arguments", " ".join(server_config.args) if server_config.args else "[dim]None[/]")
table.add_row(
"Environment",
", ".join(f"{k}={v}" for k, v in server_config.env.items()) if server_config.env else "[dim]None[/]",
Expand Down Expand Up @@ -187,12 +188,12 @@ def interactive_server_edit(server_config) -> Optional[Dict[str, Any]]:
keybindings={"interrupt": [{"key": "escape"}]},
).execute()

# Arguments as comma-separated string
current_args = ", ".join(server_config.args) if server_config.args else ""
# Arguments as space-separated string
current_args = " ".join(server_config.args) if server_config.args else ""
answers["args"] = inquirer.text(
message="Arguments (comma-separated):",
message="Arguments (space-separated, quotes supported):",
default=current_args,
instruction="(Leave empty for no arguments)",
instruction="(Leave empty for no arguments, use quotes for args with spaces)",
keybindings={"interrupt": [{"key": "escape"}]},
).execute()

Expand Down Expand Up @@ -237,7 +238,7 @@ def interactive_server_edit(server_config) -> Optional[Dict[str, Any]]:

if isinstance(server_config, STDIOServerConfig):
console.print(f"Command: [cyan]{server_config.command}[/] → [cyan]{answers['command']}[/]")
new_args = [arg.strip() for arg in answers["args"].split(",") if arg.strip()] if answers["args"] else []
new_args = shlex.split(answers["args"]) if answers["args"] else []
console.print(f"Arguments: [cyan]{server_config.args}[/] → [cyan]{new_args}[/]")

new_env = {}
Expand Down Expand Up @@ -298,7 +299,7 @@ def apply_interactive_changes(server_config, interactive_result):

# Parse arguments
if answers["args"].strip():
server_config.args = [arg.strip() for arg in answers["args"].split(",") if arg.strip()]
server_config.args = shlex.split(answers["args"])
else:
server_config.args = []

Expand Down Expand Up @@ -381,9 +382,7 @@ def _create_new_server():
server_config = STDIOServerConfig(
name=server_name,
command=result["answers"]["command"],
args=[arg.strip() for arg in result["answers"]["args"].split(",") if arg.strip()]
if result["answers"]["args"]
else [],
args=shlex.split(result["answers"]["args"]) if result["answers"]["args"] else [],
env={},
)

Expand Down Expand Up @@ -462,8 +461,8 @@ def _interactive_new_server_form() -> Optional[Dict[str, Any]]:
).execute()

answers["args"] = inquirer.text(
message="Arguments (comma-separated):",
instruction="(Leave empty for no arguments)",
message="Arguments (space-separated, quotes supported):",
instruction="(Leave empty for no arguments, use quotes for args with spaces)",
keybindings={"interrupt": [{"key": "escape"}]},
).execute()

Expand Down Expand Up @@ -497,7 +496,7 @@ def _interactive_new_server_form() -> Optional[Dict[str, Any]]:

if answers["type"] == "stdio":
console.print(f"Command: [cyan]{answers['command']}[/]")
new_args = [arg.strip() for arg in answers["args"].split(",") if arg.strip()] if answers["args"] else []
new_args = shlex.split(answers["args"]) if answers["args"] else []
console.print(f"Arguments: [cyan]{new_args}[/]")

new_env = {}
Expand Down
52 changes: 51 additions & 1 deletion tests/test_edit.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Tests for the edit command
"""

import shlex
from unittest.mock import Mock

from click.testing import CliRunner
Expand Down Expand Up @@ -44,13 +45,39 @@ def test_edit_server_interactive_fallback(monkeypatch):
assert result.exit_code == 0 # CliRunner may not properly handle our return codes
assert "Current Configuration for 'test-server'" in result.output
assert "test-cmd" in result.output
assert "arg1, arg2" in result.output
assert "arg1 arg2" in result.output
assert "KEY=value" in result.output
assert "test-profile" in result.output
assert "Interactive editing not available" in result.output
assert "This command requires a terminal for interactive input" in result.output


def test_edit_server_with_spaces_in_args(monkeypatch):
"""Test display of arguments with spaces in the fallback view."""
test_server = STDIOServerConfig(
name="test-server",
command="test-cmd",
args=["arg with spaces", "another arg", "--flag=value with spaces"],
env={"KEY": "value"},
profile_tags=["test-profile"],
)

mock_global_config = Mock()
mock_global_config.get_server.return_value = test_server
monkeypatch.setattr("mcpm.commands.edit.global_config_manager", mock_global_config)

runner = CliRunner()
result = runner.invoke(edit, ["test-server"])

# In test environment, interactive mode falls back and shows message
assert result.exit_code == 0
assert "Current Configuration for 'test-server'" in result.output
assert "test-cmd" in result.output
# Check that arguments with spaces are displayed correctly
assert "arg with spaces another arg --flag=value with spaces" in result.output
assert "Interactive editing not available" in result.output


def test_edit_command_help():
"""Test the edit command help output."""
runner = CliRunner()
Expand Down Expand Up @@ -94,3 +121,26 @@ def test_edit_editor_flag(monkeypatch):

# Verify subprocess.run was called with correct arguments
mock_subprocess.assert_called_once_with(["open", "/tmp/test_servers.json"])


def test_shlex_argument_parsing():
"""Test that shlex correctly parses arguments with spaces."""
# Test basic space-separated arguments
result = shlex.split("arg1 arg2 arg3")
assert result == ["arg1", "arg2", "arg3"]

# Test quoted arguments with spaces
result = shlex.split('arg1 "arg with spaces" arg3')
assert result == ["arg1", "arg with spaces", "arg3"]

# Test mixed quotes
result = shlex.split("arg1 'arg with spaces' --flag=\"value with spaces\"")
assert result == ["arg1", "arg with spaces", "--flag=value with spaces"]

# Test empty string
result = shlex.split("")
assert result == []

# Test single argument with spaces
result = shlex.split('"single arg with spaces"')
assert result == ["single arg with spaces"]