From ad9a0cb5b62f99eaccc3e08b2ab2cbfe92708ae5 Mon Sep 17 00:00:00 2001 From: datYori Date: Wed, 25 Jun 2025 17:23:45 +0200 Subject: [PATCH 1/4] feat(cli): add Cursor IDE integration to init command Add Cursor IDE support to `mxcp init` with auto-detection, config generation, one-click deeplinks, and feature parity with Claude Desktop integration. - Cross-platform Cursor detection and configuration - Project-specific and global MCP setup options - Correct cursor://anysphere.cursor-deeplink/mcp/install deeplink format - Safe config merging with existing Cursor configurations - Complete documentation and test coverage Closes #41 --- README.md | 2 + docs/guides/integrations.md | 87 ++++++++++- examples/covid_owid/README.md | 55 ++++++- examples/earthquakes/README.md | 55 ++++++- pyproject.toml | 2 +- src/mxcp/cli/init.py | 246 +++++++++++++++++++++++++++--- tests/test_cli_init.py | 264 +++++++++++++++++++++++++++++++-- 7 files changed, 668 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index abec5f89..39b59d42 100644 --- a/README.md +++ b/README.md @@ -596,12 +596,14 @@ mxcp run # Run a specific endpoint MXCP implements the Model Context Protocol (MCP), making it compatible with: - **Claude Desktop** — Native MCP support +- **Cursor IDE** — Native MCP support with one-click installation - **OpenAI-compatible tools** — Via MCP adapters - **Custom integrations** — Using the MCP specification For specific setup instructions, see: - [Earthquakes Example](https://github.com/raw-labs/mxcp/blob/main/examples/earthquakes/README.md) — Complete Claude Desktop setup - [COVID + dbt Example](https://github.com/raw-labs/mxcp/blob/main/examples/covid_owid/README.md) — Advanced dbt integration +- [Integrations Guide](https://github.com/raw-labs/mxcp/blob/main/docs/guides/integrations.md) — Claude Desktop and Cursor IDE setup ## 📚 Documentation diff --git a/docs/guides/integrations.md b/docs/guides/integrations.md index 29eb620a..06f139dc 100644 --- a/docs/guides/integrations.md +++ b/docs/guides/integrations.md @@ -1,9 +1,10 @@ --- title: "Integrations" -description: "Integrate MXCP with AI platforms, dbt, and data sources. Connect with Claude Desktop, OpenAI, and other LLM providers. Access diverse data sources through DuckDB." +description: "Integrate MXCP with AI platforms, dbt, and data sources. Connect with Claude Desktop, Cursor IDE, OpenAI, and other LLM providers. Access diverse data sources through DuckDB." keywords: - mxcp integrations - claude desktop integration + - cursor ide integration - dbt integration - duckdb extensions - llm integration @@ -18,7 +19,7 @@ MXCP provides seamless integration with AI platforms and data tools to create po ## Table of Contents -- [LLM Integration](#llm-integration) — Connect with Claude Desktop, OpenAI, and other AI platforms +- [LLM Integration](#llm-integration) — Connect with Claude Desktop, Cursor IDE, OpenAI, and other AI platforms - [dbt Integration](#dbt-integration) — Transform and prepare data for AI consumption - [DuckDB Integration](#duckdb-integration) — Access diverse data sources with powerful SQL capabilities @@ -74,6 +75,88 @@ Claude Desktop has native MCP support, making it the easiest way to get started - Test your configuration with simple queries first - Monitor Claude's developer console for connection issues +### Cursor IDE + +Cursor IDE has native MCP support through its Model Context Protocol integration, providing another excellent way to use MXCP with AI-powered development tools. + +#### Automatic Configuration + +The easiest way to set up Cursor IDE integration is during project initialization: + +```bash +# Initialize MXCP project with IDE configuration +mxcp init my-project --bootstrap + +# Follow the prompts to configure Cursor IDE automatically +# Choose from: +# 1. Project-specific (recommended) - Only available in this project +# 2. Global - Available in all Cursor workspaces +``` + +#### Manual Configuration + +For manual setup or existing projects: + +1. **Generate Cursor configuration**: + ```bash + # Run init again in existing project to add Cursor config + cd my-existing-project + mxcp init . + ``` + +2. **Configuration locations**: + - **Project-specific**: `.cursor/mcp.json` (created in your project directory) + - **Global**: `~/.cursor/mcp.json` (available in all Cursor workspaces) + +3. **Manual configuration file**: + + For global installations: + ```json + { + "mcpServers": { + "my-project": { + "command": "mxcp", + "args": ["serve", "--transport", "stdio"], + "cwd": "/absolute/path/to/your/mxcp/project" + } + } + } + ``` + + For virtual environment installations: + ```json + { + "mcpServers": { + "my-project": { + "command": "bash", + "args": [ + "-c", + "cd /absolute/path/to/your/project && source /path/to/.venv/bin/activate && mxcp serve --transport stdio" + ] + } + } + } + ``` + +#### One-Click Installation + +MXCP generates one-click installation links for easy sharing: + +```bash +# The deeplink is automatically generated during init +# Example format: +cursor://anysphere.cursor-deeplink/mcp/install?name=my-project&config=eyJjb21tYW5kIjoi... +``` + +Share this link with team members for instant Cursor IDE setup. + +#### Best Practices + +- **Project-specific configuration** is recommended for team projects +- **Global configuration** is useful for personal tools used across multiple projects +- Use the one-click installation links for team onboarding +- Test your configuration by asking Cursor to list available tools + ### OpenAI and Other Providers While MXCP uses the MCP protocol, you can integrate with OpenAI and other providers using MCP adapters or custom implementations. diff --git a/examples/covid_owid/README.md b/examples/covid_owid/README.md index 01994999..c951a071 100644 --- a/examples/covid_owid/README.md +++ b/examples/covid_owid/README.md @@ -44,7 +44,9 @@ pip install -e . mxcp serve ``` -## 🔌 Claude Desktop Integration +## 🔌 Integration + +### Claude Desktop To use this example with Claude Desktop: @@ -99,6 +101,57 @@ In Claude Desktop, try asking: - "Compare vaccination rates between France and Germany" - "What were the peak hospitalization rates in the UK?" +### Cursor + +To use this example with Cursor: + +#### Option 1: Automatic Setup (Recommended) + +Run `mxcp init .` in the covid_owid directory and follow the prompts to automatically configure Cursor. + +#### Option 2: Manual Setup + +1. **Locate Cursor's Configuration**: + - **Project-specific**: Create `.cursor/mcp.json` in the covid_owid directory + - **Global**: `~/.cursor/mcp.json` in your home directory + +2. **Add Configuration**: + + For global installations: + ```json + { + "mcpServers": { + "covid": { + "command": "mxcp", + "args": ["serve", "--transport", "stdio"], + "cwd": "/absolute/path/to/mxcp/examples/covid_owid" + } + } + } + ``` + + For virtual environment installations: + ```json + { + "mcpServers": { + "covid": { + "command": "/bin/bash", + "args": [ + "-c", + "cd /absolute/path/to/mxcp/examples/covid_owid && source ../../.venv/bin/activate && mxcp serve --transport stdio" + ] + } + } + } + ``` + +3. **Test the Integration**: + +In Cursor, try asking: +- "Show me COVID-19 cases in the United States for 2022" +- "Compare vaccination rates between France and Germany" +- "What were the peak hospitalization rates in the UK?" + ## 🛠️ Other MCP Clients This example works with any MCP-compatible tool: diff --git a/examples/earthquakes/README.md b/examples/earthquakes/README.md index 7c1ff9ef..a249fdf0 100644 --- a/examples/earthquakes/README.md +++ b/examples/earthquakes/README.md @@ -39,7 +39,9 @@ pip install -e . mxcp serve ``` -## 🔌 Claude Desktop Integration +## 🔌 Integration + +### Claude Desktop To use this example with Claude Desktop: @@ -96,6 +98,57 @@ In Claude Desktop, try asking: Claude will automatically use the earthquake data tools to answer your questions. +### Cursor + +To use this example with Cursor: + +#### Option 1: Automatic Setup (Recommended) + +Run `mxcp init .` in the earthquakes directory and follow the prompts to automatically configure Cursor. + +#### Option 2: Manual Setup + +1. **Locate Cursor's Configuration**: + - **Project-specific**: Create `.cursor/mcp.json` in the earthquakes directory + - **Global**: `~/.cursor/mcp.json` in your home directory + +2. **Add Configuration**: + + For global installations: + ```json + { + "mcpServers": { + "earthquakes": { + "command": "mxcp", + "args": ["serve", "--transport", "stdio"], + "cwd": "/absolute/path/to/mxcp/examples/earthquakes" + } + } + } + ``` + + For virtual environment installations: + ```json + { + "mcpServers": { + "earthquakes": { + "command": "/bin/bash", + "args": [ + "-c", + "cd /absolute/path/to/mxcp/examples/earthquakes && source ../../.venv/bin/activate && mxcp serve --transport stdio" + ] + } + } + } + ``` + +3. **Test the Integration**: + +In Cursor, try asking: +- "Show me recent earthquakes above magnitude 5.0" +- "What was the strongest earthquake in the last 24 hours?" +- "List earthquakes near California" + ## 🛠️ Other MCP Clients This example also works with other MCP-compatible tools: diff --git a/pyproject.toml b/pyproject.toml index 63456e7b..f54d0e5d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "mxcp" -version = "0.2.1" +version = "0.2.2" description = "Enterprise MCP framework for building production AI tools with SQL/Python, featuring security, audit trails, and policy enforcement" authors = [{ name = "RAW Labs SA", email = "mxcp@raw-labs.com" }] readme = "README.md" diff --git a/src/mxcp/cli/init.py b/src/mxcp/cli/init.py index b63d27e7..b4cc20ea 100644 --- a/src/mxcp/cli/init.py +++ b/src/mxcp/cli/init.py @@ -5,10 +5,12 @@ import sys import json import shutil +import platform +import base64 from mxcp.cli.utils import output_error, configure_logging, get_env_flag, get_env_profile from mxcp.config.user_config import load_user_config from mxcp.config.analytics import track_command_with_timing -from typing import Optional +from typing import Optional, Dict, List def check_existing_mxcp_repo(target_dir: Path) -> bool: """Check if there's a mxcp-site.yml in the target directory or any parent directory.""" @@ -185,7 +187,122 @@ def generate_claude_config(project_dir: Path, project_name: str): return config -def show_next_steps(project_dir: Path, project_name: str, bootstrap: bool, config_generated: bool = True): +def detect_cursor_installation() -> Optional[Dict[str, str]]: + """Detect Cursor IDE installation and return relevant paths.""" + system = platform.system().lower() + cursor_info = {} + + # Common Cursor executable names + cursor_executables = ["cursor", "cursor.exe"] if system == "windows" else ["cursor"] + + # Check if Cursor is in PATH + cursor_path = None + for executable in cursor_executables: + cursor_path = shutil.which(executable) + if cursor_path: + break + + if cursor_path: + cursor_info["executable"] = cursor_path + + # Determine config directory based on OS + home = Path.home() + if system == "windows": + cursor_config_dir = home / "AppData" / "Roaming" / "Cursor" / "User" + elif system == "darwin": # macOS + cursor_config_dir = home / "Library" / "Application Support" / "Cursor" / "User" + else: # Linux and other Unix-like + cursor_config_dir = home / ".config" / "Cursor" / "User" + + if cursor_config_dir.exists(): + cursor_info["config_dir"] = str(cursor_config_dir) + cursor_info["global_mcp_config"] = str(home / ".cursor" / "mcp.json") + + return cursor_info if cursor_info else None + +def generate_cursor_config(project_dir: Path, project_name: str) -> Dict: + """Generate Cursor MCP configuration (same format as Claude Desktop).""" + return generate_claude_config(project_dir, project_name) + +def generate_cursor_deeplink(config: Dict, project_name: str) -> str: + """Generate Cursor deeplink for one-click installation.""" + # Extract just the server config for the deeplink (no mcpServers wrapper) + server_config = config["mcpServers"][project_name] + + # Base64 encode the configuration + config_json = json.dumps(server_config) + encoded_config = base64.b64encode(config_json.encode()).decode() + + # Generate the deeplink using the correct cursor:// protocol + deeplink = f"cursor://anysphere.cursor-deeplink/mcp/install?name={project_name}&config={encoded_config}" + + return deeplink + +def install_cursor_config(config: Dict, project_name: str, install_type: str = "project", project_dir: Optional[Path] = None) -> bool: + """Install Cursor MCP configuration. + + Args: + config: The MCP configuration to install + project_name: Name of the project + install_type: Either "project" or "global" + project_dir: Project directory (required for project install) + + Returns: + True if installation was successful, False otherwise + """ + try: + if install_type == "project": + if not project_dir: + return False + cursor_dir = project_dir / ".cursor" + cursor_dir.mkdir(exist_ok=True) + config_path = cursor_dir / "mcp.json" + else: # global + cursor_dir = Path.home() / ".cursor" + cursor_dir.mkdir(exist_ok=True) + config_path = cursor_dir / "mcp.json" + + # If config file exists, merge with existing configuration + existing_config = {} + if config_path.exists(): + try: + with open(config_path) as f: + existing_config = json.load(f) + except (json.JSONDecodeError, Exception): + # If we can't read existing config, start fresh + existing_config = {} + + # Merge configurations + if "mcpServers" not in existing_config: + existing_config["mcpServers"] = {} + + existing_config["mcpServers"][project_name] = config["mcpServers"][project_name] + + # Write updated configuration + with open(config_path, 'w') as f: + json.dump(existing_config, f, indent=2) + + return True + except Exception: + return False + +def show_cursor_next_steps(project_name: str, install_type: str): + """Show Cursor-specific next steps.""" + click.echo(f"\n{click.style('📝 Cursor IDE Manual Setup:', fg='cyan', bold=True)}") + + click.echo(f" 📋 To install manually:") + click.echo(f" 1. Open Cursor IDE") + click.echo(f" 2. Go to Settings > Features > Model Context Protocol") + click.echo(f" 3. Add the configuration shown above, or") + click.echo(f" 4. Use the one-click install link provided above") + + click.echo(f"\n 🚀 After installation:") + click.echo(f" • Restart Cursor IDE") + click.echo(f" • Open the Agent/Chat") + click.echo(f" • The '{project_name}' MCP server will be automatically available") + click.echo(f" • Try asking: \"List the available tools from {project_name}\"") + +def show_next_steps(project_dir: Path, project_name: str, bootstrap: bool, config_generated: bool = True, cursor_configured: bool = False, cursor_install_type: str = None): """Show helpful next steps after initialization.""" click.echo("\n" + "="*60) click.echo(click.style("✨ MXCP project initialized successfully!", fg='green', bold=True)) @@ -234,20 +351,38 @@ def show_next_steps(project_dir: Path, project_name: str, bootstrap: bool, confi click.echo(f"\n{click.style('2. Start the MCP server:', fg='yellow')}") click.echo(f" mxcp serve") - # Step 3: Connect to Claude - click.echo(f"\n{click.style('3. Connect to Claude Desktop:', fg='yellow')}") - if config_generated: + # Step 3: Connect to IDE + if config_generated and cursor_configured: + click.echo(f"\n{click.style('3. Connect to your preferred IDE:', fg='yellow')}") + click.echo(f" 🔹 Claude Desktop: Add server_config.json to Claude config") + click.echo(f" 🔹 Cursor IDE: Already configured! Open Cursor and start using.") + elif config_generated: + click.echo(f"\n{click.style('3. Connect to Claude Desktop:', fg='yellow')}") click.echo(f" Add the generated server_config.json to your Claude Desktop config") + elif cursor_configured: + click.echo(f"\n{click.style('3. Connect to Cursor IDE:', fg='yellow')}") + click.echo(f" Already configured! Open Cursor and start using.") else: - click.echo(f" Create a server configuration for Claude Desktop") - click.echo(f" Run 'mxcp init .' again to generate server_config.json") - click.echo(f" Config location:") - if sys.platform == "darwin": - click.echo(f" ~/Library/Application Support/Claude/claude_desktop_config.json") - elif sys.platform == "win32": - click.echo(f" %APPDATA%\\Claude\\claude_desktop_config.json") - else: - click.echo(f" ~/.config/Claude/claude_desktop_config.json") + click.echo(f"\n{click.style('3. Connect to your IDE:', fg='yellow')}") + click.echo(f" Create configurations for Claude Desktop or Cursor IDE") + click.echo(f" Run 'mxcp init .' again to generate configurations") + + if config_generated or cursor_configured: + # Show IDE-specific config locations + click.echo(f" Config locations:") + if config_generated: + if sys.platform == "darwin": + click.echo(f" • Claude Desktop: ~/Library/Application Support/Claude/claude_desktop_config.json") + elif sys.platform == "win32": + click.echo(f" • Claude Desktop: %APPDATA%\\Claude\\claude_desktop_config.json") + else: + click.echo(f" • Claude Desktop: ~/.config/Claude/claude_desktop_config.json") + + if cursor_configured: + if cursor_install_type == "project": + click.echo(f" • Cursor IDE: {project_dir}/.cursor/mcp.json (project-specific)") + else: + click.echo(f" • Cursor IDE: ~/.cursor/mcp.json (global)") # Step 4: Explore more click.echo(f"\n{click.style('4. Learn more:', fg='yellow')}") @@ -259,7 +394,12 @@ def show_next_steps(project_dir: Path, project_name: str, bootstrap: bool, confi if bootstrap: click.echo(f"\n{click.style('💡 Try it now:', fg='green')}") - click.echo(f" In Claude Desktop, ask: \"Use the hello_world tool to greet Alice\"") + if config_generated and cursor_configured: + click.echo(f" In Claude Desktop or Cursor IDE, ask: \"Use the hello_world tool to greet Alice\"") + elif config_generated: + click.echo(f" In Claude Desktop, ask: \"Use the hello_world tool to greet Alice\"") + elif cursor_configured: + click.echo(f" In Cursor IDE, ask: \"Use the hello_world tool to greet Alice\"") click.echo(f"\n{click.style('📚 Resources:', fg='cyan', bold=True)}") click.echo(f" • Documentation: https://mxcp.dev") @@ -281,7 +421,7 @@ def init(folder: str, project: str, profile: str, bootstrap: bool, debug: bool): This command creates a new MXCP repository by: 1. Creating a mxcp-site.yml file with project and profile configuration 2. Optionally creating example endpoint files - 3. Generating a server_config.json for Claude Desktop integration + 3. Generating configurations for Claude Desktop and/or Cursor IDE integration \b Examples: @@ -347,10 +487,14 @@ def init(folder: str, project: str, profile: str, bootstrap: bool, debug: bool): click.echo("✓ Initialized DuckDB database") except Exception as e: click.echo(f"⚠️ Warning: Failed to initialize DuckDB database: {e}") - + + # IDE Configuration Generation + config_generated = False + cursor_configured = False + cursor_install_type = None + # Generate Claude Desktop config try: - # Ask if user wants to generate Claude Desktop config if click.confirm("\nWould you like to generate a Claude Desktop configuration file?"): claude_config = generate_claude_config(target_dir, project) config_path = target_dir / "server_config.json" @@ -361,18 +505,76 @@ def init(folder: str, project: str, profile: str, bootstrap: bool, debug: bool): click.echo(f"✓ Generated server_config.json for Claude Desktop") # Show the config content - click.echo("\nGenerated configuration:") + click.echo("\nGenerated Claude Desktop configuration:") click.echo(json.dumps(claude_config, indent=2)) config_generated = True else: click.echo("ℹ️ Skipped Claude Desktop configuration generation") - config_generated = False except Exception as e: click.echo(f"⚠️ Warning: Failed to generate Claude config: {e}") - config_generated = False + + # Generate Cursor IDE config + try: + if click.confirm("\nWould you like to set up Cursor IDE integration?"): + cursor_config = generate_cursor_config(target_dir, project) + + # Generate deeplink by default + deeplink = generate_cursor_deeplink(cursor_config, project) + + # Detect Cursor installation + cursor_info = detect_cursor_installation() + + if cursor_info: + click.echo(f"✓ Detected Cursor IDE installation") + + # Offer installation options + click.echo("\nChoose Cursor configuration option:") + click.echo("1. Auto-configure (recommended) - Install directly to your Cursor config") + click.echo("2. Manual setup - Copy configuration manually") + + choice = click.prompt("Enter your choice", type=click.Choice(['1', '2']), default='1') + + if choice == '1': + # Ask for installation scope + scope_choice = click.prompt( + "Installation scope:\n1. Project-specific (only this project)\n2. Global (all Cursor workspaces)\nEnter choice", + type=click.Choice(['1', '2']), default='1' + ) + + install_type = "project" if scope_choice == '1' else "global" + + if install_cursor_config(cursor_config, project, install_type, target_dir): + click.echo(f"✓ Configured Cursor MCP server ({'project-specific' if install_type == 'project' else 'globally'})") + cursor_configured = True + cursor_install_type = install_type + else: + click.echo(f"⚠️ Failed to configure Cursor automatically, using manual setup") + cursor_install_type = "manual" + else: + cursor_install_type = "manual" + else: + # Cursor not detected, provide manual setup + click.echo("⚠️ Cursor IDE not detected in PATH") + cursor_install_type = "manual" + + # Show the config content + click.echo(f"\n📋 Cursor IDE Configuration:") + click.echo(json.dumps(cursor_config, indent=2)) + + # Always show the deeplink + click.echo(f"\n🔗 One-Click Install Link:") + click.echo(f" {deeplink}") + click.echo(f"\n 💡 Share this link to let others install your MXCP server with one click!") + + if cursor_install_type == "manual": + show_cursor_next_steps(project, cursor_install_type) + else: + click.echo("ℹ️ Skipped Cursor IDE configuration generation") + except Exception as e: + click.echo(f"⚠️ Warning: Failed to generate Cursor config: {e}") # Show next steps - show_next_steps(target_dir, project, bootstrap, config_generated) + show_next_steps(target_dir, project, bootstrap, config_generated, cursor_configured, cursor_install_type) except Exception as e: output_error(e, json_output=False, debug=debug) \ No newline at end of file diff --git a/tests/test_cli_init.py b/tests/test_cli_init.py index 339df79e..7878cd3a 100644 --- a/tests/test_cli_init.py +++ b/tests/test_cli_init.py @@ -5,6 +5,7 @@ import tempfile import sys import os +import base64 from pathlib import Path import shutil from unittest.mock import patch, Mock @@ -28,12 +29,12 @@ def set_mxcp_config_env(): def test_init_basic(tmp_path): """Test basic init without bootstrap.""" - # Simulate 'n' response to the config generation prompt + # Simulate 'n' response to both config generation prompts result = subprocess.run( ["mxcp", "init", str(tmp_path)], capture_output=True, text=True, - input="n\n" # Say no to config generation + input="n\nn\n" # Say no to both Claude and Cursor config generation ) assert result.returncode == 0 @@ -51,12 +52,12 @@ def test_init_basic(tmp_path): def test_init_bootstrap(tmp_path): """Test init with bootstrap flag.""" - # Simulate 'y' response to the config generation prompt + # Simulate 'y' response to Claude config generation, 'n' to Cursor result = subprocess.run( ["mxcp", "init", str(tmp_path), "--bootstrap"], capture_output=True, text=True, - input="y\n" # Say yes to config generation + input="y\nn\n" # Say yes to Claude config, no to Cursor ) assert result.returncode == 0 @@ -94,7 +95,7 @@ def test_init_bootstrap_complete_directory_structure(tmp_path): ["mxcp", "init", str(project_dir), "--bootstrap", "--project", project_name], capture_output=True, text=True, - input="n\n" # Say no to config generation to focus on directory structure + input="n\nn\n" # Say no to both config generations to focus on directory structure ) assert result.returncode == 0 @@ -228,7 +229,7 @@ def test_init_bootstrap_with_duckdb_initialization(tmp_path): ["mxcp", "init", str(project_dir), "--bootstrap", "--project", project_name], capture_output=True, text=True, - input="y\n", # Say yes to config generation to trigger DuckDB initialization + input="y\nn\n", # Say yes to Claude config generation, no to Cursor to trigger DuckDB initialization env=env ) @@ -299,7 +300,7 @@ def test_init_custom_project_name(tmp_path): ["mxcp", "init", str(tmp_path), "--project", "my-custom-project"], capture_output=True, text=True, - input="n\n" # Say no to config generation + input="n\nn\n" # Say no to both config generations ) assert result.returncode == 0 @@ -316,7 +317,7 @@ def test_init_with_config_generation(tmp_path): ["mxcp", "init", str(tmp_path), "--bootstrap"], capture_output=True, text=True, - input="y\n" # Say yes to config generation + input="y\nn\n" # Say yes to Claude config, no to Cursor config ) assert result.returncode == 0 @@ -334,7 +335,7 @@ def test_init_with_config_generation(tmp_path): assert "args" in server_config # Check output shows the generated config - assert "Generated configuration:" in result.stdout + assert "Generated Claude Desktop configuration:" in result.stdout assert "mcpServers" in result.stdout @@ -344,7 +345,7 @@ def test_init_without_config_generation(tmp_path): ["mxcp", "init", str(tmp_path), "--bootstrap"], capture_output=True, text=True, - input="n\n" # Say no to config generation + input="n\nn\n" # Say no to both Claude and Cursor config generation ) assert result.returncode == 0 @@ -352,7 +353,8 @@ def test_init_without_config_generation(tmp_path): # Check output mentions skipping config assert "Skipped Claude Desktop configuration generation" in result.stdout - assert "Run 'mxcp init .' again to generate server_config.json" in result.stdout + assert "Skipped Cursor IDE configuration generation" in result.stdout + assert "Run 'mxcp init .' again to generate configurations" in result.stdout def test_init_cannot_create_inside_existing_repo(tmp_path): @@ -361,7 +363,7 @@ def test_init_cannot_create_inside_existing_repo(tmp_path): parent_dir = tmp_path / "parent" parent_dir.mkdir() - subprocess.run(["mxcp", "init", str(parent_dir)], capture_output=True, input="n\n", text=True) + subprocess.run(["mxcp", "init", str(parent_dir)], capture_output=True, input="n\nn\n", text=True) # Try to create child repo child_dir = parent_dir / "child" @@ -371,7 +373,7 @@ def test_init_cannot_create_inside_existing_repo(tmp_path): ["mxcp", "init", str(child_dir)], capture_output=True, text=True, - input="n\n" # Say no to config generation + input="n\nn\n" # Say no to both config generations ) assert result.returncode != 0 @@ -388,7 +390,7 @@ def test_init_server_config_content(tmp_path): ["mxcp", "init", str(project_dir), "--bootstrap"], capture_output=True, text=True, - input="y\n" # Say yes to config generation + input="y\nn\n" # Say yes to Claude config generation, no to Cursor ) assert result.returncode == 0 @@ -429,7 +431,7 @@ def test_init_bootstrap_complete_directory_structure(): project_dir = tmpdir_path / "test-project" # Run init with bootstrap - result = runner.invoke(init, [str(project_dir), "--bootstrap", "--project", "test-project"], input="n\n") + result = runner.invoke(init, [str(project_dir), "--bootstrap", "--project", "test-project"], input="n\nn\n") # Should succeed assert result.exit_code == 0, f"Init failed with output: {result.output}" @@ -477,7 +479,7 @@ def test_init_bootstrap_with_duckdb_initialization(): project_dir = tmpdir_path / "test-db-init" # Run init with bootstrap - result = runner.invoke(init, [str(project_dir), "--bootstrap", "--project", "test-db-init"], input="n\n") + result = runner.invoke(init, [str(project_dir), "--bootstrap", "--project", "test-db-init"], input="n\nn\n") # Should succeed without version validation errors assert result.exit_code == 0, f"Init failed with output: {result.output}" @@ -506,6 +508,236 @@ def test_user_config_generation_uses_integer_version(): assert isinstance(config["mxcp"], int), f"Expected int type, got {type(config['mxcp'])}" +def test_init_with_cursor_config_generation(tmp_path): + """Test init with Cursor IDE config generation.""" + with patch('mxcp.cli.init.detect_cursor_installation') as mock_detect: + # Mock Cursor as detected + mock_detect.return_value = { + "executable": "/usr/bin/cursor", + "config_dir": str(Path.home() / ".config" / "Cursor" / "User"), + "global_mcp_config": str(Path.home() / ".cursor" / "mcp.json") + } + + result = subprocess.run( + ["mxcp", "init", str(tmp_path), "--bootstrap"], + capture_output=True, + text=True, + input="n\ny\n1\n1\n" # No to Claude, Yes to Cursor, Auto-configure, Project-specific + ) + + assert result.returncode == 0 + assert "✓ Detected Cursor IDE installation" in result.stdout + assert "✓ Configured Cursor MCP server (project-specific)" in result.stdout + + # Should show Cursor configuration + assert "📋 Cursor IDE Configuration:" in result.stdout + assert "mcpServers" in result.stdout + + # Should show deeplink + assert "🔗 One-Click Install Link:" in result.stdout + assert "cursor://anysphere.cursor-deeplink/mcp/install?name=" in result.stdout + assert "💡 Share this link to let others install your MXCP server with one click!" in result.stdout + + # Check project-specific config file was created + cursor_config_path = tmp_path / ".cursor" / "mcp.json" + assert cursor_config_path.exists() + + with open(cursor_config_path) as f: + config = json.load(f) + + assert "mcpServers" in config + assert tmp_path.name in config["mcpServers"] + + +def test_init_with_cursor_global_config(tmp_path): + """Test init with Cursor IDE global config generation.""" + with patch('mxcp.cli.init.detect_cursor_installation') as mock_detect, \ + patch('mxcp.cli.init.install_cursor_config') as mock_install: + + # Mock Cursor as detected + mock_detect.return_value = { + "executable": "/usr/bin/cursor", + "config_dir": str(Path.home() / ".config" / "Cursor" / "User"), + "global_mcp_config": str(Path.home() / ".cursor" / "mcp.json") + } + + # Mock successful installation + mock_install.return_value = True + + result = subprocess.run( + ["mxcp", "init", str(tmp_path), "--bootstrap"], + capture_output=True, + text=True, + input="n\ny\n1\n2\n" # No to Claude, Yes to Cursor, Auto-configure, Global + ) + + assert result.returncode == 0 + assert "✓ Configured Cursor MCP server (globally)" in result.stdout + + # Should call install_cursor_config with global scope + mock_install.assert_called_once() + args, kwargs = mock_install.call_args + assert args[1] == tmp_path.name # project name + assert args[2] == "global" # install type + + +def test_init_with_cursor_manual_setup(tmp_path): + """Test init with Cursor IDE manual setup.""" + with patch('mxcp.cli.init.detect_cursor_installation') as mock_detect: + # Mock Cursor as detected + mock_detect.return_value = { + "executable": "/usr/bin/cursor", + "config_dir": str(Path.home() / ".config" / "Cursor" / "User"), + "global_mcp_config": str(Path.home() / ".cursor" / "mcp.json") + } + + result = subprocess.run( + ["mxcp", "init", str(tmp_path), "--bootstrap"], + capture_output=True, + text=True, + input="n\ny\n2\n" # No to Claude, Yes to Cursor, Manual setup + ) + + assert result.returncode == 0 + assert "📝 Cursor IDE Manual Setup:" in result.stdout + assert "📋 To install manually:" in result.stdout + assert "1. Open Cursor IDE" in result.stdout + assert "2. Go to Settings > Features > Model Context Protocol" in result.stdout + assert "4. Use the one-click install link provided above" in result.stdout + + +def test_init_cursor_not_detected(tmp_path): + """Test init when Cursor IDE is not detected.""" + with patch('mxcp.cli.init.detect_cursor_installation') as mock_detect: + # Mock Cursor as not detected + mock_detect.return_value = None + + result = subprocess.run( + ["mxcp", "init", str(tmp_path), "--bootstrap"], + capture_output=True, + text=True, + input="n\ny\n" # No to Claude, Yes to Cursor + ) + + assert result.returncode == 0 + assert "⚠️ Cursor IDE not detected in PATH" in result.stdout + assert "📝 Cursor IDE Manual Setup:" in result.stdout + + # Should still show deeplink and config + assert "🔗 One-Click Install Link:" in result.stdout + assert "📋 Cursor IDE Configuration:" in result.stdout + + +def test_init_both_claude_and_cursor_config(tmp_path): + """Test init with both Claude Desktop and Cursor IDE config generation.""" + with patch('mxcp.cli.init.detect_cursor_installation') as mock_detect: + # Mock Cursor as detected + mock_detect.return_value = { + "executable": "/usr/bin/cursor", + "config_dir": str(Path.home() / ".config" / "Cursor" / "User"), + "global_mcp_config": str(Path.home() / ".cursor" / "mcp.json") + } + + result = subprocess.run( + ["mxcp", "init", str(tmp_path), "--bootstrap"], + capture_output=True, + text=True, + input="y\ny\n1\n1\n" # Yes to Claude, Yes to Cursor, Auto-configure, Project-specific + ) + + assert result.returncode == 0 + + # Should generate Claude config + assert (tmp_path / "server_config.json").exists() + assert "✓ Generated server_config.json for Claude Desktop" in result.stdout + + # Should generate Cursor config + assert "✓ Configured Cursor MCP server (project-specific)" in result.stdout + cursor_config_path = tmp_path / ".cursor" / "mcp.json" + assert cursor_config_path.exists() + + # Should show both in next steps + assert "🔹 Claude Desktop: Add server_config.json to Claude config" in result.stdout + assert "🔹 Cursor IDE: Already configured! Open Cursor and start using." in result.stdout + + +def test_cursor_deeplink_generation(): + """Test Cursor deeplink generation function.""" + from mxcp.cli.init import generate_cursor_deeplink + import base64 + + # Test config + config = { + "mcpServers": { + "test-project": { + "command": "mxcp", + "args": ["serve", "--transport", "stdio"], + "cwd": "/path/to/project" + } + } + } + + deeplink = generate_cursor_deeplink(config, "test-project") + + # Should be a valid Cursor deeplink with correct protocol + assert deeplink.startswith("cursor://anysphere.cursor-deeplink/mcp/install?name=test-project&config=") + + # Extract and decode the config + config_param = deeplink.split("&config=")[1] + decoded_config = base64.b64decode(config_param).decode() + config_data = json.loads(decoded_config) + + # Should contain just the server config directly (no mcpServers wrapper) + assert "command" in config_data + assert "args" in config_data + assert config_data["command"] == "mxcp" + assert config_data["args"] == ["serve", "--transport", "stdio"] + + +def test_cursor_config_merging(tmp_path): + """Test that Cursor config merging works with existing configs.""" + from mxcp.cli.init import install_cursor_config + + # Create existing config + cursor_dir = tmp_path / ".cursor" + cursor_dir.mkdir() + config_path = cursor_dir / "mcp.json" + + existing_config = { + "mcpServers": { + "existing-server": { + "command": "existing-command", + "args": ["existing-args"] + } + } + } + + with open(config_path, 'w') as f: + json.dump(existing_config, f) + + # Add new config + new_config = { + "mcpServers": { + "new-server": { + "command": "mxcp", + "args": ["serve", "--transport", "stdio"] + } + } + } + + result = install_cursor_config(new_config, "new-server", "project", tmp_path) + assert result is True + + # Check merged config + with open(config_path) as f: + merged_config = json.load(f) + + assert "existing-server" in merged_config["mcpServers"] + assert "new-server" in merged_config["mcpServers"] + assert merged_config["mcpServers"]["existing-server"]["command"] == "existing-command" + assert merged_config["mcpServers"]["new-server"]["command"] == "mxcp" + + def test_migration_exception_handling(): """Test that migration exceptions are properly caught and displayed by other commands.""" from mxcp.cli.list import list_endpoints From 815c93562a1da10bb00e88da2c2ebe4571cb65a6 Mon Sep 17 00:00:00 2001 From: datYori Date: Wed, 25 Jun 2025 17:50:10 +0200 Subject: [PATCH 2/4] fixup! feat(cli): add Cursor IDE integration to init command --- README.md | 4 ++-- docs/guides/integrations.md | 18 +++++++++--------- src/mxcp/cli/init.py | 36 ++++++++++++++++++------------------ tests/test_cli_init.py | 28 ++++++++++++++-------------- 4 files changed, 43 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index 39b59d42..6dfd24f0 100644 --- a/README.md +++ b/README.md @@ -596,14 +596,14 @@ mxcp run # Run a specific endpoint MXCP implements the Model Context Protocol (MCP), making it compatible with: - **Claude Desktop** — Native MCP support -- **Cursor IDE** — Native MCP support with one-click installation +- **Cursor** — Native MCP support with one-click installation - **OpenAI-compatible tools** — Via MCP adapters - **Custom integrations** — Using the MCP specification For specific setup instructions, see: - [Earthquakes Example](https://github.com/raw-labs/mxcp/blob/main/examples/earthquakes/README.md) — Complete Claude Desktop setup - [COVID + dbt Example](https://github.com/raw-labs/mxcp/blob/main/examples/covid_owid/README.md) — Advanced dbt integration -- [Integrations Guide](https://github.com/raw-labs/mxcp/blob/main/docs/guides/integrations.md) — Claude Desktop and Cursor IDE setup +- [Integrations Guide](https://github.com/raw-labs/mxcp/blob/main/docs/guides/integrations.md) — Claude Desktop and Cursor setup ## 📚 Documentation diff --git a/docs/guides/integrations.md b/docs/guides/integrations.md index 06f139dc..80da0230 100644 --- a/docs/guides/integrations.md +++ b/docs/guides/integrations.md @@ -1,10 +1,10 @@ --- title: "Integrations" -description: "Integrate MXCP with AI platforms, dbt, and data sources. Connect with Claude Desktop, Cursor IDE, OpenAI, and other LLM providers. Access diverse data sources through DuckDB." +description: "Integrate MXCP with AI platforms, dbt, and data sources. Connect with Claude Desktop, Cursor, OpenAI, and other LLM providers. Access diverse data sources through DuckDB." keywords: - mxcp integrations - claude desktop integration - - cursor ide integration + - cursor integration - dbt integration - duckdb extensions - llm integration @@ -19,7 +19,7 @@ MXCP provides seamless integration with AI platforms and data tools to create po ## Table of Contents -- [LLM Integration](#llm-integration) — Connect with Claude Desktop, Cursor IDE, OpenAI, and other AI platforms +- [LLM Integration](#llm-integration) — Connect with Claude Desktop, Cursor, OpenAI, and other AI platforms - [dbt Integration](#dbt-integration) — Transform and prepare data for AI consumption - [DuckDB Integration](#duckdb-integration) — Access diverse data sources with powerful SQL capabilities @@ -75,19 +75,19 @@ Claude Desktop has native MCP support, making it the easiest way to get started - Test your configuration with simple queries first - Monitor Claude's developer console for connection issues -### Cursor IDE +### Cursor -Cursor IDE has native MCP support through its Model Context Protocol integration, providing another excellent way to use MXCP with AI-powered development tools. +Cursor has native MCP support through its Model Context Protocol integration, providing another excellent way to use MXCP with AI-powered development tools. #### Automatic Configuration -The easiest way to set up Cursor IDE integration is during project initialization: +The easiest way to set up Cursor integration is during project initialization: ```bash -# Initialize MXCP project with IDE configuration +# Initialize MXCP project with Cursor configuration mxcp init my-project --bootstrap -# Follow the prompts to configure Cursor IDE automatically +# Follow the prompts to configure Cursor automatically # Choose from: # 1. Project-specific (recommended) - Only available in this project # 2. Global - Available in all Cursor workspaces @@ -148,7 +148,7 @@ MXCP generates one-click installation links for easy sharing: cursor://anysphere.cursor-deeplink/mcp/install?name=my-project&config=eyJjb21tYW5kIjoi... ``` -Share this link with team members for instant Cursor IDE setup. +Share this link with team members for instant Cursor setup. #### Best Practices diff --git a/src/mxcp/cli/init.py b/src/mxcp/cli/init.py index b4cc20ea..6dc2a2e8 100644 --- a/src/mxcp/cli/init.py +++ b/src/mxcp/cli/init.py @@ -188,7 +188,7 @@ def generate_claude_config(project_dir: Path, project_name: str): return config def detect_cursor_installation() -> Optional[Dict[str, str]]: - """Detect Cursor IDE installation and return relevant paths.""" + """Detect Cursor installation and return relevant paths.""" system = platform.system().lower() cursor_info = {} @@ -288,16 +288,16 @@ def install_cursor_config(config: Dict, project_name: str, install_type: str = " def show_cursor_next_steps(project_name: str, install_type: str): """Show Cursor-specific next steps.""" - click.echo(f"\n{click.style('📝 Cursor IDE Manual Setup:', fg='cyan', bold=True)}") + click.echo(f"\n{click.style('📝 Cursor Manual Setup:', fg='cyan', bold=True)}") click.echo(f" 📋 To install manually:") - click.echo(f" 1. Open Cursor IDE") + click.echo(f" 1. Open Cursor") click.echo(f" 2. Go to Settings > Features > Model Context Protocol") click.echo(f" 3. Add the configuration shown above, or") click.echo(f" 4. Use the one-click install link provided above") click.echo(f"\n 🚀 After installation:") - click.echo(f" • Restart Cursor IDE") + click.echo(f" • Restart Cursor") click.echo(f" • Open the Agent/Chat") click.echo(f" • The '{project_name}' MCP server will be automatically available") click.echo(f" • Try asking: \"List the available tools from {project_name}\"") @@ -355,16 +355,16 @@ def show_next_steps(project_dir: Path, project_name: str, bootstrap: bool, confi if config_generated and cursor_configured: click.echo(f"\n{click.style('3. Connect to your preferred IDE:', fg='yellow')}") click.echo(f" 🔹 Claude Desktop: Add server_config.json to Claude config") - click.echo(f" 🔹 Cursor IDE: Already configured! Open Cursor and start using.") + click.echo(f" 🔹 Cursor: Already configured! Open Cursor and start using.") elif config_generated: click.echo(f"\n{click.style('3. Connect to Claude Desktop:', fg='yellow')}") click.echo(f" Add the generated server_config.json to your Claude Desktop config") elif cursor_configured: - click.echo(f"\n{click.style('3. Connect to Cursor IDE:', fg='yellow')}") + click.echo(f"\n{click.style('3. Connect to Cursor:', fg='yellow')}") click.echo(f" Already configured! Open Cursor and start using.") else: click.echo(f"\n{click.style('3. Connect to your IDE:', fg='yellow')}") - click.echo(f" Create configurations for Claude Desktop or Cursor IDE") + click.echo(f" Create configurations for Claude Desktop or Cursor") click.echo(f" Run 'mxcp init .' again to generate configurations") if config_generated or cursor_configured: @@ -380,9 +380,9 @@ def show_next_steps(project_dir: Path, project_name: str, bootstrap: bool, confi if cursor_configured: if cursor_install_type == "project": - click.echo(f" • Cursor IDE: {project_dir}/.cursor/mcp.json (project-specific)") + click.echo(f" • Cursor: {project_dir}/.cursor/mcp.json (project-specific)") else: - click.echo(f" • Cursor IDE: ~/.cursor/mcp.json (global)") + click.echo(f" • Cursor: ~/.cursor/mcp.json (global)") # Step 4: Explore more click.echo(f"\n{click.style('4. Learn more:', fg='yellow')}") @@ -395,11 +395,11 @@ def show_next_steps(project_dir: Path, project_name: str, bootstrap: bool, confi if bootstrap: click.echo(f"\n{click.style('💡 Try it now:', fg='green')}") if config_generated and cursor_configured: - click.echo(f" In Claude Desktop or Cursor IDE, ask: \"Use the hello_world tool to greet Alice\"") + click.echo(f" In Claude Desktop or Cursor, ask: \"Use the hello_world tool to greet Alice\"") elif config_generated: click.echo(f" In Claude Desktop, ask: \"Use the hello_world tool to greet Alice\"") elif cursor_configured: - click.echo(f" In Cursor IDE, ask: \"Use the hello_world tool to greet Alice\"") + click.echo(f" In Cursor, ask: \"Use the hello_world tool to greet Alice\"") click.echo(f"\n{click.style('📚 Resources:', fg='cyan', bold=True)}") click.echo(f" • Documentation: https://mxcp.dev") @@ -421,7 +421,7 @@ def init(folder: str, project: str, profile: str, bootstrap: bool, debug: bool): This command creates a new MXCP repository by: 1. Creating a mxcp-site.yml file with project and profile configuration 2. Optionally creating example endpoint files - 3. Generating configurations for Claude Desktop and/or Cursor IDE integration + 3. Generating configurations for Claude Desktop and/or Cursor integration \b Examples: @@ -513,9 +513,9 @@ def init(folder: str, project: str, profile: str, bootstrap: bool, debug: bool): except Exception as e: click.echo(f"⚠️ Warning: Failed to generate Claude config: {e}") - # Generate Cursor IDE config + # Generate Cursor config try: - if click.confirm("\nWould you like to set up Cursor IDE integration?"): + if click.confirm("\nWould you like to set up Cursor integration?"): cursor_config = generate_cursor_config(target_dir, project) # Generate deeplink by default @@ -525,7 +525,7 @@ def init(folder: str, project: str, profile: str, bootstrap: bool, debug: bool): cursor_info = detect_cursor_installation() if cursor_info: - click.echo(f"✓ Detected Cursor IDE installation") + click.echo(f"✓ Detected Cursor installation") # Offer installation options click.echo("\nChoose Cursor configuration option:") @@ -554,11 +554,11 @@ def init(folder: str, project: str, profile: str, bootstrap: bool, debug: bool): cursor_install_type = "manual" else: # Cursor not detected, provide manual setup - click.echo("⚠️ Cursor IDE not detected in PATH") + click.echo("⚠️ Cursor not detected in PATH") cursor_install_type = "manual" # Show the config content - click.echo(f"\n📋 Cursor IDE Configuration:") + click.echo(f"\n📋 Cursor Configuration:") click.echo(json.dumps(cursor_config, indent=2)) # Always show the deeplink @@ -569,7 +569,7 @@ def init(folder: str, project: str, profile: str, bootstrap: bool, debug: bool): if cursor_install_type == "manual": show_cursor_next_steps(project, cursor_install_type) else: - click.echo("ℹ️ Skipped Cursor IDE configuration generation") + click.echo("ℹ️ Skipped Cursor configuration generation") except Exception as e: click.echo(f"⚠️ Warning: Failed to generate Cursor config: {e}") diff --git a/tests/test_cli_init.py b/tests/test_cli_init.py index 7878cd3a..1b9f3215 100644 --- a/tests/test_cli_init.py +++ b/tests/test_cli_init.py @@ -353,7 +353,7 @@ def test_init_without_config_generation(tmp_path): # Check output mentions skipping config assert "Skipped Claude Desktop configuration generation" in result.stdout - assert "Skipped Cursor IDE configuration generation" in result.stdout + assert "Skipped Cursor configuration generation" in result.stdout assert "Run 'mxcp init .' again to generate configurations" in result.stdout @@ -509,7 +509,7 @@ def test_user_config_generation_uses_integer_version(): def test_init_with_cursor_config_generation(tmp_path): - """Test init with Cursor IDE config generation.""" + """Test init with Cursor config generation.""" with patch('mxcp.cli.init.detect_cursor_installation') as mock_detect: # Mock Cursor as detected mock_detect.return_value = { @@ -526,11 +526,11 @@ def test_init_with_cursor_config_generation(tmp_path): ) assert result.returncode == 0 - assert "✓ Detected Cursor IDE installation" in result.stdout + assert "✓ Detected Cursor installation" in result.stdout assert "✓ Configured Cursor MCP server (project-specific)" in result.stdout # Should show Cursor configuration - assert "📋 Cursor IDE Configuration:" in result.stdout + assert "📋 Cursor Configuration:" in result.stdout assert "mcpServers" in result.stdout # Should show deeplink @@ -550,7 +550,7 @@ def test_init_with_cursor_config_generation(tmp_path): def test_init_with_cursor_global_config(tmp_path): - """Test init with Cursor IDE global config generation.""" + """Test init with Cursor global config generation.""" with patch('mxcp.cli.init.detect_cursor_installation') as mock_detect, \ patch('mxcp.cli.init.install_cursor_config') as mock_install: @@ -582,7 +582,7 @@ def test_init_with_cursor_global_config(tmp_path): def test_init_with_cursor_manual_setup(tmp_path): - """Test init with Cursor IDE manual setup.""" + """Test init with Cursor manual setup.""" with patch('mxcp.cli.init.detect_cursor_installation') as mock_detect: # Mock Cursor as detected mock_detect.return_value = { @@ -599,15 +599,15 @@ def test_init_with_cursor_manual_setup(tmp_path): ) assert result.returncode == 0 - assert "📝 Cursor IDE Manual Setup:" in result.stdout + assert "📝 Cursor Manual Setup:" in result.stdout assert "📋 To install manually:" in result.stdout - assert "1. Open Cursor IDE" in result.stdout + assert "1. Open Cursor" in result.stdout assert "2. Go to Settings > Features > Model Context Protocol" in result.stdout assert "4. Use the one-click install link provided above" in result.stdout def test_init_cursor_not_detected(tmp_path): - """Test init when Cursor IDE is not detected.""" + """Test init when Cursor is not detected.""" with patch('mxcp.cli.init.detect_cursor_installation') as mock_detect: # Mock Cursor as not detected mock_detect.return_value = None @@ -620,16 +620,16 @@ def test_init_cursor_not_detected(tmp_path): ) assert result.returncode == 0 - assert "⚠️ Cursor IDE not detected in PATH" in result.stdout - assert "📝 Cursor IDE Manual Setup:" in result.stdout + assert "⚠️ Cursor not detected in PATH" in result.stdout + assert "📝 Cursor Manual Setup:" in result.stdout # Should still show deeplink and config assert "🔗 One-Click Install Link:" in result.stdout - assert "📋 Cursor IDE Configuration:" in result.stdout + assert "📋 Cursor Configuration:" in result.stdout def test_init_both_claude_and_cursor_config(tmp_path): - """Test init with both Claude Desktop and Cursor IDE config generation.""" + """Test init with both Claude Desktop and Cursor config generation.""" with patch('mxcp.cli.init.detect_cursor_installation') as mock_detect: # Mock Cursor as detected mock_detect.return_value = { @@ -658,7 +658,7 @@ def test_init_both_claude_and_cursor_config(tmp_path): # Should show both in next steps assert "🔹 Claude Desktop: Add server_config.json to Claude config" in result.stdout - assert "🔹 Cursor IDE: Already configured! Open Cursor and start using." in result.stdout + assert "🔹 Cursor: Already configured! Open Cursor and start using." in result.stdout def test_cursor_deeplink_generation(): From 417f29a54b1f8e84d41d0be6a7738a0b68cae429 Mon Sep 17 00:00:00 2001 From: datYori Date: Wed, 25 Jun 2025 17:56:19 +0200 Subject: [PATCH 3/4] follow bugbot comment https://github.com/raw-labs/mxcp/pull/42#pullrequestreview-2958697238 --- tests/test_cli_init.py | 99 ++++++++++++++++++++++-------------------- 1 file changed, 52 insertions(+), 47 deletions(-) diff --git a/tests/test_cli_init.py b/tests/test_cli_init.py index 1b9f3215..48f07d9d 100644 --- a/tests/test_cli_init.py +++ b/tests/test_cli_init.py @@ -353,7 +353,7 @@ def test_init_without_config_generation(tmp_path): # Check output mentions skipping config assert "Skipped Claude Desktop configuration generation" in result.stdout - assert "Skipped Cursor configuration generation" in result.stdout + assert "Skipped Cursor configuration generation" in result.stdout assert "Run 'mxcp init .' again to generate configurations" in result.stdout @@ -510,6 +510,8 @@ def test_user_config_generation_uses_integer_version(): def test_init_with_cursor_config_generation(tmp_path): """Test init with Cursor config generation.""" + runner = CliRunner() + with patch('mxcp.cli.init.detect_cursor_installation') as mock_detect: # Mock Cursor as detected mock_detect.return_value = { @@ -518,25 +520,24 @@ def test_init_with_cursor_config_generation(tmp_path): "global_mcp_config": str(Path.home() / ".cursor" / "mcp.json") } - result = subprocess.run( - ["mxcp", "init", str(tmp_path), "--bootstrap"], - capture_output=True, - text=True, + result = runner.invoke( + init, + [str(tmp_path), "--bootstrap"], input="n\ny\n1\n1\n" # No to Claude, Yes to Cursor, Auto-configure, Project-specific ) - assert result.returncode == 0 - assert "✓ Detected Cursor installation" in result.stdout - assert "✓ Configured Cursor MCP server (project-specific)" in result.stdout + assert result.exit_code == 0 + assert "✓ Detected Cursor installation" in result.output + assert "✓ Configured Cursor MCP server (project-specific)" in result.output # Should show Cursor configuration - assert "📋 Cursor Configuration:" in result.stdout - assert "mcpServers" in result.stdout + assert "📋 Cursor Configuration:" in result.output + assert "mcpServers" in result.output # Should show deeplink - assert "🔗 One-Click Install Link:" in result.stdout - assert "cursor://anysphere.cursor-deeplink/mcp/install?name=" in result.stdout - assert "💡 Share this link to let others install your MXCP server with one click!" in result.stdout + assert "🔗 One-Click Install Link:" in result.output + assert "cursor://anysphere.cursor-deeplink/mcp/install?name=" in result.output + assert "💡 Share this link to let others install your MXCP server with one click!" in result.output # Check project-specific config file was created cursor_config_path = tmp_path / ".cursor" / "mcp.json" @@ -551,6 +552,8 @@ def test_init_with_cursor_config_generation(tmp_path): def test_init_with_cursor_global_config(tmp_path): """Test init with Cursor global config generation.""" + runner = CliRunner() + with patch('mxcp.cli.init.detect_cursor_installation') as mock_detect, \ patch('mxcp.cli.init.install_cursor_config') as mock_install: @@ -564,15 +567,14 @@ def test_init_with_cursor_global_config(tmp_path): # Mock successful installation mock_install.return_value = True - result = subprocess.run( - ["mxcp", "init", str(tmp_path), "--bootstrap"], - capture_output=True, - text=True, + result = runner.invoke( + init, + [str(tmp_path), "--bootstrap"], input="n\ny\n1\n2\n" # No to Claude, Yes to Cursor, Auto-configure, Global ) - assert result.returncode == 0 - assert "✓ Configured Cursor MCP server (globally)" in result.stdout + assert result.exit_code == 0 + assert "✓ Configured Cursor MCP server (globally)" in result.output # Should call install_cursor_config with global scope mock_install.assert_called_once() @@ -583,6 +585,8 @@ def test_init_with_cursor_global_config(tmp_path): def test_init_with_cursor_manual_setup(tmp_path): """Test init with Cursor manual setup.""" + runner = CliRunner() + with patch('mxcp.cli.init.detect_cursor_installation') as mock_detect: # Mock Cursor as detected mock_detect.return_value = { @@ -591,45 +595,47 @@ def test_init_with_cursor_manual_setup(tmp_path): "global_mcp_config": str(Path.home() / ".cursor" / "mcp.json") } - result = subprocess.run( - ["mxcp", "init", str(tmp_path), "--bootstrap"], - capture_output=True, - text=True, + result = runner.invoke( + init, + [str(tmp_path), "--bootstrap"], input="n\ny\n2\n" # No to Claude, Yes to Cursor, Manual setup ) - assert result.returncode == 0 - assert "📝 Cursor Manual Setup:" in result.stdout - assert "📋 To install manually:" in result.stdout - assert "1. Open Cursor" in result.stdout - assert "2. Go to Settings > Features > Model Context Protocol" in result.stdout - assert "4. Use the one-click install link provided above" in result.stdout + assert result.exit_code == 0 + assert "📝 Cursor Manual Setup:" in result.output + assert "📋 To install manually:" in result.output + assert "1. Open Cursor" in result.output + assert "2. Go to Settings > Features > Model Context Protocol" in result.output + assert "4. Use the one-click install link provided above" in result.output def test_init_cursor_not_detected(tmp_path): """Test init when Cursor is not detected.""" + runner = CliRunner() + with patch('mxcp.cli.init.detect_cursor_installation') as mock_detect: # Mock Cursor as not detected mock_detect.return_value = None - result = subprocess.run( - ["mxcp", "init", str(tmp_path), "--bootstrap"], - capture_output=True, - text=True, + result = runner.invoke( + init, + [str(tmp_path), "--bootstrap"], input="n\ny\n" # No to Claude, Yes to Cursor ) - assert result.returncode == 0 - assert "⚠️ Cursor not detected in PATH" in result.stdout - assert "📝 Cursor Manual Setup:" in result.stdout + assert result.exit_code == 0 + assert "⚠️ Cursor not detected in PATH" in result.output + assert "📝 Cursor Manual Setup:" in result.output # Should still show deeplink and config - assert "🔗 One-Click Install Link:" in result.stdout - assert "📋 Cursor Configuration:" in result.stdout + assert "🔗 One-Click Install Link:" in result.output + assert "📋 Cursor Configuration:" in result.output def test_init_both_claude_and_cursor_config(tmp_path): """Test init with both Claude Desktop and Cursor config generation.""" + runner = CliRunner() + with patch('mxcp.cli.init.detect_cursor_installation') as mock_detect: # Mock Cursor as detected mock_detect.return_value = { @@ -638,27 +644,26 @@ def test_init_both_claude_and_cursor_config(tmp_path): "global_mcp_config": str(Path.home() / ".cursor" / "mcp.json") } - result = subprocess.run( - ["mxcp", "init", str(tmp_path), "--bootstrap"], - capture_output=True, - text=True, + result = runner.invoke( + init, + [str(tmp_path), "--bootstrap"], input="y\ny\n1\n1\n" # Yes to Claude, Yes to Cursor, Auto-configure, Project-specific ) - assert result.returncode == 0 + assert result.exit_code == 0 # Should generate Claude config assert (tmp_path / "server_config.json").exists() - assert "✓ Generated server_config.json for Claude Desktop" in result.stdout + assert "✓ Generated server_config.json for Claude Desktop" in result.output # Should generate Cursor config - assert "✓ Configured Cursor MCP server (project-specific)" in result.stdout + assert "✓ Configured Cursor MCP server (project-specific)" in result.output cursor_config_path = tmp_path / ".cursor" / "mcp.json" assert cursor_config_path.exists() # Should show both in next steps - assert "🔹 Claude Desktop: Add server_config.json to Claude config" in result.stdout - assert "🔹 Cursor: Already configured! Open Cursor and start using." in result.stdout + assert "🔹 Claude Desktop: Add server_config.json to Claude config" in result.output + assert "🔹 Cursor: Already configured! Open Cursor and start using." in result.output def test_cursor_deeplink_generation(): From 01a9e1199e562fbba3bb91a382a00a54c3ac6d08 Mon Sep 17 00:00:00 2001 From: datYori Date: Thu, 26 Jun 2025 09:48:47 +0200 Subject: [PATCH 4/4] fix clickable link --- src/mxcp/cli/init.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/mxcp/cli/init.py b/src/mxcp/cli/init.py index 6dc2a2e8..3a0bef8d 100644 --- a/src/mxcp/cli/init.py +++ b/src/mxcp/cli/init.py @@ -563,8 +563,9 @@ def init(folder: str, project: str, profile: str, bootstrap: bool, debug: bool): # Always show the deeplink click.echo(f"\n🔗 One-Click Install Link:") - click.echo(f" {deeplink}") - click.echo(f"\n 💡 Share this link to let others install your MXCP server with one click!") + clickable_link = f"\033]8;;{deeplink}\033\\{deeplink}\033]8;;\033\\" + click.echo(clickable_link) + click.echo(f"\n💡 Share this link to let others install your MXCP server with one click!") if cursor_install_type == "manual": show_cursor_next_steps(project, cursor_install_type)