diff --git a/cecli/args.py b/cecli/args.py index 6d26d53bd13..2a8b9ebeea8 100644 --- a/cecli/args.py +++ b/cecli/args.py @@ -14,7 +14,7 @@ MarkdownHelpFormatter, YamlHelpFormatter, ) -from cecli.deprecated_args import add_deprecated_model_args +from cecli.deprecated_args import add_deprecated_mcp_args, add_deprecated_model_args from .dump import dump # noqa: F401 @@ -364,10 +364,11 @@ def get_parser(default_config_files, git_root): default=None, ) group.add_argument( - "--mcp-servers-file", - metavar="MCP_CONFIG_FILE", - help="Specify a file path with MCP server configurations", - default=None, + "--mcp-servers-files", + metavar="MCP_CONFIG_FILES", + help="Specify a file path with MCP server configurations (can be specified multiple times)", + action="append", + default=[], ) group.add_argument( "--mcp-transport", @@ -1089,6 +1090,9 @@ def get_parser(default_config_files, git_root): # Add deprecated model shortcut arguments add_deprecated_model_args(parser, group) + group = parser.add_argument_group("Deprecated agent settings") + add_deprecated_mcp_args(parser, group) + return parser diff --git a/cecli/deprecated_args.py b/cecli/deprecated_args.py index e5b3986f31f..ca80605fa8b 100644 --- a/cecli/deprecated_args.py +++ b/cecli/deprecated_args.py @@ -142,6 +142,17 @@ def add_deprecated_model_args(parser, group): ) +def add_deprecated_mcp_args(parser, group): + """Add deprecated mcp arguments to the argparse parser.""" + group.add_argument( + "--mcp-servers-file", + help=argparse.SUPPRESS, + action="append", + dest="mcp_servers_file_deprecated", + env_var="CECLI_MCP_SERVERS_FILE", + ) + + def handle_deprecated_model_args(args, io): """Handle deprecated model shortcut arguments and provide appropriate warnings.""" # Define model mapping diff --git a/cecli/main.py b/cecli/main.py index 2fea8b4946a..b9fc425509b 100644 --- a/cecli/main.py +++ b/cecli/main.py @@ -1065,8 +1065,17 @@ def apply_model_overrides(model_name): # Default since some models do not have max_input_tokens specified somehow args.context_compaction_max_tokens = 65536 try: + if getattr(args, "mcp_servers_file_deprecated", None): + io.tool_warning( + "The --mcp-servers-file argument is deprecated and will be removed in a future" + " version. Please use --mcp-servers-files instead." + ) + if not args.mcp_servers_files: + args.mcp_servers_files = [] + args.mcp_servers_files.extend(args.mcp_servers_file_deprecated) + mcp_servers = load_mcp_servers( - args.mcp_servers, args.mcp_servers_file, io, args.verbose, args.mcp_transport + args.mcp_servers, args.mcp_servers_files, io, args.verbose, args.mcp_transport ) mcp_manager = await McpServerManager.from_servers(mcp_servers, io, args.verbose) # Load hooks if specified diff --git a/cecli/mcp/utils.py b/cecli/mcp/utils.py index 0bfc919f991..a0e2fbe5a80 100644 --- a/cecli/mcp/utils.py +++ b/cecli/mcp/utils.py @@ -149,7 +149,7 @@ def _parse_mcp_servers_from_file(file_path, io, verbose=False, mcp_transport="st def load_mcp_servers( - mcp_servers, mcp_servers_file, io, verbose=False, mcp_transport="stdio" + mcp_servers, mcp_servers_files, io, verbose=False, mcp_transport="stdio" ) -> list["McpServer"]: """Load MCP servers from a JSON string or file.""" servers = [] @@ -160,9 +160,14 @@ def load_mcp_servers( if servers: return servers - # If JSON string failed or wasn't provided, try the file - if mcp_servers_file: - servers = _parse_mcp_servers_from_file(mcp_servers_file, io, verbose, mcp_transport) + # If JSON string failed or wasn't provided, try the files + if mcp_servers_files: + servers = [] + for mcp_servers_file in mcp_servers_files: + file_servers = _parse_mcp_servers_from_file( + mcp_servers_file, io, verbose, mcp_transport + ) + servers.extend(file_servers) if not servers: # A default MCP server is actually now necessary for the overall agentic loop diff --git a/tests/basic/test_main.py b/tests/basic/test_main.py index 3fa6730c035..4bd8456e842 100644 --- a/tests/basic/test_main.py +++ b/tests/basic/test_main.py @@ -1373,7 +1373,7 @@ def test_mcp_servers_parsing(dummy_io, git_temp_dir, mocker): mcp_file = Path("mcp_servers.json") mcp_content = {"mcpServers": {"git": {"command": "uvx", "args": ["mcp-server-git"]}}} mcp_file.write_text(json.dumps(mcp_content)) - main(["--mcp-servers-file", str(mcp_file), "--exit", "--yes-always"], **dummy_io) + main(["--mcp-servers-files", str(mcp_file), "--exit", "--yes-always"], **dummy_io) mock_coder_create.assert_called_once() _, kwargs = mock_coder_create.call_args @@ -1381,3 +1381,43 @@ def test_mcp_servers_parsing(dummy_io, git_temp_dir, mocker): assert kwargs["mcp_manager"] is not None assert len(kwargs["mcp_manager"].servers) > 0 assert hasattr(kwargs["mcp_manager"].servers[0], "name") + + +def test_mcp_servers_file_multiple(dummy_io, git_temp_dir, mocker): + mocker.patch("cecli.mcp.server.McpServer.connect", new_callable=AsyncMock) + mock_coder_create = mocker.patch("cecli.coders.Coder.create") + mock_coder_instance = MagicMock() + mock_coder_instance.mcp_manager = False + mock_coder_instance._autosave_future = mock_autosave_future() + mock_coder_create.return_value = mock_coder_instance + + mcp_file1 = Path("mcp_servers1.json") + mcp_content1 = {"mcpServers": {"server1": {"command": "cmd1"}}} + mcp_file1.write_text(json.dumps(mcp_content1)) + + mcp_file2 = Path("mcp_servers2.json") + mcp_content2 = {"mcpServers": {"server2": {"command": "cmd2"}}} + mcp_file2.write_text(json.dumps(mcp_content2)) + + main( + [ + "--mcp-servers-files", + str(mcp_file1), + "--mcp-servers-files", + str(mcp_file2), + "--exit", + "--yes-always", + ], + **dummy_io, + ) + + mock_coder_create.assert_called_once() + _, kwargs = mock_coder_create.call_args + + assert "mcp_manager" in kwargs + mcp_manager = kwargs["mcp_manager"] + assert mcp_manager is not None + assert len(mcp_manager.servers) == 2 + server_names = {server.name for server in mcp_manager.servers} + assert "server1" in server_names + assert "server2" in server_names