diff --git a/schema/mcp-agent.config.schema.json b/schema/mcp-agent.config.schema.json index 946ce15ee..18b388a2e 100644 --- a/schema/mcp-agent.config.schema.json +++ b/schema/mcp-agent.config.schema.json @@ -358,6 +358,18 @@ "default": false, "title": "Vertexai", "type": "boolean" + }, + "default_model": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default Model" } }, "title": "GoogleSettings", @@ -774,95 +786,6 @@ "title": "MCPSettings", "type": "object" }, - "OTelConsoleExporterSettings": { - "additionalProperties": true, - "properties": { - "type": { - "const": "console", - "default": "console", - "title": "Type", - "type": "string" - } - }, - "title": "OTelConsoleExporterSettings", - "type": "object" - }, - "OTelFileExporterSettings": { - "additionalProperties": true, - "properties": { - "type": { - "const": "file", - "default": "file", - "title": "Type", - "type": "string" - }, - "path": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Path" - }, - "path_settings": { - "anyOf": [ - { - "$ref": "#/$defs/TracePathSettings" - }, - { - "type": "null" - } - ], - "default": null - } - }, - "title": "OTelFileExporterSettings", - "type": "object" - }, - "OTelOTLPExporterSettings": { - "additionalProperties": true, - "properties": { - "type": { - "const": "otlp", - "default": "otlp", - "title": "Type", - "type": "string" - }, - "endpoint": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Endpoint" - }, - "headers": { - "anyOf": [ - { - "additionalProperties": { - "type": "string" - }, - "type": "object" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Headers" - } - }, - "title": "OTelOTLPExporterSettings", - "type": "object" - }, "OpenAISettings": { "additionalProperties": true, "description": "Settings for using OpenAI models in the MCP Agent application.", @@ -956,28 +879,16 @@ "exporters": { "default": [], "items": { - "discriminator": { - "mapping": { - "console": "#/$defs/OTelConsoleExporterSettings", - "file": "#/$defs/OTelFileExporterSettings", - "otlp": "#/$defs/OTelOTLPExporterSettings" - }, - "propertyName": "type" - }, - "oneOf": [ - { - "$ref": "#/$defs/OTelConsoleExporterSettings" - }, - { - "$ref": "#/$defs/OTelFileExporterSettings" - }, - { - "$ref": "#/$defs/OTelOTLPExporterSettings" - } - ] + "enum": [ + "console", + "file", + "otlp" + ], + "type": "string" }, "title": "Exporters", - "type": "array" + "type": "array", + "description": "List of exporters to use (can enable multiple simultaneously)" }, "service_name": { "default": "mcp-agent", @@ -1024,7 +935,7 @@ } ], "default": null, - "description": "Deprecated single OTLP settings. Prefer exporters list with type \"otlp\"." + "description": "OTLP settings for OpenTelemetry tracing. Required if using otlp exporter." }, "path": { "anyOf": [ @@ -1268,6 +1179,32 @@ "additionalProperties": true, "description": "Configuration schema for MCP Agent applications", "properties": { + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Name", + "description": "The name of the MCP application" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Description", + "description": "The description of the MCP application" + }, "mcp": { "anyOf": [ { diff --git a/src/mcp_agent/app.py b/src/mcp_agent/app.py index fa8946cff..2ada0483b 100644 --- a/src/mcp_agent/app.py +++ b/src/mcp_agent/app.py @@ -101,10 +101,6 @@ def __init__( initialize_model_selector: Initializes the built-in ModelSelector to help with model selection. Defaults to False. """ self.mcp = mcp - self.name = name or (mcp.name if mcp else None) - self.description = description or ( - mcp.instructions if mcp else "MCP Agent Application" - ) # We use these to initialize the context in initialize() if settings is None: @@ -114,6 +110,14 @@ def __init__( else: self._config = settings + self.name = name or self._config.name or (mcp.name if mcp else None) + + self.description = ( + description + or self._config.description + or (mcp.instructions if mcp else "MCP Agent Application") + ) + # We initialize the task and decorator registries at construction time # (prior to initializing the context) to ensure that they are available # for any decorators that are applied to the workflow or task methods. diff --git a/src/mcp_agent/cli/cloud/commands/deploy/main.py b/src/mcp_agent/cli/cloud/commands/deploy/main.py index 19a7f6e37..e3ed10bb3 100644 --- a/src/mcp_agent/cli/cloud/commands/deploy/main.py +++ b/src/mcp_agent/cli/cloud/commands/deploy/main.py @@ -41,6 +41,7 @@ create_git_tag, sanitize_git_ref_component, ) +from mcp_agent.config import get_settings from .wrangler_wrapper import wrangler_deploy @@ -57,11 +58,22 @@ def deploy_config( "-d", help="Description of the MCP App being deployed.", ), - config_dir: Path = typer.Option( - Path(""), + config_dir: Optional[Path] = typer.Option( + None, "--config-dir", "-c", - help="Path to the directory containing the app config and app files.", + help="Path to the directory containing the app config and app files." + " If relative, it is resolved against --working-dir.", + readable=True, + dir_okay=True, + file_okay=False, + resolve_path=False, + ), + working_dir: Path = typer.Option( + Path("."), + "--working-dir", + "-w", + help="Working directory to resolve config and bundle files from. Defaults to the current directory.", exists=True, readable=True, dir_okay=True, @@ -126,12 +138,45 @@ def deploy_config( Returns: Newly-deployed MCP App ID """ - # Show help if no app_name is provided - if app_name is None: - typer.echo(ctx.get_help()) - raise typer.Exit() - try: + if config_dir is None: + resolved_config_dir = working_dir + elif config_dir.is_absolute(): + resolved_config_dir = config_dir + else: + resolved_config_dir = working_dir / config_dir + + if not resolved_config_dir.exists() or not resolved_config_dir.is_dir(): + raise CLIError( + f"Configuration directory '{resolved_config_dir}' does not exist or is not a directory.", + retriable=False, + ) + + config_dir = resolved_config_dir + + config_file, secrets_file, deployed_secrets_file = get_config_files(config_dir) + + default_app_name, default_app_description = _get_app_info_from_config( + config_file + ) + + if app_name is None: + if default_app_name: + print_info( + f"No app name provided. Using '{default_app_name}' from configuration." + ) + app_name = default_app_name + else: + app_name = "default" + print_info("No app name provided. Using 'default' as app name.") + + if app_description is None: + if default_app_description: + print_info( + "No app description provided. Using description from configuration." + ) + app_description = default_app_description + provided_key = api_key effective_api_url = api_url or settings.API_BASE_URL effective_api_key = ( @@ -174,7 +219,8 @@ def deploy_config( ) if not non_interactive: use_existing = typer.confirm( - f"Do you want deploy an update to the existing app ID: {app_id}?" + f"Do you want deploy an update to the existing app ID: {app_id}?", + default=True, ) if use_existing: print_info(f"Will deploy an update to app ID: {app_id}") @@ -195,9 +241,6 @@ def deploy_config( except Exception as e: raise CLIError(f"Error checking or creating app: {str(e)}") from e - # Validate config directory and required files - config_file, secrets_file, deployed_secrets_file = get_config_files(config_dir) - # If a deployed secrets file already exists, determine if it should be used or overwritten if deployed_secrets_file: if secrets_file: @@ -209,10 +252,11 @@ def deploy_config( "--non-interactive specified, using existing deployed secrets file without changes." ) else: - update = typer.confirm( - f"Do you want to update the existing '{MCP_DEPLOYED_SECRETS_FILENAME}' by re-processing '{MCP_SECRETS_FILENAME}'?" + reuse = typer.confirm( + f"Do you want to reuse the previously deployed secrets in '{MCP_DEPLOYED_SECRETS_FILENAME}'?", + default=True, ) - if update: + if not reuse: print_info( f"Will update existing '{MCP_DEPLOYED_SECRETS_FILENAME}' by re-processing '{MCP_SECRETS_FILENAME}'." ) @@ -443,3 +487,30 @@ def get_config_files(config_dir: Path) -> tuple[Path, Optional[Path], Optional[P deployed_secrets_file = deployed_secrets_path return config_file, secrets_file, deployed_secrets_file + + +def _get_app_info_from_config(config_file: Path) -> Optional[tuple[str, str]]: + """Return a default deployment name sourced from configuration if available.""" + + try: + loaded_settings = get_settings( + config_path=str(config_file), + set_global=False, + ) + except Exception: + return None, None + + app_name = ( + loaded_settings.name + if isinstance(loaded_settings.name, str) and loaded_settings.name.strip() + else None + ) + + app_description = ( + loaded_settings.description + if isinstance(loaded_settings.description, str) + and loaded_settings.description.strip() + else None + ) + + return app_name, app_description diff --git a/src/mcp_agent/config.py b/src/mcp_agent/config.py index 2d980b65b..5afd81a23 100644 --- a/src/mcp_agent/config.py +++ b/src/mcp_agent/config.py @@ -600,6 +600,12 @@ class Settings(BaseSettings): nested_model_default_partial_update=True, ) # Customize the behavior of settings here + name: str | None = None + """The name of the MCP application""" + + description: str | None = None + """The description of the MCP application""" + mcp: MCPSettings | None = Field(default_factory=MCPSettings) """MCP config, such as MCP servers""" diff --git a/tests/cli/commands/test_deploy_command.py b/tests/cli/commands/test_deploy_command.py index 5728d20cc..66ef882d4 100644 --- a/tests/cli/commands/test_deploy_command.py +++ b/tests/cli/commands/test_deploy_command.py @@ -142,6 +142,252 @@ async def mock_process_secrets(*args, **kwargs): assert "Transformed secrets file written to" in result.stdout +def test_deploy_defaults_to_configured_app_name(runner, temp_config_dir): + """Command should fall back to the config-defined name when none is provided.""" + + config_path = temp_config_dir / MCP_CONFIG_FILENAME + original_config = config_path.read_text() + config_path.write_text("name: fixture-app\n" + original_config) + + output_path = temp_config_dir / MCP_DEPLOYED_SECRETS_FILENAME + + async def mock_process_secrets(*args, **kwargs): + with open(kwargs.get("output_path", output_path), "w", encoding="utf-8") as f: + f.write("key: value\n") + return { + "deployment_secrets": [], + "user_secrets": [], + "reused_secrets": [], + "skipped_secrets": [], + } + + mock_client = AsyncMock() + mock_client.get_app_id_by_name.return_value = None + + mock_app = MagicMock() + mock_app.appId = MOCK_APP_ID + mock_client.create_app.return_value = mock_app + + with ( + patch( + "mcp_agent.cli.secrets.processor.process_config_secrets", + side_effect=mock_process_secrets, + ), + patch( + "mcp_agent.cli.cloud.commands.deploy.main.MCPAppClient", + return_value=mock_client, + ), + patch( + "mcp_agent.cli.cloud.commands.deploy.main.wrangler_deploy", + return_value=MOCK_APP_ID, + ), + ): + result = runner.invoke( + app, + [ + "deploy", + "--working-dir", + temp_config_dir, + "--api-url", + "http://test-api.com", + "--api-key", + "test-api-key", + "--non-interactive", + ], + ) + + assert result.exit_code == 0, f"Deploy command failed: {result.stdout}" + first_call = mock_client.get_app_id_by_name.await_args_list[0] + assert first_call.args[0] == "fixture-app" + + +def test_deploy_defaults_to_directory_name_when_config_missing_name( + runner, temp_config_dir +): + """Fallback uses the default name when config doesn't define one.""" + + config_path = temp_config_dir / MCP_CONFIG_FILENAME + original_config = config_path.read_text() + config_path.write_text(original_config) # ensure no name present + + secrets_path = temp_config_dir / MCP_SECRETS_FILENAME + if secrets_path.exists(): + secrets_path.unlink() + + output_path = temp_config_dir / MCP_DEPLOYED_SECRETS_FILENAME + + async def mock_process_secrets(*args, **kwargs): + with open(kwargs.get("output_path", output_path), "w", encoding="utf-8") as f: + f.write("key: value\n") + return { + "deployment_secrets": [], + "user_secrets": [], + "reused_secrets": [], + "skipped_secrets": [], + } + + mock_client = AsyncMock() + mock_client.get_app_id_by_name.return_value = None + + mock_app = MagicMock() + mock_app.appId = MOCK_APP_ID + mock_client.create_app.return_value = mock_app + + with ( + patch( + "mcp_agent.cli.secrets.processor.process_config_secrets", + side_effect=mock_process_secrets, + ), + patch( + "mcp_agent.cli.cloud.commands.deploy.main.MCPAppClient", + return_value=mock_client, + ), + patch( + "mcp_agent.cli.cloud.commands.deploy.main.wrangler_deploy", + return_value=MOCK_APP_ID, + ), + ): + result = runner.invoke( + app, + [ + "deploy", + "--working-dir", + temp_config_dir, + "--api-url", + "http://test-api.com", + "--api-key", + "test-api-key", + "--non-interactive", + ], + ) + + assert result.exit_code == 0, f"Deploy command failed: {result.stdout}" + first_call = mock_client.get_app_id_by_name.await_args_list[0] + assert first_call.args[0] == "default" + + +def test_deploy_uses_config_description_when_not_provided(runner, temp_config_dir): + """If CLI description is omitted, reuse the config-defined description.""" + + config_path = temp_config_dir / MCP_CONFIG_FILENAME + original_config = config_path.read_text() + config_path.write_text( + "description: Configured app description\n" + original_config + ) + + output_path = temp_config_dir / MCP_DEPLOYED_SECRETS_FILENAME + + async def mock_process_secrets(*args, **kwargs): + with open(kwargs.get("output_path", output_path), "w", encoding="utf-8") as f: + f.write("key: value\n") + return { + "deployment_secrets": [], + "user_secrets": [], + "reused_secrets": [], + "skipped_secrets": [], + } + + mock_client = AsyncMock() + mock_client.get_app_id_by_name.return_value = None + + mock_app = MagicMock() + mock_app.appId = MOCK_APP_ID + mock_client.create_app.return_value = mock_app + + with ( + patch( + "mcp_agent.cli.secrets.processor.process_config_secrets", + side_effect=mock_process_secrets, + ), + patch( + "mcp_agent.cli.cloud.commands.deploy.main.MCPAppClient", + return_value=mock_client, + ), + patch( + "mcp_agent.cli.cloud.commands.deploy.main.wrangler_deploy", + return_value=MOCK_APP_ID, + ), + ): + result = runner.invoke( + app, + [ + "deploy", + "--working-dir", + temp_config_dir, + "--api-url", + "http://test-api.com", + "--api-key", + "test-api-key", + "--non-interactive", + ], + ) + + assert result.exit_code == 0, f"Deploy command failed: {result.stdout}" + create_call = mock_client.create_app.await_args + assert create_call.kwargs["description"] == "Configured app description" + + +def test_deploy_uses_defaults_when_config_cannot_be_loaded(runner, temp_config_dir): + """If config parsing fails, fall back to default name and unset description.""" + + config_path = temp_config_dir / MCP_CONFIG_FILENAME + config_path.write_text("invalid: [\n") + + output_path = temp_config_dir / MCP_DEPLOYED_SECRETS_FILENAME + + async def mock_process_secrets(*args, **kwargs): + with open(kwargs.get("output_path", output_path), "w", encoding="utf-8") as f: + f.write("key: value\n") + return { + "deployment_secrets": [], + "user_secrets": [], + "reused_secrets": [], + "skipped_secrets": [], + } + + mock_client = AsyncMock() + mock_client.get_app_id_by_name.return_value = None + + mock_app = MagicMock() + mock_app.appId = MOCK_APP_ID + mock_client.create_app.return_value = mock_app + + with ( + patch( + "mcp_agent.cli.secrets.processor.process_config_secrets", + side_effect=mock_process_secrets, + ), + patch( + "mcp_agent.cli.cloud.commands.deploy.main.MCPAppClient", + return_value=mock_client, + ), + patch( + "mcp_agent.cli.cloud.commands.deploy.main.wrangler_deploy", + return_value=MOCK_APP_ID, + ), + ): + result = runner.invoke( + app, + [ + "deploy", + "--working-dir", + temp_config_dir, + "--api-url", + "http://test-api.com", + "--api-key", + "test-api-key", + "--non-interactive", + ], + ) + + assert result.exit_code == 0, f"Deploy command failed: {result.stdout}" + name_call = mock_client.get_app_id_by_name.await_args_list[0] + assert name_call.args[0] == "default" + + create_call = mock_client.create_app.await_args + assert create_call.kwargs.get("description") is None + + def test_deploy_with_secrets_file(): """Test the deploy command with a secrets file.""" # Create a temporary directory for test files @@ -206,9 +452,9 @@ def test_deploy_with_secrets_file(): # Verify secrets file is unchanged with open(secrets_path, "r", encoding="utf-8") as f: content = f.read() - assert content == secrets_content, ( - "Output file content should match original secrets" - ) + assert ( + content == secrets_content + ), "Output file content should match original secrets" # Verify the function deployed the correct mock app assert result == MOCK_APP_ID diff --git a/tests/cli/commands/test_wrangler_wrapper.py b/tests/cli/commands/test_wrangler_wrapper.py index c2b21a026..e5e503588 100644 --- a/tests/cli/commands/test_wrangler_wrapper.py +++ b/tests/cli/commands/test_wrangler_wrapper.py @@ -470,9 +470,9 @@ def check_files_during_subprocess(*args, **kwargs): # Temp directory should have .mcpac.py versions assert temp_mcpac_file.exists(), f"Temp {file_path}.mcpac.py should exist" # Original files in temp should be renamed away - assert not temp_original_file.exists(), ( - f"Temp {file_path} should be renamed" - ) + assert ( + not temp_original_file.exists() + ), f"Temp {file_path} should be renamed" return MagicMock(returncode=0) @@ -544,9 +544,9 @@ def test_wrangler_deploy_venv_exclusion(complex_project_structure): def check_venv_during_subprocess(*args, **kwargs): temp_project_dir = Path(kwargs["cwd"]) # During subprocess execution, .venv should not exist in temp directory - assert not (temp_project_dir / ".venv").exists(), ( - ".venv should not be copied to temp dir" - ) + assert not ( + temp_project_dir / ".venv" + ).exists(), ".venv should not be copied to temp dir" # Original .venv should still exist and be untouched assert venv_dir.exists(), "Original .venv should still exist" return MagicMock(returncode=0) @@ -569,12 +569,12 @@ def check_nested_files_during_subprocess(*args, **kwargs): deep_mcpac = temp_project_dir / "nested/deep/deep_file.txt.mcpac.py" # During subprocess execution, .mcpac.py files should exist in temp nested directories - assert nested_mcpac.exists(), ( - "Nested .mcpac.py file should exist during subprocess" - ) - assert deep_mcpac.exists(), ( - "Deep nested .mcpac.py file should exist during subprocess" - ) + assert ( + nested_mcpac.exists() + ), "Nested .mcpac.py file should exist during subprocess" + assert ( + deep_mcpac.exists() + ), "Deep nested .mcpac.py file should exist during subprocess" # Check that the nested directory structure is preserved in temp directory assert nested_mcpac.parent == temp_project_dir / "nested" @@ -650,17 +650,17 @@ def check_complex_extensions_during_subprocess(*args, **kwargs): original_temp_file = temp_project_dir / filename original_project_file = project_path / filename - assert mcpac_file.exists(), ( - f"Temp {filename}.mcpac.py should exist during subprocess" - ) + assert ( + mcpac_file.exists() + ), f"Temp {filename}.mcpac.py should exist during subprocess" # Original should not exist in temp directory (renamed to .mcpac.py) - assert not original_temp_file.exists(), ( - f"Temp {filename} should be renamed during subprocess" - ) + assert ( + not original_temp_file.exists() + ), f"Temp {filename} should be renamed during subprocess" # Original project file should be unchanged - assert original_project_file.exists(), ( - f"Original {filename} should be unchanged" - ) + assert ( + original_project_file.exists() + ), f"Original {filename} should be unchanged" return MagicMock(returncode=0) @@ -674,15 +674,15 @@ def check_complex_extensions_during_subprocess(*args, **kwargs): original_file = project_path / filename mcpac_file = project_path / f"{filename}.mcpac.py" - assert original_file.exists(), ( - f"Original {filename} should be unchanged" - ) - assert original_file.read_text() == expected_content, ( - f"{filename} content should be preserved" - ) - assert not mcpac_file.exists(), ( - f"No {filename}.mcpac.py should exist in original directory" - ) + assert ( + original_file.exists() + ), f"Original {filename} should be unchanged" + assert ( + original_file.read_text() == expected_content + ), f"{filename} content should be preserved" + assert ( + not mcpac_file.exists() + ), f"No {filename}.mcpac.py should exist in original directory" # Requirements.txt processing tests @@ -725,9 +725,9 @@ def test_needs_requirements_modification_with_relative_imports(): {relative_import} numpy==1.24.0""" requirements_path.write_text(requirements_content) - assert _needs_requirements_modification(requirements_path), ( - f"Should detect relative import: {relative_import}" - ) + assert _needs_requirements_modification( + requirements_path + ), f"Should detect relative import: {relative_import}" def test_needs_requirements_modification_mixed_content(): @@ -843,9 +843,9 @@ def check_requirements_during_subprocess(*args, **kwargs): assert "mcp-agent\n" in deployed_content # Original project requirements.txt should be unchanged - assert requirements_path.exists(), ( - "Original requirements.txt should be unchanged" - ) + assert ( + requirements_path.exists() + ), "Original requirements.txt should be unchanged" assert requirements_path.read_text() == original_content return MagicMock(returncode=0) @@ -873,17 +873,17 @@ def check_requirements_during_subprocess(*args, **kwargs): # In temp directory, requirements.txt should be renamed to .mcpac.py assert temp_mcpac_path.exists(), "Temp requirements.txt.mcpac.py should exist" - assert not temp_requirements_path.exists(), ( - "Temp requirements.txt should be renamed" - ) + assert ( + not temp_requirements_path.exists() + ), "Temp requirements.txt should be renamed" # Content should be preserved in .mcpac.py version assert temp_mcpac_path.read_text() == original_content # Original project requirements.txt should be unchanged - assert requirements_path.exists(), ( - "Original requirements.txt should be unchanged" - ) + assert ( + requirements_path.exists() + ), "Original requirements.txt should be unchanged" assert requirements_path.read_text() == original_content return MagicMock(returncode=0) @@ -938,7 +938,7 @@ def test_wrangler_deploy_secrets_file_exclusion(): # Create other YAML files that should be processed config_file = project_path / "config.yaml" - config_file.write_text("app_name: test-app") + config_file.write_text("name: test-app") mcp_config_file = project_path / "mcp_agent.config.yaml" mcp_config_file.write_text("config: value") @@ -950,37 +950,37 @@ def check_secrets_exclusion_during_subprocess(*args, **kwargs): temp_project_dir = Path(kwargs["cwd"]) # Secrets file should NOT exist in temp directory at all - assert not (temp_project_dir / MCP_SECRETS_FILENAME).exists(), ( - "Secrets file should be excluded from temp directory" - ) + assert not ( + temp_project_dir / MCP_SECRETS_FILENAME + ).exists(), "Secrets file should be excluded from temp directory" assert not ( temp_project_dir / f"{MCP_SECRETS_FILENAME}.mcpac.py" ).exists(), "Secrets file should not be processed as .mcpac.py" # Other YAML files should be processed normally - assert (temp_project_dir / "config.yaml.mcpac.py").exists(), ( - "Other YAML files should be processed as .mcpac.py" - ) - assert (temp_project_dir / "mcp_agent.config.yaml.mcpac.py").exists(), ( - "mcp_agent.config.yaml should be processed as .mcpac.py" - ) + assert ( + temp_project_dir / "config.yaml.mcpac.py" + ).exists(), "Other YAML files should be processed as .mcpac.py" + assert ( + temp_project_dir / "mcp_agent.config.yaml.mcpac.py" + ).exists(), "mcp_agent.config.yaml should be processed as .mcpac.py" assert ( temp_project_dir / "mcp_agent.deployed.secrets.yaml.mcpac.py" ).exists(), ( "mcp_agent.deployed.secrets.yaml should be processed as .mcpac.py" ) - assert not (temp_project_dir / "config.yaml").exists(), ( - "Other YAML files should be renamed in temp directory" - ) + assert not ( + temp_project_dir / "config.yaml" + ).exists(), "Other YAML files should be renamed in temp directory" # Original files should remain untouched - assert secrets_file.exists(), ( - "Original secrets file should remain untouched" - ) + assert ( + secrets_file.exists() + ), "Original secrets file should remain untouched" assert config_file.exists(), "Original config file should remain untouched" - assert secrets_file.read_text() == secrets_content, ( - "Secrets file content should be unchanged" - ) + assert ( + secrets_file.read_text() == secrets_content + ), "Secrets file content should be unchanged" return MagicMock(returncode=0) @@ -991,12 +991,12 @@ def check_secrets_exclusion_during_subprocess(*args, **kwargs): # After deployment, original files should be unchanged assert secrets_file.exists(), "Secrets file should still exist" - assert secrets_file.read_text() == secrets_content, ( - "Secrets file content should be preserved" - ) + assert ( + secrets_file.read_text() == secrets_content + ), "Secrets file content should be preserved" assert config_file.exists(), "Config file should still exist" # No secrets-related mcpac.py files should exist in original directory - assert not (project_path / f"{MCP_SECRETS_FILENAME}.mcpac.py").exists(), ( - "No secrets .mcpac.py file should exist in original directory" - ) + assert not ( + project_path / f"{MCP_SECRETS_FILENAME}.mcpac.py" + ).exists(), "No secrets .mcpac.py file should exist in original directory" diff --git a/tests/test_app.py b/tests/test_app.py index 677163457..4b94015f3 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -17,6 +17,8 @@ def mock_context(self): """Create a mock Context with necessary attributes.""" mock_context = MagicMock(spec=Context) mock_context.config = MagicMock(spec=Settings) + mock_context.config.name = None + mock_context.config.description = None mock_context.server_registry = MagicMock() mock_context.task_registry = MagicMock() mock_context.decorator_registry = MagicMock() @@ -116,6 +118,8 @@ async def test_initialization_minimal(self): async def test_initialization_with_custom_settings(self): """Test initialization with custom settings.""" mock_settings = MagicMock(spec=Settings) + mock_settings.name = None + mock_settings.description = None app = MCPApp(name="test_app", settings=mock_settings) assert app._config is mock_settings diff --git a/tests/utils/test_config_preload.py b/tests/utils/test_config_preload.py index b6acc7208..37b7e0d87 100644 --- a/tests/utils/test_config_preload.py +++ b/tests/utils/test_config_preload.py @@ -147,7 +147,9 @@ def test_default_sets_global_state(self, sample_config): config_path = "/fake/path/config.yaml" with patch("mcp_agent.config._check_file_exists", return_value=True): - with patch("mcp_agent.config._read_file_content", return_value=yaml_content): + with patch( + "mcp_agent.config._read_file_content", return_value=yaml_content + ): # Load settings with default behavior settings = get_settings(config_path=config_path) @@ -164,10 +166,10 @@ def test_set_global_false_no_global_state(self, sample_config): config_path = "/fake/path/config.yaml" with patch("mcp_agent.config._check_file_exists", return_value=True): - with patch("mcp_agent.config._read_file_content", return_value=yaml_content): - settings = get_settings( - config_path=config_path, set_global=False - ) + with patch( + "mcp_agent.config._read_file_content", return_value=yaml_content + ): + settings = get_settings(config_path=config_path, set_global=False) # Global state should remain None assert mcp_agent.config._settings is None @@ -183,10 +185,10 @@ def test_explicit_set_global_true(self, sample_config): config_path = "/fake/path/config.yaml" with patch("mcp_agent.config._check_file_exists", return_value=True): - with patch("mcp_agent.config._read_file_content", return_value=yaml_content): - settings = get_settings( - config_path=config_path, set_global=True - ) + with patch( + "mcp_agent.config._read_file_content", return_value=yaml_content + ): + settings = get_settings(config_path=config_path, set_global=True) assert mcp_agent.config._settings is not None assert mcp_agent.config._settings == settings @@ -197,7 +199,9 @@ def test_returns_cached_global_when_set(self, sample_config): config_path = "/fake/path/config.yaml" with patch("mcp_agent.config._check_file_exists", return_value=True): - with patch("mcp_agent.config._read_file_content", return_value=yaml_content): + with patch( + "mcp_agent.config._read_file_content", return_value=yaml_content + ): # First call sets global state settings1 = get_settings(config_path=config_path) @@ -214,16 +218,14 @@ def test_no_cached_return_when_set_global_false(self, sample_config): config_path = "/fake/path/config.yaml" with patch("mcp_agent.config._check_file_exists", return_value=True): - with patch("mcp_agent.config._read_file_content", return_value=yaml_content): + with patch( + "mcp_agent.config._read_file_content", return_value=yaml_content + ): # First call with set_global=False - settings1 = get_settings( - config_path=config_path, set_global=False - ) + settings1 = get_settings(config_path=config_path, set_global=False) # Second call with set_global=False - settings2 = get_settings( - config_path=config_path, set_global=False - ) + settings2 = get_settings(config_path=config_path, set_global=False) # They should be different objects (not cached) assert settings1 is not settings2 @@ -271,7 +273,9 @@ def test_explicit_config_path_with_cache_returns_cached(self, sample_config): # First load to set global cache with initial config with patch("mcp_agent.config._check_file_exists", return_value=True): - with patch("mcp_agent.config._read_file_content", return_value=initial_yaml): + with patch( + "mcp_agent.config._read_file_content", return_value=initial_yaml + ): settings1 = get_settings(config_path="/fake/path/initial.yaml") assert settings1.execution_engine == "asyncio" assert settings1.logger.type == "console" @@ -285,7 +289,9 @@ def test_explicit_config_path_with_cache_returns_cached(self, sample_config): # Third call with different config_path still returns cached settings (current behavior) with patch("mcp_agent.config._check_file_exists", return_value=True): - with patch("mcp_agent.config._read_file_content", return_value=updated_yaml): + with patch( + "mcp_agent.config._read_file_content", return_value=updated_yaml + ): settings3 = get_settings(config_path="/fake/path/updated.yaml") # Still returns cached settings, not the new config assert settings3 is settings1 @@ -296,8 +302,12 @@ def test_explicit_config_path_with_cache_returns_cached(self, sample_config): # To actually load new config, must use set_global=False with patch("mcp_agent.config._check_file_exists", return_value=True): - with patch("mcp_agent.config._read_file_content", return_value=updated_yaml): - settings4 = get_settings(config_path="/fake/path/updated.yaml", set_global=False) + with patch( + "mcp_agent.config._read_file_content", return_value=updated_yaml + ): + settings4 = get_settings( + config_path="/fake/path/updated.yaml", set_global=False + ) # Now we get the new config assert settings4.execution_engine == "temporal" assert settings4.logger.type == "file" @@ -382,7 +392,9 @@ def load_settings(thread_id, config_path): # Mock at test level, not inside threads with patch("mcp_agent.config._check_file_exists", return_value=True): - with patch("mcp_agent.config._read_file_content", return_value=yaml_content): + with patch( + "mcp_agent.config._read_file_content", return_value=yaml_content + ): # Create threads threads = [] for i in range(3): @@ -443,9 +455,7 @@ def test_config_and_secrets_merge_with_set_global_false( with patch("mcp_agent.config._check_file_exists", return_value=True): with patch("mcp_agent.config._read_file_content", return_value=merged_yaml): - settings = get_settings( - config_path=config_path, set_global=False - ) + settings = get_settings(config_path=config_path, set_global=False) # Global state should not be set assert mcp_agent.config._settings is None