#!/usr/bin/env python3
"""
Setup script for Kotlin Android MCP Server
Handles installation and configuration for different deployment scenarios
This script provides an automated installation and configuration system for the
Kotlin Android MCP Server, supporting multiple deployment modes:
- Portable: Run directly from project directory
- System: Install command to system PATH
- Module: Enable Python module execution
Key Features:
- Interacti # Step 7: Display results and configuration summary
print("\n๐ Configuration files created:")
for config_file in config_files:
print(f" ๐ {config_file.name}")
print("\n๐ Setup complete!")
# Display configuration summary based on user choices
print(f"\n๐ Configuration Summary:")
print(f" ๐ท๏ธ Server name: {user_config.get('server_name', 'kotlin-android')}")
if user_config.get('default_project_path'):
print(f" ๐ Project path: {user_config['default_project_path']}")
else:
print(f" ๐ Project path: {'Dynamic (environment variables)' if user_config.get('use_env_vars') else 'Manual configuration required'}")teractive installation modes
- Automatic configuration file generation for different MCP clients
- Smart environment variable handling
- Cross-platform compatibility
- Zero-manual-configuration setup
Author: MCP Development Team
Version: 2.0.0
License: MIT
"""
import json
import sys
from pathlib import Path
def create_symlink_installation():
"""
Create a symlink-based installation in user's local bin directory
This function creates a system-wide installation by:
1. Creating ~/.local/bin directory if it doesn't exist
2. Creating a wrapper script that calls the MCP server
3. Making the wrapper executable and accessible via PATH
Returns:
Path: The path to the created symlink/wrapper script
Raises:
Exception: If the symlink creation fails due to permissions or other issues
"""
# Get user's home directory and create local bin path
home = Path.home()
local_bin = home / ".local" / "bin"
# Ensure the local bin directory exists (equivalent to mkdir -p)
local_bin.mkdir(parents=True, exist_ok=True)
# Get the absolute path to this script's directory
script_dir = Path(__file__).parent.absolute()
# Define the path for our command wrapper
symlink_path = local_bin / "kotlin-android-mcp"
# Remove existing symlink if it exists to avoid conflicts
if symlink_path.exists():
symlink_path.unlink()
# Create a bash wrapper script that:
# - Changes to the MCP server directory
# - Executes the Python server with all passed arguments
wrapper_content = f"""#!/bin/bash
cd "{script_dir}"
python3 kotlin_mcp_server.py "$@"
"""
# Write the wrapper script and make it executable
symlink_path.write_text(wrapper_content)
symlink_path.chmod(0o755) # rwxr-xr-x permissions
return symlink_path
def get_user_configuration():
"""
Collect user configuration either from command line or interactive prompts
This function handles:
1. Command line argument parsing for non-interactive usage
2. Interactive prompts with input validation
3. Default value handling for missing parameters
4. Input sanitization for security
Returns:
dict: User configuration containing project settings and preferences
"""
# Check if running in non-interactive mode (command line arguments provided)
if len(sys.argv) > 2:
# Non-interactive mode - extract configuration from command line
return parse_command_line_config()
# Interactive mode - prompt user for configuration with validation
print("\n๐ Configuration Setup:")
print("You can press Enter to use default values or provide custom settings.")
# Get project path with validation
while True:
try:
default_project_path = input(
"๐ Default Android project path (or leave empty for dynamic): "
).strip()
# Basic path validation and sanitization
if default_project_path:
# Remove dangerous characters and validate path format
if any(char in default_project_path for char in ["|", "&", ";", "$", "`"]):
print("โ Invalid characters in path. Please use a standard file path.")
continue
# Convert to absolute path and validate
validated_path = str(Path(default_project_path).expanduser().resolve())
default_project_path = validated_path
break
except (OSError, ValueError) as e:
print(f"โ Invalid path: {e}. Please try again.")
continue
# Get server name with validation
while True:
server_name = input("๐ท๏ธ MCP Server name [kotlin-android]: ").strip()
if not server_name:
server_name = "kotlin-android" # Default value
# Validate server name (alphanumeric, hyphens, underscores only)
if not server_name.replace("-", "").replace("_", "").isalnum():
print("โ Server name must contain only letters, numbers, hyphens, and underscores.")
continue
break
# Get environment variable preference with validation
while True:
env_input = (
input("๐ Use environment variables for dynamic project paths? [y/N]: ").strip().lower()
)
if env_input in ["", "n", "no"]:
use_env_vars = False
break
elif env_input in ["y", "yes"]:
use_env_vars = True
break
else:
print("โ Please enter 'y' for yes or 'n' for no (or press Enter for default).")
continue
return {
"default_project_path": default_project_path or None,
"server_name": server_name,
"use_env_vars": use_env_vars,
}
def parse_command_line_config():
"""
Parse configuration from command line arguments for non-interactive setup
Command line argument format:
python install.py [install_type] [project_path] [server_name] [use_env_vars]
Arguments:
- install_type: 1=Portable, 2=System, 3=Module (handled by main())
- project_path: Path to Android project, or 'none' for dynamic configuration
- server_name: MCP server identifier (default: kotlin-android)
- use_env_vars: 'true'/'false' for environment variable usage
Returns:
dict: Configuration dictionary with parsed values and defaults
Example:
python install.py 1 /path/to/project my-server true
-> Portable installation with fixed path, custom name, env vars enabled
"""
# Initialize configuration with sensible defaults
config = {
"default_project_path": None, # No fixed path by default
"server_name": "kotlin-android", # Standard server name
"use_env_vars": True, # Enable environment variables by default
}
# Parse arguments: python install.py [install_type] [project_path] [server_name] [use_env_vars]
# sys.argv[0] = script name, sys.argv[1] = install_type, sys.argv[2] onwards = our config
if len(sys.argv) > 2:
# Parse project path - 'none' means dynamic configuration
config["default_project_path"] = sys.argv[2] if sys.argv[2] != "none" else None
if len(sys.argv) > 3:
# Parse custom server name
config["server_name"] = sys.argv[3]
if len(sys.argv) > 4:
# Parse environment variable preference - accept various true/false formats
config["use_env_vars"] = sys.argv[4].lower() in ["true", "yes", "1", "y"]
# Display parsed configuration for user confirmation
print("๐ Using command-line configuration:")
print(f" ๐ท๏ธ Server name: {config['server_name']}")
print(f" ๐ Project path: {config['default_project_path'] or 'Dynamic'}")
print(f" ๐ Environment variables: {'Yes' if config['use_env_vars'] else 'No'}")
return config
def update_config_file(config_file, installation_type, script_dir=None, user_config=None):
"""
Update configuration file based on installation type and user preferences
This function generates platform-specific MCP configuration files:
- mcp_config.json: Generic configuration for any MCP client
- mcp_config_claude.json: Optimized for Claude Desktop with ${workspaceRoot}
- mcp_config_vscode.json: Optimized for VS Code with ${workspaceFolder}
Args:
config_file (Path): Base path for configuration files
installation_type (str): 'portable', 'installable', or 'module'
script_dir (Path, optional): Directory containing the MCP server script
user_config (dict, optional): User preferences from get_user_configuration()
Returns:
tuple: (list of created config files, user configuration dict)
Installation Types:
- portable: Run directly from project directory with absolute paths
- installable: Use system command (kotlin-android-mcp)
- module: Use Python module execution (python -m kotlin_mcp_server)
"""
# Use provided user configuration or empty dict as fallback
if user_config is None:
user_config = {}
# Determine working directory for the MCP server
# For portable/module: use absolute script directory
# For installable: use environment variable placeholder
if script_dir is None:
cwd_path = "${MCP_SERVER_DIR}"
server_script_path = "kotlin_mcp_server.py"
else:
cwd_path = str(script_dir)
server_script_path = str(script_dir / "kotlin_mcp_server.py")
# Define command configurations for different installation types
configs = {
"portable": {
"command": "python3", # Use system Python
"args": [server_script_path], # Execute server script with absolute path
"cwd": cwd_path, # Set working directory
},
"installable": {
"command": "kotlin-android-mcp", # Use installed system command
"args": [], # No additional arguments needed
},
"module": {
"command": "python3", # Use system Python
"args": ["-m", "kotlin_mcp_server"], # Execute as Python module
"cwd": cwd_path, # Set working directory
},
}
# Get the appropriate configuration for the installation type
config = configs.get(installation_type, configs["portable"])
# Extract user preferences with defaults
server_name = user_config.get("server_name", "kotlin-android")
default_project_path = user_config.get("default_project_path")
use_env_vars = user_config.get("use_env_vars", True)
# Configure environment variables based on user preferences
env_config = {}
if default_project_path:
# User provided a specific project path - use it directly
# This creates a fixed configuration for a single project
env_config["PROJECT_PATH"] = default_project_path
elif use_env_vars:
# Use environment variables for dynamic project paths
# This allows the server to work with different projects
env_config["PROJECT_PATH"] = "${WORKSPACE_ROOT}"
else:
# No project path specified and env vars disabled
# User will need to manually configure this later
env_config["PROJECT_PATH"] = "/path/to/android/project"
# Create base MCP configuration with user's server name
mcp_config = {"mcpServers": {server_name: {**config, "env": env_config}}}
# Create platform-specific environment configurations
# Start with copies of the base environment config
claude_env = env_config.copy()
vscode_env = env_config.copy()
# Override environment variables for platform-specific behavior
# Only when using dynamic configuration (env vars enabled, no fixed path)
if use_env_vars and not default_project_path:
# Claude Desktop uses ${workspaceRoot} variable
claude_env["PROJECT_PATH"] = "${workspaceRoot}"
# VS Code uses ${workspaceFolder} variable (standard VS Code variable)
vscode_env["PROJECT_PATH"] = "${workspaceFolder}"
# Create multiple config files optimized for different MCP clients
configs_to_create = {
# Generic configuration - works with any MCP client
"mcp_config.json": mcp_config,
# Claude Desktop optimized configuration
"mcp_config_claude.json": {"mcpServers": {server_name: {**config, "env": claude_env}}},
# VS Code optimized configuration
"mcp_config_vscode.json": {"mcpServers": {server_name: {**config, "env": vscode_env}}},
}
# Write all configuration files to disk
created_files = []
for filename, config_content in configs_to_create.items():
config_path = Path(config_file).parent / filename
# Write JSON with proper formatting (2-space indentation)
with open(config_path, "w") as f:
json.dump(config_content, f, indent=2)
created_files.append(config_path)
return created_files, user_config
def main():
"""
Main installation function - orchestrates the entire setup process
This function handles:
1. Command line argument parsing and help display
2. User configuration collection (interactive or non-interactive)
3. Python environment validation and dependency installation
4. File permission setup for executable scripts
5. Installation type selection and execution
6. Configuration file generation
7. User guidance and testing instructions
Installation Flow:
1. Parse command line arguments or show help
2. Get user configuration (project path, server name, env vars)
3. Validate Python installation and install dependencies
4. Set up file permissions for scripts
5. Execute chosen installation type (portable/system/module)
6. Generate platform-specific configuration files
7. Display integration instructions and testing commands
Returns:
int: Exit code (0 for success, 1 for failure)
"""
# Get absolute path to the script directory for all operations
script_dir = Path(__file__).parent.absolute()
# Display main header
print("๐ง Kotlin Android MCP Server Setup")
print("=" * 40)
# Handle help requests before any other processing
if len(sys.argv) > 1 and sys.argv[1] in ["-h", "--help", "help"]:
print("\n๐ Usage:")
print(" python install.py [install_type] [project_path] [server_name] [use_env_vars]")
print("\n๐ Arguments:")
print(" install_type : 1=Portable, 2=System, 3=Module")
print(" project_path : Path to Android project (or 'none' for dynamic)")
print(" server_name : MCP server identifier (default: kotlin-android)")
print(" use_env_vars : true/false for environment variable usage")
print("\n๐ Examples:")
print(" python install.py 1 # Interactive portable")
print(" python install.py 1 /path/to/project # Portable with fixed path")
print(" python install.py 2 none my-server true # System with env vars")
print(" python install.py 3 /home/user/app kotlin-dev false # Module with fixed path")
return 0
# Step 1: Get user configuration (interactive or from command line)
user_config = get_user_configuration()
# Step 2: Validate Python installation
try:
import subprocess
# Check Python version and display it to user
# Use list format to prevent command injection
result = subprocess.run(
[sys.executable, "--version"],
capture_output=True,
text=True,
shell=False, # Explicitly disable shell execution
timeout=10, # Add timeout for safety
)
print(f"โ
Python: {result.stdout.strip()}")
except subprocess.TimeoutExpired:
print("โ Python version check timed out")
return 1
except Exception as e:
print(f"โ Python check failed: {e}")
return 1
# Step 3: Install Python dependencies from requirements.txt
print("\n๐ฆ Installing dependencies...")
try:
# Use the same Python executable that's running this script
# Install dependencies in the script's directory context
# Use list format to prevent command injection
subprocess.run(
[sys.executable, "-m", "pip", "install", "-r", "requirements.txt"],
check=True, # Raise exception on non-zero exit code
cwd=script_dir, # Run from script directory
shell=False, # Explicitly disable shell execution
timeout=300, # 5-minute timeout for package installation
)
print("โ
Dependencies installed")
except subprocess.TimeoutExpired:
print("โ Dependency installation timed out")
return 1
except subprocess.CalledProcessError as e:
print(f"โ Failed to install dependencies: {e}")
return 1
# Step 4: Make scripts executable (Unix-like systems) with secure permissions
# Set executable permissions for main server script and helper scripts
scripts = ["kotlin_mcp_server.py", "servers/mcp-process/mcp-gradle-wrapper.sh"]
for script in scripts:
script_path = script_dir / script
if script_path.exists():
try:
# Set secure permissions: rwxr-xr-x (755)
# Owner: read, write, execute
# Group and others: read, execute only
script_path.chmod(0o755)
print(f"โ
Set executable permissions for {script}")
except OSError as e:
print(f"โ ๏ธ Warning: Could not set permissions for {script}: {e}")
else:
print(f"โ ๏ธ Warning: Script not found: {script}")
# Step 5: Installation type selection
print("\n๐ง Choose installation type:")
print("1. Portable (run from this directory)")
print("2. System installation (add to PATH)")
print("3. Python module (importable)")
# Get installation choice from command line or default to portable
if len(sys.argv) > 1 and sys.argv[1] in ["1", "2", "3"]:
choice = sys.argv[1]
else:
choice = "1" # Default to portable installation
# Step 6: Execute the chosen installation type
if choice == "1":
# Portable installation - run directly from project directory
config_files, user_config = update_config_file(
script_dir / "mcp_config.json", "portable", script_dir, user_config
)
print("โ
Portable configuration created")
print(f"๐ Server directory: {script_dir}")
elif choice == "2":
# System installation - add command to PATH
try:
# Create wrapper script in ~/.local/bin
symlink_path = create_symlink_installation()
# Generate configuration for system command
config_files, user_config = update_config_file(
script_dir / "mcp_config.json",
"installable",
None, # No cwd needed for system command
user_config,
)
print(f"โ
System installation created: {symlink_path}")
print("๐ง Command 'kotlin-android-mcp' is now available")
except Exception as e:
print(f"โ System installation failed: {e}")
return 1
elif choice == "3":
# Python module installation - enable module execution
config_files, user_config = update_config_file(
script_dir / "mcp_config.json", "module", script_dir, user_config
)
print("โ
Module configuration created")
print("๐ง Can be run with: python -m kotlin_mcp_server")
else:
print("โ Invalid choice")
return 1
print("\n๐ Configuration files created:")
for config_file in config_files:
print(f" ๐ {config_file.name}")
print("\n๐ Setup complete!")
# Display configuration summary
print("\n๐ Configuration Summary:")
print(f" ๐ท๏ธ Server name: {user_config.get('server_name', 'kotlin-android')}")
if user_config.get("default_project_path"):
print(f" ๏ฟฝ Project path: {user_config['default_project_path']}")
else:
print(
f" ๐ Project path: {'Dynamic (environment variables)' if user_config.get('use_env_vars') else 'Manual configuration required'}"
)
# Step 8: Provide integration instructions based on configuration
print("\n๐ Integration Instructions:")
if user_config.get("default_project_path"):
print("\n๐น Fixed Project Path Configuration:")
print(f" Your server is configured to work with: {user_config['default_project_path']}")
print(" All configuration files use this path directly.")
elif user_config.get("use_env_vars"):
print("\n๐น Dynamic Project Path Configuration:")
print(" Your server uses environment variables for project paths.")
else:
print("\n๐น Manual Configuration Required:")
print(" Update PROJECT_PATH in configuration files to your project path.")
# Platform-specific integration instructions
print("\n๐น Claude Desktop:")
print(" Copy content from 'mcp_config_claude.json' to:")
print(" ~/Library/Application Support/Claude/claude_desktop_config.json")
print("\n๐น VS Code:")
print(" Use 'mcp_config_vscode.json' for VS Code extensions")
print("\n๐น Other MCP Clients:")
print(" Use 'mcp_config.json' for generic MCP client integration")
# Step 9: Provide testing instructions based on installation type
print("\n๐งช Test the server:")
if choice == "2":
# System installation testing
if user_config.get("default_project_path"):
print(" kotlin-android-mcp")
else:
print(" kotlin-android-mcp /path/to/android/project")
else:
# Portable or module installation testing
print(f" cd {script_dir}")
if user_config.get("default_project_path"):
print(" python3 kotlin_mcp_server.py")
else:
print(" python3 kotlin_mcp_server.py /path/to/android/project")
# Step 10: Display additional tips based on configuration choices
if user_config.get("use_env_vars") and not user_config.get("default_project_path"):
print("\nโ ๏ธ Environment Variables:")
print(" - ${workspaceFolder}: Standard VS Code variable (automatically resolved)")
print(" - ${workspaceRoot}: Claude Desktop variable (may need manual setup)")
print(" - ${WORKSPACE_ROOT}: Generic variable (set manually or via shell)")
print("\n๐ก Pro Tip:")
print(" The server uses workspace/project context automatically!")
print(" Environment variables will be resolved by your MCP client.")
elif user_config.get("default_project_path"):
print("\n๐ก Pro Tip:")
print(f" Your server is pre-configured for: {user_config['default_project_path']}")
print(" No additional configuration needed!")
else:
print("\nโ ๏ธ Manual Configuration:")
print(" Remember to update PROJECT_PATH in your configuration files")
print(" to point to your actual Android project directory.")
return 0
if __name__ == "__main__":
sys.exit(main())