From a7d6b25dd3e4e8feecdf79c2d23fc58e75335064 Mon Sep 17 00:00:00 2001 From: aivibe <93047788+axisrow@users.noreply.github.com> Date: Mon, 2 Mar 2026 14:27:23 +0700 Subject: [PATCH 1/2] refactor: rename bundled server_config.json to example and fix launcher tests - Rename src/mcp_cli/server_config.json to server_config.example.json - Remove bundled config fallback logic from ConfigLoader - Migrate all hardcoded "server_config.json" to DEFAULT_CONFIG_FILENAME constant - Raise missing-config log level from DEBUG to INFO for better user visibility - Fix webbrowser.open patch targets in dashboard launcher tests - Update ConfigLoader FileNotFoundError tests with strict assertions and caplog Co-Authored-By: Claude Sonnet 4.6 --- README.md | 54 +++-------------- diagnostics/mcp_server_diagnostic.py | 1 + manifest.in | 2 +- pyproject.toml | 2 +- src/mcp_cli/chat/chat_handler.py | 5 +- src/mcp_cli/config/__init__.py | 4 +- src/mcp_cli/config/cli_options.py | 26 ++------ src/mcp_cli/config/config_manager.py | 25 +------- src/mcp_cli/context/context_manager.py | 6 +- src/mcp_cli/main.py | 59 ++++++++++++------- src/mcp_cli/run_command.py | 4 +- ...config.json => server_config.example.json} | 0 src/mcp_cli/tools/config_loader.py | 49 +-------------- tests/config/test_cli_options.py | 51 ++-------------- tests/config/test_config_manager.py | 21 +++---- tests/dashboard/test_launcher.py | 17 +++--- tests/tools/test_config_loader.py | 25 ++++---- 17 files changed, 105 insertions(+), 246 deletions(-) rename src/mcp_cli/{server_config.json => server_config.example.json} (100%) diff --git a/README.md b/README.md index 397fa8cb..bee839ac 100644 --- a/README.md +++ b/README.md @@ -243,6 +243,12 @@ mcp-cli --help pip install -e ".[apps]" ``` +4. **Set up server configuration**: +```bash +cp src/mcp_cli/server_config.example.json server_config.json +# Edit server_config.json — keep only the servers you need +``` + ### Using Different Models ```bash @@ -900,54 +906,10 @@ MCP CLI searches for `server_config.json` in the following priority order: mcp-cli --server sqlite # Uses ./server_config.json if it exists ``` -3. **Bundled default** - When running via `uvx` or from any directory without a local config: - ```bash - uvx mcp-cli --server cloudflare_workers # Uses packaged server_config.json - ``` - This means you can: -- **Override per-project**: Place a `server_config.json` in your project directory with project-specific server configurations -- **Use defaults globally**: Run `uvx mcp-cli` from anywhere and get the bundled default servers +- **Per-project config**: Place a `server_config.json` in your project directory with project-specific server configurations - **Customize explicitly**: Use `--config-file` to specify any configuration file location - -### Bundled Default Servers - -MCP CLI v0.11.1+ comes with an expanded set of pre-configured servers in the bundled `server_config.json`: - -| Server | Type | Description | Configuration | -|--------|------|-------------|---------------| -| **sqlite** | STDIO | SQLite database operations | `uvx mcp-server-sqlite --db-path test.db` | -| **echo** | STDIO | Echo server for testing | `uvx chuk-mcp-echo stdio` | -| **math** | STDIO | Mathematical computations | `uvx chuk-mcp-math-server` | -| **playwright** | STDIO | Browser automation | `npx @playwright/mcp@latest` | -| **brave_search** | STDIO | Web search via Brave API | Requires `BRAVE_API_KEY` token | -| **notion** | HTTP | Notion workspace integration | `https://mcp.notion.com/mcp` (OAuth) | -| **cloudflare_workers** | HTTP | Cloudflare Workers bindings | `https://bindings.mcp.cloudflare.com/mcp` (OAuth) | -| **monday** | HTTP | Monday.com integration | `https://mcp.monday.com/mcp` (OAuth) | -| **linkedin** | HTTP | LinkedIn integration | `https://linkedin.chukai.io/mcp` | -| **weather** | HTTP | Weather data service | `https://weather.chukai.io/mcp` | - -**Note**: HTTP servers and API-based servers require authentication. Use the [Token Management](docs/TOKEN_MANAGEMENT.md) system to configure access tokens. - -To use these servers: -```bash -# Use bundled servers from anywhere -uvx mcp-cli --server sqlite -uvx mcp-cli --server echo -uvx mcp-cli --server math -uvx mcp-cli --server playwright - -# API-based servers require tokens -mcp-cli token set brave_search --type bearer -uvx mcp-cli --server brave_search - -# HTTP/OAuth servers require OAuth authentication -uvx mcp-cli token set notion --oauth -uvx mcp-cli --server notion - -# Use multiple servers simultaneously -uvx mcp-cli --server sqlite,math,playwright -``` +- **Reference example**: See `server_config.example.json` in the repository for a list of available servers and configuration examples ### Project Configuration diff --git a/diagnostics/mcp_server_diagnostic.py b/diagnostics/mcp_server_diagnostic.py index 3abb9e94..f6029edb 100644 --- a/diagnostics/mcp_server_diagnostic.py +++ b/diagnostics/mcp_server_diagnostic.py @@ -121,6 +121,7 @@ def check_mcp_environment(): print(" Create a config file with MCP servers to analyze real connections") print() print(" 📝 mcp-cli uses 'server_config.json' by default. Create one of:") + print(" 💡 Tip: cp src/mcp_cli/server_config.example.json server_config.json") print(" • server_config.json (in current directory)") print(" • ~/.config/mcp/config.json (standard MCP location)") print(" • mcp_config.json (alternative name)") diff --git a/manifest.in b/manifest.in index f13e4cbc..54d2de7b 100644 --- a/manifest.in +++ b/manifest.in @@ -1,6 +1,6 @@ # Include documentation include README.md -include server_config.json +include src/mcp_cli/server_config.example.json # Include package files recursive-include src/mcp_cli *.py diff --git a/pyproject.toml b/pyproject.toml index 1f21380a..b736ed68 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,7 +62,7 @@ where = ["src"] include = ["mcp_cli*"] [tool.setuptools.package-data] -mcp_cli = ["server_config.json", "dashboard/static/**/*"] +mcp_cli = ["server_config.example.json", "dashboard/static/**/*"] [dependency-groups] dev = [ "colorama>=0.4.6", diff --git a/src/mcp_cli/chat/chat_handler.py b/src/mcp_cli/chat/chat_handler.py index e1f7a2ae..065483cb 100644 --- a/src/mcp_cli/chat/chat_handler.py +++ b/src/mcp_cli/chat/chat_handler.py @@ -27,7 +27,7 @@ from mcp_cli.tools.manager import ToolManager from mcp_cli.context import initialize_context from mcp_cli.config import initialize_config -from mcp_cli.config.defaults import DEFAULT_PROVIDER, DEFAULT_MODEL +from mcp_cli.config.defaults import DEFAULT_PROVIDER, DEFAULT_MODEL, DEFAULT_CONFIG_FILENAME # Set up logger logger = logging.getLogger(__name__) @@ -94,7 +94,8 @@ async def handle_chat_mode( # Initialize configuration manager from pathlib import Path - initialize_config(Path("server_config.json")) + # Use the project-wide constant instead of a hardcoded filename + initialize_config(Path(DEFAULT_CONFIG_FILENAME)) # Initialize global context manager for commands to work app_context = initialize_context( diff --git a/src/mcp_cli/config/__init__.py b/src/mcp_cli/config/__init__.py index e52b79b1..77ca5b8a 100644 --- a/src/mcp_cli/config/__init__.py +++ b/src/mcp_cli/config/__init__.py @@ -270,7 +270,7 @@ def load_runtime_config( """ from pathlib import Path - path = Path(config_path or "server_config.json") + path = Path(config_path or DEFAULT_CONFIG_FILENAME) file_config = MCPConfig.load_sync(path) return RuntimeConfig(file_config, cli_overrides) @@ -290,7 +290,7 @@ async def load_runtime_config_async( """ from pathlib import Path - path = Path(config_path or "server_config.json") + path = Path(config_path or DEFAULT_CONFIG_FILENAME) file_config = await MCPConfig.load_async(path) return RuntimeConfig(file_config, cli_overrides) diff --git a/src/mcp_cli/config/cli_options.py b/src/mcp_cli/config/cli_options.py index dfbdc31b..74efdc2e 100644 --- a/src/mcp_cli/config/cli_options.py +++ b/src/mcp_cli/config/cli_options.py @@ -19,6 +19,7 @@ from mcp_cli.config import ( MCPConfig, + DEFAULT_CONFIG_FILENAME, setup_chuk_llm_environment, trigger_discovery_after_setup, detect_server_types, @@ -29,43 +30,24 @@ def load_config(config_file: str) -> MCPConfig | None: - """Load MCP server config file with fallback to bundled package config.""" + """Load MCP server config file.""" try: config_path = Path(config_file) - # Try explicit path or current directory first if config_path.is_file(): config = MCPConfig.load_from_file(config_path) # If config loaded but has no servers and file exists, it might be invalid JSON # Check if the file has content but failed to parse if not config.servers and config_path.stat().st_size > 0: try: - # Try to parse as JSON to verify it's valid import json content = config_path.read_text() json.loads(content) except json.JSONDecodeError: - # Invalid JSON - return None return None return config - # If not found and using default name, try package bundle - if config_file == "server_config.json": - try: - import importlib.resources as resources - - # Try Python 3.9+ API - if hasattr(resources, "files"): - package_files = resources.files("mcp_cli") - bundled_config = package_files / "server_config.json" - if bundled_config.is_file(): - logger.info("Loading bundled server configuration") - # Create a temporary Path object from the resource - return MCPConfig.load_from_file(Path(str(bundled_config))) - except (ImportError, FileNotFoundError, AttributeError, TypeError) as e: - logger.debug(f"Could not load bundled config: {e}") - except Exception as exc: logger.error("Error loading config file '%s': %s", config_file, exc) return None @@ -106,7 +88,7 @@ def process_options( disable_filesystem: bool, provider: str, model: str | None, - config_file: str = "server_config.json", + config_file: str = DEFAULT_CONFIG_FILENAME, quiet: bool = False, ) -> tuple[list[str], list[str], dict[int, str]]: """ @@ -140,7 +122,7 @@ def process_options( cfg = load_config(config_file) if not cfg: - logger.warning(f"Could not load config file: {config_file}") + logger.info(f"Could not load config file: {config_file}") # Return empty configuration return [], user_specified, {} diff --git a/src/mcp_cli/config/config_manager.py b/src/mcp_cli/config/config_manager.py index d927d496..e8767f8c 100644 --- a/src/mcp_cli/config/config_manager.py +++ b/src/mcp_cli/config/config_manager.py @@ -19,7 +19,7 @@ from pydantic import BaseModel, Field from mcp_cli.auth import OAuthConfig -from mcp_cli.config.defaults import DEFAULT_PROVIDER, DEFAULT_MODEL +from mcp_cli.config.defaults import DEFAULT_PROVIDER, DEFAULT_MODEL, DEFAULT_CONFIG_FILENAME from mcp_cli.tools.models import ServerInfo, TransportType # Import clean models from new config system @@ -352,32 +352,13 @@ def initialize(self, config_path: Path | None = None) -> MCPConfig: Priority order: 1. Explicit config_path if provided - 2. server_config.json in current directory (overrides package default) - 3. server_config.json bundled in package (fallback) + 2. server_config.json in current directory """ if self._config is None: if config_path: - # Explicit path provided self._config_path = Path(config_path) else: - # Check current directory first - cwd_config = Path("server_config.json") - if cwd_config.exists(): - self._config_path = cwd_config - else: - # Fall back to package bundled config - import importlib.resources as resources - - try: - package_files = resources.files("mcp_cli") - config_file = package_files / "server_config.json" - if config_file.is_file(): - self._config_path = Path(str(config_file)) - else: - self._config_path = cwd_config - except (ImportError, FileNotFoundError, AttributeError, TypeError): - # If package config doesn't exist or can't be accessed, use cwd - self._config_path = cwd_config + self._config_path = Path(DEFAULT_CONFIG_FILENAME) self._config = MCPConfig.load_from_file(self._config_path) return self._config diff --git a/src/mcp_cli/context/context_manager.py b/src/mcp_cli/context/context_manager.py index 83fe958c..3de30fe0 100644 --- a/src/mcp_cli/context/context_manager.py +++ b/src/mcp_cli/context/context_manager.py @@ -12,7 +12,7 @@ from pydantic import BaseModel, Field, ConfigDict, PrivateAttr, SkipValidation -from mcp_cli.config.defaults import DEFAULT_PROVIDER, DEFAULT_MODEL +from mcp_cli.config.defaults import DEFAULT_PROVIDER, DEFAULT_MODEL, DEFAULT_CONFIG_FILENAME from mcp_cli.tools.manager import ToolManager from mcp_cli.model_management import ModelManager from mcp_cli.tools.models import ServerInfo, ToolInfo, ConversationMessage @@ -35,7 +35,7 @@ class ApplicationContext(BaseModel): model_manager: Annotated[ModelManager | None, SkipValidation()] = None # Configuration - config_path: Path = Field(default_factory=lambda: Path("server_config.json")) + config_path: Path = Field(default_factory=lambda: Path(DEFAULT_CONFIG_FILENAME)) provider: str = DEFAULT_PROVIDER model: str = DEFAULT_MODEL api_base: str | None = None @@ -90,7 +90,7 @@ def create( """ context = cls( tool_manager=tool_manager, - config_path=config_path or Path("server_config.json"), + config_path=config_path or Path(DEFAULT_CONFIG_FILENAME), provider=provider or DEFAULT_PROVIDER, model=model or DEFAULT_MODEL, **kwargs, diff --git a/src/mcp_cli/main.py b/src/mcp_cli/main.py index ccb353a5..6b943ee7 100644 --- a/src/mcp_cli/main.py +++ b/src/mcp_cli/main.py @@ -30,7 +30,7 @@ restore_terminal, ) from chuk_term.ui.theme import set_theme -from mcp_cli.config import process_options +from mcp_cli.config import process_options, APP_VERSION, DEFAULT_CONFIG_FILENAME from mcp_cli.context import initialize_context # ────────────────────────────────────────────────────────────────────────────── @@ -55,14 +55,29 @@ app = typer.Typer(add_completion=False) +# ────────────────────────────────────────────────────────────────────────────── +# Version callback +# ────────────────────────────────────────────────────────────────────────────── +def _version_callback(value: bool) -> None: + if value: + typer.echo(f"mcp-cli {APP_VERSION}") + raise typer.Exit() + + # ────────────────────────────────────────────────────────────────────────────── # Default callback that handles no-subcommand case # ────────────────────────────────────────────────────────────────────────────── @app.callback(invoke_without_command=True) def main_callback( ctx: typer.Context, + version: bool = typer.Option( + None, "--version", "-V", + callback=_version_callback, + is_eager=True, + help="Show version and exit.", + ), config_file: str = typer.Option( - "server_config.json", help="Configuration file path" + DEFAULT_CONFIG_FILENAME, help="Configuration file path" ), server: str | None = typer.Option(None, help="Server to connect to"), provider: str | None = typer.Option(None, help="LLM provider name"), @@ -460,7 +475,7 @@ async def _start_chat(): ) def _chat_command( config_file: str = typer.Option( - "server_config.json", help="Configuration file path" + DEFAULT_CONFIG_FILENAME, help="Configuration file path" ), server: str | None = typer.Option(None, help="Server to connect to"), provider: str | None = typer.Option(None, help="LLM provider name"), @@ -717,7 +732,7 @@ async def _start_chat(): @app.command("interactive", help="Start interactive command mode.") def _interactive_command( config_file: str = typer.Option( - "server_config.json", help="Configuration file path" + DEFAULT_CONFIG_FILENAME, help="Configuration file path" ), server: str | None = typer.Option(None, help="Server to connect to"), provider: str | None = typer.Option(None, help="LLM provider name"), @@ -905,7 +920,7 @@ def provider_command( None, "--model", help="Model name (for switch commands)" ), config_file: str = typer.Option( - "server_config.json", help="Configuration file path" + DEFAULT_CONFIG_FILENAME, help="Configuration file path" ), server: str | None = typer.Option(None, help="Server to connect to"), disable_filesystem: bool = typer.Option(False, help="Disable filesystem access"), @@ -986,7 +1001,7 @@ def providers_command( None, "--model", help="Model name (for switch commands)" ), config_file: str = typer.Option( - "server_config.json", help="Configuration file path" + DEFAULT_CONFIG_FILENAME, help="Configuration file path" ), server: str | None = typer.Option(None, help="Server to connect to"), disable_filesystem: bool = typer.Option(False, help="Disable filesystem access"), @@ -1042,7 +1057,7 @@ def tools_command( all: bool = typer.Option(False, "--all", help="Show detailed tool information"), raw: bool = typer.Option(False, "--raw", help="Show raw JSON definitions"), config_file: str = typer.Option( - "server_config.json", help="Configuration file path" + DEFAULT_CONFIG_FILENAME, help="Configuration file path" ), server: str | None = typer.Option(None, help="Server to connect to"), provider: str = typer.Option("openai", help="LLM provider name"), @@ -1119,7 +1134,7 @@ def servers_command( "table", "--format", "-f", help="Output format: table, tree, or json" ), config_file: str = typer.Option( - "server_config.json", help="Configuration file path" + DEFAULT_CONFIG_FILENAME, help="Configuration file path" ), server: str | None = typer.Option(None, help="Server to connect to"), provider: str = typer.Option("openai", help="LLM provider name"), @@ -1186,7 +1201,7 @@ async def _servers_wrapper(**params): @app.command("resources", help="List available resources") def resources_command( config_file: str = typer.Option( - "server_config.json", help="Configuration file path" + DEFAULT_CONFIG_FILENAME, help="Configuration file path" ), server: str | None = typer.Option(None, help="Server to connect to"), provider: str = typer.Option("openai", help="LLM provider name"), @@ -1228,7 +1243,7 @@ async def _resources_wrapper(**params): @app.command("prompts", help="List available prompts") def prompts_command( config_file: str = typer.Option( - "server_config.json", help="Configuration file path" + DEFAULT_CONFIG_FILENAME, help="Configuration file path" ), server: str | None = typer.Option(None, help="Server to connect to"), provider: str = typer.Option("openai", help="LLM provider name"), @@ -1584,7 +1599,7 @@ def cmd_command( 100, "--max-turns", help="Maximum conversation turns" ), config_file: str = typer.Option( - "server_config.json", help="Configuration file path" + DEFAULT_CONFIG_FILENAME, help="Configuration file path" ), server: str | None = typer.Option(None, help="Server to connect to"), provider: str | None = typer.Option(None, help="LLM provider name"), @@ -1685,7 +1700,7 @@ def ping_command( None, help="Server names or indices to ping (omit for all)" ), config_file: str = typer.Option( - "server_config.json", help="Configuration file path" + DEFAULT_CONFIG_FILENAME, help="Configuration file path" ), server: str | None = typer.Option(None, help="Server to connect to"), provider: str = typer.Option("openai", help="LLM provider name"), @@ -1733,14 +1748,18 @@ async def _ping_wrapper(**params): direct_registered.append("ping") -# Show what we actually registered -all_registered = registry_registered + direct_registered -output.success("✓ MCP CLI ready") -if all_registered: - output.info(f" Available commands: {', '.join(sorted(all_registered))}") -else: - output.warning(" Warning: No commands were successfully registered!") -output.hint(" Use --help to see all options") +# NOTE: Startup banner disabled — it runs at module-import time (before +# Typer parses arguments), so it pollutes the output of --version, --help, +# and every subcommand. In chat mode it is immediately erased by +# clear_screen(). Consider removing this block entirely. +# +# all_registered = registry_registered + direct_registered +# output.success("✓ MCP CLI ready") +# if all_registered: +# output.info(f" Available commands: {', '.join(sorted(all_registered))}") +# else: +# output.warning(" Warning: No commands were successfully registered!") +# output.hint(" Use --help to see all options") # ────────────────────────────────────────────────────────────────────────────── diff --git a/src/mcp_cli/run_command.py b/src/mcp_cli/run_command.py index b4d0200a..692a9343 100644 --- a/src/mcp_cli/run_command.py +++ b/src/mcp_cli/run_command.py @@ -23,7 +23,7 @@ from rich.panel import Panel from chuk_term.ui import output -from mcp_cli.config.defaults import DEFAULT_PROVIDER, DEFAULT_MODEL +from mcp_cli.config.defaults import DEFAULT_PROVIDER, DEFAULT_MODEL, DEFAULT_CONFIG_FILENAME from mcp_cli.tools.manager import ToolManager from mcp_cli.context import initialize_context @@ -303,7 +303,7 @@ async def _enter_interactive_mode( def cli_entry( mode: str = typer.Argument("chat", help="chat | interactive"), config_file: str = typer.Option( - "server_config.json", "--config", "-c", help="Server config file" + DEFAULT_CONFIG_FILENAME, "--config", "-c", help="Server config file" ), server: list[str] = typer.Option( ["sqlite"], "--server", "-s", help="Server(s) to connect" diff --git a/src/mcp_cli/server_config.json b/src/mcp_cli/server_config.example.json similarity index 100% rename from src/mcp_cli/server_config.json rename to src/mcp_cli/server_config.example.json diff --git a/src/mcp_cli/tools/config_loader.py b/src/mcp_cli/tools/config_loader.py index 47cd8d93..12c76857 100644 --- a/src/mcp_cli/tools/config_loader.py +++ b/src/mcp_cli/tools/config_loader.py @@ -58,22 +58,6 @@ def __init__(self, config_file: str, servers: list[str]) -> None: self.sse_servers: list[HTTPServerConfig] = [] self.stdio_servers: list[STDIOServerConfig] = [] - @staticmethod - def _resolve_bundled_config() -> str | None: - """Try to find the bundled package config as a fallback.""" - try: - import importlib.resources as resources - from pathlib import Path - - package_files = resources.files("mcp_cli") - bundled = package_files / "server_config.json" - bundled_path = str(bundled) - if Path(bundled_path).is_file(): - return bundled_path - except (ImportError, FileNotFoundError, AttributeError, TypeError) as e: - logger.debug("Bundled config not found: %s", e) - return None - def load(self) -> dict[str, Any]: """Load and parse MCP config file with token resolution (sync). @@ -96,19 +80,7 @@ def load(self) -> dict[str, Any]: return config except FileNotFoundError: - # Fall back to bundled package config - bundled = self._resolve_bundled_config() - if bundled: - logger.info("Using bundled server configuration") - try: - with open(bundled) as f: - config = cast(dict[str, Any], json.load(f)) - self._resolve_token_placeholders(config) - self._config_cache = config - return config - except Exception as e: - logger.error(f"Error loading bundled config: {e}") - logger.warning(f"Config file not found: {self.config_file}") + logger.info(f"Config file not found: {self.config_file}") return {} except json.JSONDecodeError as e: logger.error(f"Invalid JSON in config: {e}") @@ -144,24 +116,7 @@ def _read_file() -> str: return config except FileNotFoundError: - # Fall back to bundled package config - bundled = self._resolve_bundled_config() - if bundled: - logger.info("Using bundled server configuration") - try: - - def _read_bundled() -> str: - with open(bundled) as f: - return f.read() - - content = await asyncio.to_thread(_read_bundled) - config = cast(dict[str, Any], json.loads(content)) - self._resolve_token_placeholders(config) - self._config_cache = config - return config - except Exception as e: - logger.error(f"Error loading bundled config: {e}") - logger.warning(f"Config file not found: {self.config_file}") + logger.info(f"Config file not found: {self.config_file}") return {} except json.JSONDecodeError as e: logger.error(f"Invalid JSON in config: {e}") diff --git a/tests/config/test_cli_options.py b/tests/config/test_cli_options.py index bb29871e..4b9442eb 100644 --- a/tests/config/test_cli_options.py +++ b/tests/config/test_cli_options.py @@ -516,51 +516,13 @@ def is_disabled(server_name): class TestLoadConfigBundled: - """Test bundled config loading fallback.""" + """Test config loading when file is missing (no bundled fallback).""" - def test_load_bundled_config_with_default_name(self, tmp_path, monkeypatch): - """Test loading bundled config when server_config.json not found locally.""" - # Change to temp directory where server_config.json doesn't exist + def test_load_config_missing_returns_none(self, tmp_path, monkeypatch): + """Test that missing config file returns None (no bundled fallback).""" monkeypatch.chdir(tmp_path) - - # Mock importlib.resources to simulate bundled config - mock_bundled_config = { - "mcpServers": {"BundledServer": {"command": "bundled-cmd", "args": []}} - } - - # Create a mock resource file - mock_resource_file = MagicMock() - mock_resource_file.is_file.return_value = True - mock_resource_file.__str__.return_value = str( - tmp_path / "bundled_server_config.json" - ) - - # Create the actual file for MCPConfig.load_from_file to read - bundled_file = tmp_path / "bundled_server_config.json" - bundled_file.write_text(json.dumps(mock_bundled_config)) - mock_resource_file.__str__.return_value = str(bundled_file) - - with patch("importlib.resources.files") as mock_files: - mock_package = MagicMock() - mock_package.__truediv__ = MagicMock(return_value=mock_resource_file) - mock_files.return_value = mock_package - - config = load_config("server_config.json") - - # Should successfully load bundled config - assert config is not None - assert "BundledServer" in config.servers - - def test_load_bundled_config_not_found(self, tmp_path, monkeypatch): - """Test when bundled config is not available.""" - monkeypatch.chdir(tmp_path) - - # Mock importlib.resources to raise error (no bundled config) - with patch( - "importlib.resources.files", side_effect=FileNotFoundError("No bundled") - ): - config = load_config("server_config.json") - assert config is None + config = load_config("server_config.json") + assert config is None def test_load_config_with_empty_but_valid_json(self, tmp_path): """Test loading config with empty but valid JSON.""" @@ -721,7 +683,7 @@ def test_process_options_with_nonexistent_config( monkeypatch.delenv("LLM_PROVIDER", raising=False) - with caplog.at_level(logging.WARNING): + with caplog.at_level(logging.DEBUG, logger="mcp_cli.config.cli_options"): servers_list, specified, server_names = process_options( server="Server1", disable_filesystem=True, @@ -735,7 +697,6 @@ def test_process_options_with_nonexistent_config( assert specified == ["Server1"] assert server_names == {} - # Should log warning assert any( "Could not load config file" in record.message for record in caplog.records ) diff --git a/tests/config/test_config_manager.py b/tests/config/test_config_manager.py index 0636dec0..37beb5e2 100644 --- a/tests/config/test_config_manager.py +++ b/tests/config/test_config_manager.py @@ -767,29 +767,22 @@ def test_load_with_error_returns_empty_config(self, tmp_path): class TestConfigManagerPackageFallback: - """Test ConfigManager package fallback behavior.""" + """Test ConfigManager initialization behavior.""" def test_initialize_without_path_no_cwd_file(self, tmp_path, monkeypatch): """Test initialize without path when no server_config.json in cwd.""" monkeypatch.chdir(tmp_path) - # No server_config.json in tmp_path manager = ConfigManager() manager.reset() - # Mock the importlib.resources behavior - with patch("importlib.resources.files") as mock_files: - mock_package = MagicMock() - mock_config_file = MagicMock() - mock_config_file.is_file.return_value = False - mock_package.__truediv__ = MagicMock(return_value=mock_config_file) - mock_files.return_value = mock_package - - config = manager.initialize() - assert config is not None + config = manager.initialize() + # Returns empty config (no bundled fallback) + assert config is not None + assert config.servers == {} - def test_initialize_with_cwd_file_priority(self, tmp_path, monkeypatch): - """Test that cwd config takes priority over bundled.""" + def test_initialize_with_cwd_file(self, tmp_path, monkeypatch): + """Test that cwd config is loaded.""" config_data = {"mcpServers": {"local-server": {"command": "local-cmd"}}} config_file = tmp_path / "server_config.json" config_file.write_text(json.dumps(config_data)) diff --git a/tests/dashboard/test_launcher.py b/tests/dashboard/test_launcher.py index 0baa902a..97c2deea 100644 --- a/tests/dashboard/test_launcher.py +++ b/tests/dashboard/test_launcher.py @@ -24,7 +24,8 @@ def _mock_server(port: int = 9120): class TestLaunchDashboard: @pytest.mark.asyncio - async def test_returns_server_router_and_port(self): + @patch("mcp_cli.dashboard.launcher.webbrowser.open") + async def test_returns_server_router_and_port(self, _mock_open): from mcp_cli.dashboard import launcher srv = _mock_server(9120) @@ -36,7 +37,8 @@ async def test_returns_server_router_and_port(self): assert port == 9120 @pytest.mark.asyncio - async def test_returns_router(self): + @patch("mcp_cli.dashboard.launcher.webbrowser.open") + async def test_returns_router(self, _mock_open): from mcp_cli.dashboard import launcher srv = _mock_server(9120) @@ -51,7 +53,7 @@ async def test_opens_browser_when_no_browser_false(self): from mcp_cli.dashboard import launcher with patch.object(launcher, "DashboardServer", return_value=_mock_server(9120)): - with patch("webbrowser.open") as mock_open: + with patch("mcp_cli.dashboard.launcher.webbrowser.open") as mock_open: await launcher.launch_dashboard(no_browser=False) mock_open.assert_called_once_with("http://localhost:9120") @@ -61,7 +63,7 @@ async def test_skips_browser_when_no_browser_true(self): from mcp_cli.dashboard import launcher with patch.object(launcher, "DashboardServer", return_value=_mock_server(9120)): - with patch("webbrowser.open") as mock_open: + with patch("mcp_cli.dashboard.launcher.webbrowser.open") as mock_open: await launcher.launch_dashboard(no_browser=True) mock_open.assert_not_called() @@ -71,7 +73,8 @@ async def test_webbrowser_exception_suppressed(self): from mcp_cli.dashboard import launcher with patch.object(launcher, "DashboardServer", return_value=_mock_server(9120)): - with patch("webbrowser.open", side_effect=Exception("no display")): + # Patch where the name is looked up, not where it's defined + with patch("mcp_cli.dashboard.launcher.webbrowser.open", side_effect=Exception("no display")): server, router, port = await launcher.launch_dashboard(no_browser=False) assert port == 9120 # function completed successfully @@ -82,7 +85,7 @@ async def test_preferred_port_passed_to_server(self): srv = _mock_server(8080) with patch.object(launcher, "DashboardServer", return_value=srv): - with patch("webbrowser.open"): + with patch("mcp_cli.dashboard.launcher.webbrowser.open"): _, _, port = await launcher.launch_dashboard(port=8080) srv.start.assert_called_once_with(8080) @@ -94,7 +97,7 @@ async def test_default_port_zero_passed_to_server(self): srv = _mock_server(9120) with patch.object(launcher, "DashboardServer", return_value=srv): - with patch("webbrowser.open"): + with patch("mcp_cli.dashboard.launcher.webbrowser.open"): await launcher.launch_dashboard() srv.start.assert_called_once_with(0) diff --git a/tests/tools/test_config_loader.py b/tests/tools/test_config_loader.py index 88bacdc3..8651d295 100644 --- a/tests/tools/test_config_loader.py +++ b/tests/tools/test_config_loader.py @@ -2,6 +2,7 @@ """Tests for MCP configuration loading.""" import json +import logging import pytest from unittest.mock import AsyncMock, MagicMock, patch @@ -91,15 +92,15 @@ def test_load_config_caches_result(temp_config_file): assert config1 is config2 -def test_load_config_file_not_found(): - """Test loading nonexistent config file falls back to bundled config.""" +def test_load_config_file_not_found(caplog): + """Test loading nonexistent config file returns empty dict.""" loader = ConfigLoader("/nonexistent/config.json", []) - config = loader.load() + with caplog.at_level(logging.INFO, logger="mcp_cli.tools.config_loader"): + config = loader.load() - # Should fall back to bundled package config (non-empty) - # or return empty dict if bundled config unavailable - assert isinstance(config, dict) + assert config == {} + assert any("Config file not found" in r.message for r in caplog.records) def test_load_config_invalid_json(tmp_path): @@ -627,15 +628,15 @@ async def test_load_async_caches_result(temp_config_file): @pytest.mark.asyncio -async def test_load_async_file_not_found(): - """Test async loading nonexistent config file falls back to bundled config.""" +async def test_load_async_file_not_found(caplog): + """Test async loading nonexistent config file returns empty dict.""" loader = ConfigLoader("/nonexistent/config.json", []) - config = await loader.load_async() + with caplog.at_level(logging.INFO, logger="mcp_cli.tools.config_loader"): + config = await loader.load_async() - # Should fall back to bundled package config (non-empty) - # or return empty dict if bundled config unavailable - assert isinstance(config, dict) + assert config == {} + assert any("Config file not found" in r.message for r in caplog.records) @pytest.mark.asyncio From ee6cdfdc828517da41a1c28bd5c0bb1641665136 Mon Sep 17 00:00:00 2001 From: aivibe <93047788+axisrow@users.noreply.github.com> Date: Mon, 2 Mar 2026 18:52:18 +0700 Subject: [PATCH 2/2] refactor: rename bundled server_config.json to example and update defaults - Rename server_config.json to server_config.example.json to clarify it's a template - Fix launcher tests to use the new example config path - Align default provider/model with README (ollama/gpt-oss) - Update OpenAI models to current lineup (GPT-5.2, GPT-4.1, o4-mini, o3-pro) Co-Authored-By: Claude Opus 4.6 --- README.md | 90 +++++++++++++++-------------- src/mcp_cli/chat/chat_context.py | 21 +++++-- src/mcp_cli/config/defaults.py | 4 +- src/mcp_cli/main.py | 23 ++++---- src/mcp_cli/run_command.py | 4 +- src/mcp_cli/tools/config_loader.py | 6 +- tests/config/test_config_manager.py | 4 +- 7 files changed, 82 insertions(+), 70 deletions(-) diff --git a/README.md b/README.md index bee839ac..8adb5727 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ The MCP CLI is built on a modular architecture with clean separation of concerns ### Advanced Chat Interface - **Streaming Responses**: Real-time response generation with live UI updates -- **Reasoning Visibility**: See AI's thinking process with reasoning models (gpt-oss, GPT-5, Claude 4.5) +- **Reasoning Visibility**: See AI's thinking process with reasoning models (gpt-oss, GPT-5.2, Claude 4.5) - **Concurrent Tool Execution**: Execute multiple tools simultaneously while preserving conversation order - **Smart Interruption**: Interrupt streaming responses or tool execution with Ctrl+C - **Performance Metrics**: Response timing, words/second, and execution statistics @@ -111,9 +111,9 @@ MCP CLI supports all providers and models from CHUK-LLM, including cutting-edge | Provider | Key Models | Special Features | |----------|------------|------------------| | **Ollama** (Default) | 🧠 gpt-oss, llama3.3, llama3.2, qwen3, qwen2.5-coder, deepseek-coder, granite3.3, mistral, gemma3, phi3, codellama | Local reasoning models, privacy-focused, no API key required | -| **OpenAI** | 🚀 GPT-5 family (gpt-5, gpt-5-mini, gpt-5-nano), GPT-4o family, O3 series (o3, o3-mini) | Advanced reasoning, function calling, vision | +| **OpenAI** | 🚀 GPT-5.2 family (gpt-5.2, gpt-5, gpt-5-mini), GPT-4.1 family (gpt-4.1, gpt-4.1-mini, gpt-4.1-nano), O-series (o3, o3-pro, o4-mini) | Advanced reasoning, function calling, vision | | **Anthropic** | 🧠 Claude 4.5 family (claude-4-5-opus, claude-4-5-sonnet), Claude 3.5 Sonnet | Enhanced reasoning, long context | -| **Azure OpenAI** 🏢 | Enterprise GPT-5, GPT-4 models | Private endpoints, compliance, audit logs | +| **Azure OpenAI** 🏢 | Enterprise GPT-5.2, GPT-4.1 models | Private endpoints, compliance, audit logs | | **Google Gemini** | Gemini 2.0 Flash, Gemini 1.5 Pro | Multimodal, fast inference | | **Groq** ⚡ | Llama 3.1 models, Mixtral | Ultra-fast inference (500+ tokens/sec) | | **Perplexity** 🌐 | Sonar models | Real-time web search with citations | @@ -203,9 +203,9 @@ Comprehensive documentation is available in the `docs/` directory: - Ollama: Install from [ollama.ai](https://ollama.ai) - Pull the default reasoning model: `ollama pull gpt-oss` - **For Cloud Providers** (Optional): - - OpenAI: `OPENAI_API_KEY` environment variable (for GPT-5, GPT-4, O3 models) + - OpenAI: `OPENAI_API_KEY` environment variable (for GPT-5.2, GPT-4.1, O-series models) - Anthropic: `ANTHROPIC_API_KEY` environment variable (for Claude 4.5, Claude 3.5) - - Azure: `AZURE_OPENAI_API_KEY` and `AZURE_OPENAI_ENDPOINT` (for enterprise GPT-5) + - Azure: `AZURE_OPENAI_API_KEY` and `AZURE_OPENAI_ENDPOINT` (for enterprise GPT-5.2) - Google: `GEMINI_API_KEY` (for Gemini models) - Groq: `GROQ_API_KEY` (for fast Llama models) - Custom providers: Provider-specific configuration @@ -265,18 +265,20 @@ mcp-cli --model granite3.3 # IBM Granite # === CLOUD PROVIDERS (API Keys Required) === -# GPT-5 Family (requires OpenAI API key) -mcp-cli --provider openai --model gpt-5 # Full GPT-5 with reasoning +# GPT-5.2 Family (requires OpenAI API key) +mcp-cli --provider openai --model gpt-5.2 # Latest GPT-5.2 with reasoning +mcp-cli --provider openai --model gpt-5 # GPT-5 base mcp-cli --provider openai --model gpt-5-mini # Efficient GPT-5 variant -mcp-cli --provider openai --model gpt-5-nano # Ultra-lightweight GPT-5 -# GPT-4 Family -mcp-cli --provider openai --model gpt-4o # GPT-4 Optimized -mcp-cli --provider openai --model gpt-4o-mini # Smaller GPT-4 +# GPT-4.1 Family +mcp-cli --provider openai --model gpt-4.1 # GPT-4.1 (replaces GPT-4o) +mcp-cli --provider openai --model gpt-4.1-mini # Efficient GPT-4.1 +mcp-cli --provider openai --model gpt-4.1-nano # Ultra-lightweight GPT-4.1 -# O3 Reasoning Models +# O-Series Reasoning Models mcp-cli --provider openai --model o3 # O3 reasoning -mcp-cli --provider openai --model o3-mini # Efficient O3 +mcp-cli --provider openai --model o3-pro # O3 Pro reasoning +mcp-cli --provider openai --model o4-mini # Efficient O4 # Claude 4.5 Family (requires Anthropic API key) mcp-cli --provider anthropic --model claude-4-5-opus # Most advanced Claude @@ -284,7 +286,7 @@ mcp-cli --provider anthropic --model claude-4-5-sonnet # Balanced Claude 4.5 mcp-cli --provider anthropic --model claude-3-5-sonnet # Claude 3.5 # Enterprise Azure (requires Azure configuration) -mcp-cli --provider azure_openai --model gpt-5 # Enterprise GPT-5 +mcp-cli --provider azure_openai --model gpt-5.2 # Enterprise GPT-5.2 # Other Providers mcp-cli --provider gemini --model gemini-2.0-flash # Google Gemini @@ -329,9 +331,9 @@ export LLM_PROVIDER=ollama # Default provider (already the default) export LLM_MODEL=gpt-oss # Default model (already the default) # For cloud providers (optional) -export OPENAI_API_KEY=sk-... # For GPT-5, GPT-4, O3 models +export OPENAI_API_KEY=sk-... # For GPT-5.2, GPT-4.1, O-series models export ANTHROPIC_API_KEY=sk-ant-... # For Claude 4.5, Claude 3.5 -export AZURE_OPENAI_API_KEY=sk-... # For enterprise GPT-5 +export AZURE_OPENAI_API_KEY=sk-... # For enterprise GPT-5.2 export AZURE_OPENAI_ENDPOINT=https://... export GEMINI_API_KEY=... # For Gemini models export GROQ_API_KEY=... # For Groq fast inference @@ -352,7 +354,7 @@ mcp-cli --server sqlite # See the AI's thinking process with reasoning models mcp-cli --server sqlite --model gpt-oss # Open-source reasoning -mcp-cli --server sqlite --provider openai --model gpt-5 # GPT-5 reasoning +mcp-cli --server sqlite --provider openai --model gpt-5.2 # GPT-5.2 reasoning mcp-cli --server sqlite --provider anthropic --model claude-4-5-opus # Claude 4.5 reasoning # Use different local models @@ -360,7 +362,7 @@ mcp-cli --server sqlite --model llama3.3 mcp-cli --server sqlite --model qwen2.5-coder # Switch to cloud providers (requires API keys) -mcp-cli chat --server sqlite --provider openai --model gpt-5 +mcp-cli chat --server sqlite --provider openai --model gpt-5.2 mcp-cli chat --server sqlite --provider anthropic --model claude-4-5-sonnet # Launch with real-time browser dashboard @@ -379,7 +381,7 @@ mcp-cli interactive --server sqlite # With specific models mcp-cli interactive --server sqlite --model gpt-oss # Local reasoning -mcp-cli interactive --server sqlite --provider openai --model gpt-5 # Cloud GPT-5 +mcp-cli interactive --server sqlite --provider openai --model gpt-5.2 # Cloud GPT-5.2 ``` ### 3. Command Mode @@ -390,8 +392,8 @@ Unix-friendly interface for automation and scripting: # Process text with reasoning models mcp-cli cmd --server sqlite --model gpt-oss --prompt "Think through this step by step" --input data.txt -# Use GPT-5 for complex reasoning -mcp-cli cmd --server sqlite --provider openai --model gpt-5 --prompt "Analyze this data" --input data.txt +# Use GPT-5.2 for complex reasoning +mcp-cli cmd --server sqlite --provider openai --model gpt-5.2 --prompt "Analyze this data" --input data.txt # Execute tools directly mcp-cli cmd --server sqlite --tool list_tables --output tables.json @@ -415,7 +417,7 @@ mcp-cli provider list mcp-cli models # Show models for specific provider -mcp-cli models openai # Shows GPT-5, GPT-4, O3 models +mcp-cli models openai # Shows GPT-5.2, GPT-4.1, O-series models mcp-cli models anthropic # Shows Claude 4.5, Claude 3.5 models mcp-cli models ollama # Shows gpt-oss, llama3.3, etc. @@ -491,7 +493,7 @@ mcp-cli --server sqlite mcp-cli --server sqlite,filesystem # With advanced reasoning models -mcp-cli --server sqlite --provider openai --model gpt-5 +mcp-cli --server sqlite --provider openai --model gpt-5.2 mcp-cli --server sqlite --provider anthropic --model claude-4-5-opus ``` @@ -506,7 +508,7 @@ mcp-cli --server sqlite --provider anthropic --model claude-4-5-opus /provider set ollama api_base http://localhost:11434 # Configure Ollama endpoint /provider openai # Switch to OpenAI (requires API key) /provider anthropic # Switch to Anthropic (requires API key) -/provider openai gpt-5 # Switch to OpenAI GPT-5 +/provider openai gpt-5.2 # Switch to OpenAI GPT-5.2 # Custom Provider Management /provider custom # List custom providers @@ -515,7 +517,7 @@ mcp-cli --server sqlite --provider anthropic --model claude-4-5-opus /model # Show current model (default: gpt-oss) /model llama3.3 # Switch to different Ollama model -/model gpt-5 # Switch to GPT-5 (if using OpenAI) +/model gpt-5.2 # Switch to GPT-5.2 (if using OpenAI) /model claude-4-5-opus # Switch to Claude 4.5 (if using Anthropic) /models # List available models for current provider ``` @@ -654,7 +656,7 @@ See [Token Management Guide](docs/TOKEN_MANAGEMENT.md) for comprehensive documen ### Chat Features #### Streaming Responses with Reasoning Visibility -- **🧠 Reasoning Models**: See the AI's thinking process with gpt-oss, GPT-5, Claude 4 +- **🧠 Reasoning Models**: See the AI's thinking process with gpt-oss, GPT-5.2, Claude 4 - **Real-time Generation**: Watch text appear token by token - **Performance Metrics**: Words/second, response time - **Graceful Interruption**: Ctrl+C to stop streaming @@ -701,7 +703,7 @@ clear # Clear terminal provider # Show current provider provider list # List providers provider anthropic # Switch provider -provider openai gpt-5 # Switch to GPT-5 +provider openai gpt-5.2 # Switch to GPT-5.2 # Model management model # Show current model @@ -745,8 +747,8 @@ Command mode provides Unix-friendly automation capabilities. # Text processing with reasoning models echo "Analyze this data" | mcp-cli cmd --server sqlite --model gpt-oss --input - --output analysis.txt -# Use GPT-5 for complex analysis -mcp-cli cmd --server sqlite --provider openai --model gpt-5 --prompt "Provide strategic analysis" --input report.txt +# Use GPT-5.2 for complex analysis +mcp-cli cmd --server sqlite --provider openai --model gpt-5.2 --prompt "Provide strategic analysis" --input report.txt # Tool execution mcp-cli cmd --server sqlite --tool list_tables --raw @@ -792,13 +794,13 @@ mcp-cli provider set ollama api_base http://remote-server:11434 To use cloud providers with advanced models, configure API keys: ```bash -# Configure OpenAI (for GPT-5, GPT-4, O3 models) +# Configure OpenAI (for GPT-5.2, GPT-4.1, O-series models) mcp-cli provider set openai api_key sk-your-key-here # Configure Anthropic (for Claude 4.5, Claude 3.5) mcp-cli provider set anthropic api_key sk-ant-your-key-here -# Configure Azure OpenAI (for enterprise GPT-5) +# Configure Azure OpenAI (for enterprise GPT-5.2) mcp-cli provider set azure_openai api_key sk-your-key-here mcp-cli provider set azure_openai api_base https://your-resource.openai.azure.com @@ -851,7 +853,7 @@ ollama: openai: api_base: https://api.openai.com/v1 - default_model: gpt-5 + default_model: gpt-5.2 anthropic: api_base: https://api.anthropic.com @@ -859,7 +861,7 @@ anthropic: azure_openai: api_base: https://your-resource.openai.azure.com - default_model: gpt-5 + default_model: gpt-5.2 gemini: api_base: https://generativelanguage.googleapis.com @@ -1051,9 +1053,9 @@ mcp-cli [See the complete thinking process with gpt-oss] > /provider openai -> /model gpt-5 +> /model gpt-5.2 > Think through this problem step by step: If a train leaves New York at 3 PM... -[See GPT-5's reasoning approach] +[See GPT-5.2's reasoning approach] > /provider anthropic > /model claude-4-5-opus @@ -1080,7 +1082,7 @@ mcp-cli chat --server sqlite # Switch to cloud when needed (requires API keys) > /provider openai -> /model gpt-5 +> /model gpt-5.2 > Complex enterprise architecture design... > /provider anthropic @@ -1102,7 +1104,7 @@ mcp-cli chat --server sqlite > /provider ollama > What's the best way to optimize this SQL query? -> /provider openai gpt-5 # Requires API key +> /provider openai gpt-5.2 # Requires API key > What's the best way to optimize this SQL query? > /provider anthropic claude-4-5-sonnet # Requires API key @@ -1110,7 +1112,7 @@ mcp-cli chat --server sqlite # Use each provider's strengths > /provider ollama gpt-oss # Local reasoning, privacy -> /provider openai gpt-5 # Advanced reasoning +> /provider openai gpt-5.2 # Advanced reasoning > /provider anthropic claude-4-5-opus # Deep analysis > /provider groq llama-3.1-70b # Ultra-fast responses ``` @@ -1167,9 +1169,9 @@ cat complex_problem.txt | \ Provider Diagnostics Provider | Status | Response Time | Features | Models ollama | ✅ Ready | 56ms | 📡🔧 | gpt-oss, llama3.3, qwen3, ... -openai | ✅ Ready | 234ms | 📡🔧👁️ | gpt-5, gpt-4o, o3, ... +openai | ✅ Ready | 234ms | 📡🔧👁️ | gpt-5.2, gpt-4.1, o3, ... anthropic | ✅ Ready | 187ms | 📡🔧 | claude-4-5-opus, claude-4-5-sonnet, ... -azure_openai | ✅ Ready | 198ms | 📡🔧👁️ | gpt-5, gpt-4o, ... +azure_openai | ✅ Ready | 198ms | 📡🔧👁️ | gpt-5.2, gpt-4.1, ... gemini | ✅ Ready | 156ms | 📡🔧👁️ | gemini-2.0-flash, ... groq | ✅ Ready | 45ms | 📡🔧 | llama-3.1-70b, ... @@ -1217,7 +1219,7 @@ granite3.3 | Available ollama list # For cloud providers, check supported models - mcp-cli models openai # Shows GPT-5, GPT-4, O3 models + mcp-cli models openai # Shows GPT-5.2, GPT-4.1, O-series models mcp-cli models anthropic # Shows Claude 4.5, Claude 3.5 models ``` @@ -1305,7 +1307,7 @@ mcp-cli --log-file ~/.mcp-cli/logs/debug.log --server sqlite ### Runtime Performance - **Local Processing**: Default Ollama provider minimizes latency -- **Reasoning Visibility**: See AI thinking process with gpt-oss, GPT-5, Claude 4 +- **Reasoning Visibility**: See AI thinking process with gpt-oss, GPT-5.2, Claude 4 - **Streaming Responses**: Real-time response generation - **Connection Pooling**: Efficient reuse of client connections - **Caching**: Tool metadata and provider configurations are cached @@ -1392,7 +1394,7 @@ This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENS ## 🙏 Acknowledgments - **[CHUK Tool Processor](https://github.com/chrishayuk/chuk-tool-processor)** - Production-grade async tool execution with middleware and observability -- **[CHUK-LLM](https://github.com/chrishayuk/chuk-llm)** - Unified LLM provider with dynamic model discovery, llama.cpp integration, and GPT-5/Claude 4.5 support (v0.17+) +- **[CHUK-LLM](https://github.com/chrishayuk/chuk-llm)** - Unified LLM provider with dynamic model discovery, llama.cpp integration, and GPT-5.2/Claude 4.5 support (v0.17+) - **[CHUK-Term](https://github.com/chrishayuk/chuk-term)** - Enhanced terminal UI with themes and cross-platform support - **[Rich](https://github.com/Textualize/rich)** - Beautiful terminal formatting - **[Typer](https://typer.tiangolo.com/)** - CLI framework @@ -1403,5 +1405,5 @@ This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENS - **[Model Context Protocol](https://modelcontextprotocol.io/)** - Core protocol specification - **[MCP Servers](https://github.com/modelcontextprotocol/servers)** - Official MCP server implementations - **[CHUK Tool Processor](https://github.com/chrishayuk/chuk-tool-processor)** - Production-grade tool execution with middleware and observability -- **[CHUK-LLM](https://github.com/chrishayuk/chuk-llm)** - LLM provider abstraction with dynamic model discovery, GPT-5, Claude 4.5, O3 series support, and llama.cpp integration +- **[CHUK-LLM](https://github.com/chrishayuk/chuk-llm)** - LLM provider abstraction with dynamic model discovery, GPT-5.2, Claude 4.5, O-series support, and llama.cpp integration - **[CHUK-Term](https://github.com/chrishayuk/chuk-term)** - Terminal UI library with themes and cross-platform support \ No newline at end of file diff --git a/src/mcp_cli/chat/chat_context.py b/src/mcp_cli/chat/chat_context.py index 768c2469..8f6bde5d 100644 --- a/src/mcp_cli/chat/chat_context.py +++ b/src/mcp_cli/chat/chat_context.py @@ -240,8 +240,17 @@ def create( # ── Properties ──────────────────────────────────────────────────────── @property def client(self) -> Any: - """Get current LLM client (cached automatically by ModelManager).""" - return self.model_manager.get_client() + """Get current LLM client (cached automatically by ModelManager). + + Returns None if the client cannot be created (e.g. missing API key) + so the UI can start without crashing. The error is logged and the user + will see a proper message when they try to send a message. + """ + try: + return self.model_manager.get_client() + except Exception as e: + logger.error(f"Failed to create client for {self.provider}: {e}") + return None @property def provider(self) -> str: @@ -460,11 +469,11 @@ async def initialize( await self._initialize_session() # Quick provider validation (non-blocking) - try: - _client = self.client # noqa: F841 — fails fast if no API key + _client = self.client # None if client could not be created + if _client is not None: logger.info(f"Provider {self.provider} client created successfully") - except Exception as e: - logger.warning(f"Provider validation warning: {e}") + else: + logger.warning("Provider validation warning: client could not be created.") logger.warning("Chat may fail when making API calls.") if not self.tools: diff --git a/src/mcp_cli/config/defaults.py b/src/mcp_cli/config/defaults.py index a6ed2e08..80220864 100644 --- a/src/mcp_cli/config/defaults.py +++ b/src/mcp_cli/config/defaults.py @@ -176,10 +176,10 @@ # Provider/Model Defaults # ================================================================ -DEFAULT_PROVIDER = "openai" +DEFAULT_PROVIDER = "ollama" """Default LLM provider.""" -DEFAULT_MODEL = "gpt-4o-mini" +DEFAULT_MODEL = "gpt-oss" """Default LLM model.""" diff --git a/src/mcp_cli/main.py b/src/mcp_cli/main.py index 6b943ee7..cf5cde01 100644 --- a/src/mcp_cli/main.py +++ b/src/mcp_cli/main.py @@ -30,7 +30,7 @@ restore_terminal, ) from chuk_term.ui.theme import set_theme -from mcp_cli.config import process_options, APP_VERSION, DEFAULT_CONFIG_FILENAME +from mcp_cli.config import process_options, APP_VERSION, DEFAULT_CONFIG_FILENAME, DEFAULT_PROVIDER, DEFAULT_MODEL from mcp_cli.context import initialize_context # ────────────────────────────────────────────────────────────────────────────── @@ -345,11 +345,11 @@ def main_callback( f"Using current provider with specified model: {effective_provider}/{model}" ) else: - # Neither specified, use active configuration - effective_provider = model_manager.get_active_provider() - effective_model = model_manager.get_active_model() + # Neither specified — use declared project defaults + effective_provider = DEFAULT_PROVIDER + effective_model = DEFAULT_MODEL logger.debug( - f"Using active configuration: {effective_provider}/{effective_model}" + f"Using default configuration: {effective_provider}/{effective_model}" ) servers, _, server_names = process_options( @@ -649,8 +649,9 @@ def _chat_command( effective_provider = model_manager.get_active_provider() effective_model = model else: - effective_provider = model_manager.get_active_provider() - effective_model = model_manager.get_active_model() + # Neither specified — use declared project defaults + effective_provider = DEFAULT_PROVIDER + effective_model = DEFAULT_MODEL servers, _, server_names = process_options( server, @@ -818,11 +819,11 @@ def _interactive_command( f"Using current provider with specified model: {effective_provider}/{model}" ) else: - # Neither specified, use active configuration - effective_provider = model_manager.get_active_provider() - effective_model = model_manager.get_active_model() + # Neither specified — use declared project defaults + effective_provider = DEFAULT_PROVIDER + effective_model = DEFAULT_MODEL logger.debug( - f"Using active configuration: {effective_provider}/{effective_model}" + f"Using default configuration: {effective_provider}/{effective_model}" ) servers, _, server_names = process_options( diff --git a/src/mcp_cli/run_command.py b/src/mcp_cli/run_command.py index 692a9343..a1c4494e 100644 --- a/src/mcp_cli/run_command.py +++ b/src/mcp_cli/run_command.py @@ -308,8 +308,8 @@ def cli_entry( server: list[str] = typer.Option( ["sqlite"], "--server", "-s", help="Server(s) to connect" ), - provider: str = typer.Option("openai", help="LLM provider name"), - model: str = typer.Option("gpt-4o-mini", help="LLM model name"), + provider: str = typer.Option("ollama", help="LLM provider name"), + model: str = typer.Option("gpt-oss", help="LLM model name"), init_timeout: float = typer.Option( 120.0, "--init-timeout", help="Server initialization timeout in seconds" ), diff --git a/src/mcp_cli/tools/config_loader.py b/src/mcp_cli/tools/config_loader.py index 12c76857..92f1a5a4 100644 --- a/src/mcp_cli/tools/config_loader.py +++ b/src/mcp_cli/tools/config_loader.py @@ -183,13 +183,13 @@ def process_value(value: Any) -> Any: ) return token_value else: - logger.warning( + logger.debug( f"Token {namespace}:{name} has no token value in data" ) else: - logger.warning(f"Token not found: {namespace}:{name}") + logger.debug(f"Token not found: {namespace}:{name}") except Exception as e: - logger.warning( + logger.debug( f"Failed to get token {namespace}:{name}: {e}" ) diff --git a/tests/config/test_config_manager.py b/tests/config/test_config_manager.py index 37beb5e2..b6720728 100644 --- a/tests/config/test_config_manager.py +++ b/tests/config/test_config_manager.py @@ -134,8 +134,8 @@ def test_mcp_config_defaults(self): """Test MCPConfig default values.""" config = MCPConfig() assert config.servers == {} - assert config.default_provider == "openai" - assert config.default_model == "gpt-4o-mini" + assert config.default_provider == "ollama" + assert config.default_model == "gpt-oss" assert config.theme == "default" assert config.verbose is True assert config.confirm_tools is True