From fe9968b633d0312b82426e9ebddfe1d6515be3c5 Mon Sep 17 00:00:00 2001 From: Fahad Date: Sat, 4 Oct 2025 23:07:50 +0400 Subject: [PATCH] feat: support for Qwen Code --- README.md | 30 +++- docs/getting-started.md | 30 ++++ run-server.ps1 | 204 +++++++++++++++++++++++++++ run-server.sh | 306 +++++++++++++++++++++++++++++++++++++++- 4 files changed, 560 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 559dd20..6156c69 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [zen_web.webm](https://github.com/user-attachments/assets/851e3911-7f06-47c0-a4ab-a2601236697c)
- πŸ€– Claude Code OR Gemini CLI OR Codex CLI + [Gemini / OpenAI / Azure / Grok / OpenRouter / DIAL / Ollama / Anthropic / Any Model] = Your Ultimate AI Development Team + πŸ€– Claude Code OR Gemini CLI OR Codex CLI OR Qwen Code CLI + [Gemini / OpenAI / Azure / Grok / OpenRouter / DIAL / Ollama / Anthropic / Any Model] = Your Ultimate AI Development Team

@@ -119,7 +119,7 @@ git clone https://github.com/BeehiveInnovations/zen-mcp-server.git cd zen-mcp-server # Handles everything: setup, config, API keys from system environment. -# Auto-configures Claude Desktop, Claude Code, Gemini CLI, Codex CLI +# Auto-configures Claude Desktop, Claude Code, Gemini CLI, Codex CLI, Qwen CLI # Enable / disable additional settings in .env ./run-server.sh ``` @@ -144,6 +144,30 @@ cd zen-mcp-server } ``` +**For Qwen Code CLI:** +Edit `~/.qwen/settings.json` and register Zen as an MCP server: + +```json +{ + "mcpServers": { + "zen": { + "command": "bash", + "args": [ + "-c", + "for p in $(which uvx 2>/dev/null) $HOME/.local/bin/uvx /opt/homebrew/bin/uvx /usr/local/bin/uvx uvx; do [ -x \"$p\" ] && exec \"$p\" --from git+https://github.com/BeehiveInnovations/zen-mcp-server.git zen-mcp-server; done; echo 'uvx not found' >&2; exit 1" + ], + "cwd": "/path/to/zen-mcp-server", + "env": { + "PATH": "/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin:~/.local/bin", + "GEMINI_API_KEY": "your_api_key_here" + } + } + } +} +``` + +Update the `env` block with the API keys you use (Gemini, OpenAI, OpenRouter, etc.). + **3. Start Using!** ``` "Use zen to analyze this code for security issues with gemini pro" @@ -151,7 +175,7 @@ cd zen-mcp-server "Plan the migration strategy with zen, get consensus from multiple models" ``` -πŸ‘‰ **[Complete Setup Guide](docs/getting-started.md)** with detailed installation, configuration for Gemini / Codex, and troubleshooting +πŸ‘‰ **[Complete Setup Guide](docs/getting-started.md)** with detailed installation, configuration for Gemini / Codex / Qwen, and troubleshooting πŸ‘‰ **[Cursor & VS Code Setup](docs/getting-started.md#ide-clients)** for IDE integration instructions ## Provider Configuration diff --git a/docs/getting-started.md b/docs/getting-started.md index fc3ac59..5e48fdf 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -147,6 +147,31 @@ PATH = "/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin:$HOME/.local/bin:$HOME/.c GEMINI_API_KEY = "your_api_key_here" ``` + +**For Qwen Code CLI:** +Create or edit `~/.qwen/settings.json`: + +```json +{ + "mcpServers": { + "zen": { + "command": "bash", + "args": [ + "-c", + "for p in $(which uvx 2>/dev/null) $HOME/.local/bin/uvx /opt/homebrew/bin/uvx /usr/local/bin/uvx uvx; do [ -x \"$p\" ] && exec \"$p\" --from git+https://github.com/BeehiveInnovations/zen-mcp-server.git zen-mcp-server; done; echo 'uvx not found' >&2; exit 1" + ], + "cwd": "/path/to/zen-mcp-server", + "env": { + "PATH": "/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin:~/.local/bin", + "GEMINI_API_KEY": "your_api_key_here" + } + } + } +} +``` + +Replace the placeholder API key with the providers you use (Gemini, OpenAI, OpenRouter, etc.). + #### IDE Clients (Cursor & VS Code) Zen works in GUI IDEs that speak MCP. The configuration mirrors the CLI examples aboveβ€”point the client at the `uvx` launcher and set any required environment variables. @@ -263,6 +288,11 @@ CUSTOM_MODEL_NAME=llama3.2 # Default model name ### For Gemini CLI: **Note**: While Zen MCP connects to Gemini CLI, tool invocation isn't working correctly yet. See [Gemini CLI Setup](gemini-setup.md) for updates. +### For Qwen Code CLI: +1. Restart the Qwen Code CLI if it's running (`qwen exit`). +2. Run `qwen mcp list --scope user` and confirm `zen` shows `CONNECTED`. +3. Try: `"/mcp"` to inspect available tools or `"Use zen to analyze this repo"`. + ### For Codex CLI: 1. Restart Codex CLI if running 2. Open a new conversation diff --git a/run-server.ps1 b/run-server.ps1 index e145352..b757f24 100644 --- a/run-server.ps1 +++ b/run-server.ps1 @@ -1409,6 +1409,7 @@ function Invoke-McpClientConfiguration { if (!$UseDocker) { Test-ClaudeCliIntegration $PythonPath $ServerPath Test-GeminiCliIntegration (Split-Path $ServerPath -Parent) + Test-QwenCliIntegration $PythonPath $ServerPath } } @@ -1522,6 +1523,208 @@ if exist ".zen_venv\Scripts\python.exe" ( } } } + +function Show-QwenManualConfig { + param( + [string]$PythonPath, + [string]$ServerPath, + [string]$ScriptDir, + [string]$ConfigPath, + [System.Collections.IDictionary]$EnvironmentMap + ) + + Write-Host "Manual config location: $ConfigPath" -ForegroundColor Yellow + Write-Host "Add or update this entry:" -ForegroundColor Yellow + + if ($EnvironmentMap -and $EnvironmentMap.Count -gt 0) { + $pairs = $EnvironmentMap.GetEnumerator() | ForEach-Object { + $escaped = ($_.Value -replace '\\', '\\\\' -replace '"', '\\"') + ' "{0}": "{1}"' -f $_.Key, $escaped + } + + Write-Host "{" -ForegroundColor Yellow + Write-Host " \"mcpServers\": {" -ForegroundColor Yellow + Write-Host " \"zen\": {" -ForegroundColor Yellow + Write-Host " \"command\": \"$PythonPath\"," -ForegroundColor Yellow + Write-Host " \"args\": [\"$ServerPath\"]," -ForegroundColor Yellow + Write-Host " \"cwd\": \"$ScriptDir\"," -ForegroundColor Yellow + Write-Host " \"env\": {" -ForegroundColor Yellow + Write-Host ($pairs -join "`n") -ForegroundColor Yellow + Write-Host " }" -ForegroundColor Yellow + Write-Host " }" -ForegroundColor Yellow + Write-Host " }" -ForegroundColor Yellow + Write-Host "}" -ForegroundColor Yellow + } else { + Write-Host "{" -ForegroundColor Yellow + Write-Host " \"mcpServers\": {" -ForegroundColor Yellow + Write-Host " \"zen\": {" -ForegroundColor Yellow + Write-Host " \"command\": \"$PythonPath\"," -ForegroundColor Yellow + Write-Host " \"args\": [\"$ServerPath\"]," -ForegroundColor Yellow + Write-Host " \"cwd\": \"$ScriptDir\"" -ForegroundColor Yellow + Write-Host " }" -ForegroundColor Yellow + Write-Host " }" -ForegroundColor Yellow + Write-Host "}" -ForegroundColor Yellow + } +} + +function Test-QwenCliIntegration { + param([string]$PythonPath, [string]$ServerPath) + + if (!(Test-Command "qwen")) { + return + } + + Write-Info "Qwen CLI detected - checking configuration..." + + $configPath = Join-Path $env:USERPROFILE ".qwen\settings.json" + $configDir = Split-Path $configPath -Parent + $scriptDir = Split-Path $ServerPath -Parent + + $configStatus = "missing" + $config = @{} + + if (Test-Path $configPath) { + try { + Add-Type -AssemblyName System.Web.Extensions -ErrorAction SilentlyContinue + $serializer = New-Object System.Web.Script.Serialization.JavaScriptSerializer + $serializer.MaxJsonLength = 67108864 + $rawJson = Get-Content $configPath -Raw + $config = $serializer.DeserializeObject($rawJson) + if (-not ($config -is [System.Collections.IDictionary])) { + $config = @{} + } + + if ($config.ContainsKey('mcpServers') -and $config['mcpServers'] -is [System.Collections.IDictionary]) { + $servers = $config['mcpServers'] + if ($servers.Contains('zen') -and $servers['zen'] -is [System.Collections.IDictionary]) { + $zenConfig = $servers['zen'] + $commandMatches = ($zenConfig['command'] -eq $PythonPath) + + $argsValue = $zenConfig['args'] + $argsList = @() + if ($argsValue -is [System.Collections.IEnumerable] -and $argsValue -isnot [string]) { + $argsList = @($argsValue) + } elseif ($null -ne $argsValue) { + $argsList = @($argsValue) + } + $argsMatches = ($argsList.Count -eq 1 -and $argsList[0] -eq $ServerPath) + + $cwdValue = $null + if ($zenConfig.Contains('cwd')) { + $cwdValue = $zenConfig['cwd'] + } + $cwdMatches = ([string]::IsNullOrEmpty($cwdValue) -or $cwdValue -eq $scriptDir) + + if ($commandMatches -and $argsMatches -and $cwdMatches) { + Write-Success "Qwen CLI already configured for zen server" + return + } + + $configStatus = "mismatch" + Write-Warning "Existing Qwen CLI configuration differs from the current setup." + } + } + } catch { + $configStatus = "invalid" + Write-Warning "Unable to parse Qwen CLI settings at $configPath ($_)." + $config = @{} + } + } + + $envMap = [ordered]@{} + if (Test-Path ".env") { + foreach ($line in Get-Content ".env") { + $trimmed = $line.Trim() + if ([string]::IsNullOrWhiteSpace($trimmed) -or $trimmed.StartsWith('#')) { + continue + } + + if ($line -match '^\s*([^=]+)=(.*)$') { + $key = $matches[1].Trim() + $value = $matches[2] + $value = ($value -replace '\s+#.*$', '').Trim() + if ($value.StartsWith('"') -and $value.EndsWith('"')) { + $value = $value.Substring(1, $value.Length - 2) + } + if ([string]::IsNullOrWhiteSpace($value)) { + $value = [Environment]::GetEnvironmentVariable($key, "Process") + } + if (![string]::IsNullOrWhiteSpace($value) -and $value -notmatch '^your_.*_here$') { + $envMap[$key] = $value + } + } + } + } + + $extraKeys = @( + "GEMINI_API_KEY","OPENAI_API_KEY","XAI_API_KEY","DIAL_API_KEY","OPENROUTER_API_KEY", + "AZURE_OPENAI_API_KEY","AZURE_OPENAI_ENDPOINT","AZURE_OPENAI_API_VERSION","AZURE_OPENAI_ALLOWED_MODELS","AZURE_MODELS_CONFIG_PATH", + "CUSTOM_API_URL","CUSTOM_API_KEY","CUSTOM_MODEL_NAME","DEFAULT_MODEL","GOOGLE_ALLOWED_MODELS", + "OPENAI_ALLOWED_MODELS","OPENROUTER_ALLOWED_MODELS","XAI_ALLOWED_MODELS","DEFAULT_THINKING_MODE_THINKDEEP", + "DISABLED_TOOLS","CONVERSATION_TIMEOUT_HOURS","MAX_CONVERSATION_TURNS","LOG_LEVEL","ZEN_MCP_FORCE_ENV_OVERRIDE" + ) + + foreach ($key in $extraKeys) { + if (-not $envMap.Contains($key)) { + $value = [Environment]::GetEnvironmentVariable($key, "Process") + if (![string]::IsNullOrWhiteSpace($value) -and $value -notmatch '^your_.*_here$') { + $envMap[$key] = $value + } + } + } + + $prompt = "Configure Zen for Qwen CLI? (y/N)" + if ($configStatus -eq "mismatch" -or $configStatus -eq "invalid") { + $prompt = "Update Qwen CLI zen configuration? (y/N)" + } + + $response = Read-Host $prompt + if ($response -ne 'y' -and $response -ne 'Y') { + Write-Info "Skipping Qwen CLI integration" + Show-QwenManualConfig $PythonPath $ServerPath $scriptDir $configPath $envMap + return + } + + if (!(Test-Path $configDir)) { + New-Item -ItemType Directory -Path $configDir -Force | Out-Null + } + + if (Test-Path $configPath -and $configStatus -ne "missing") { + Manage-ConfigBackups $configPath | Out-Null + } + + try { + if (-not ($config -is [System.Collections.IDictionary])) { + $config = @{} + } + + if (-not $config.ContainsKey('mcpServers') -or $config['mcpServers'] -isnot [System.Collections.IDictionary]) { + $config['mcpServers'] = @{} + } + + $zenConfig = [ordered]@{ + command = $PythonPath + args = @($ServerPath) + cwd = $scriptDir + } + + if ($envMap.Count -gt 0) { + $zenConfig['env'] = $envMap + } + + $config['mcpServers']['zen'] = $zenConfig + + $json = ($config | ConvertTo-Json -Depth 20) + Set-Content -Path $configPath -Value $json -Encoding UTF8 + + Write-Success "Successfully configured Qwen CLI" + Write-Host " Config: $configPath" -ForegroundColor Gray + Write-Host " Restart Qwen CLI to use Zen MCP Server" -ForegroundColor Gray + } catch { + Write-Error "Failed to update Qwen CLI configuration: $_" + Show-QwenManualConfig $PythonPath $ServerPath $scriptDir $configPath $envMap + } +} "@ -ForegroundColor Yellow } } @@ -1606,6 +1809,7 @@ function Show-ConfigInstructions { Write-Host "βœ“ Windsurf" -ForegroundColor White Write-Host "βœ“ Trae" -ForegroundColor White Write-Host "βœ“ Gemini CLI" -ForegroundColor White + Write-Host "βœ“ Qwen CLI" -ForegroundColor White Write-Host "" Write-Host "The script automatically detects and configures compatible clients." -ForegroundColor Gray Write-Host "Restart your MCP clients after configuration to use the Zen MCP Server." -ForegroundColor Yellow diff --git a/run-server.sh b/run-server.sh index ccc64af..f91a513 100755 --- a/run-server.sh +++ b/run-server.sh @@ -1779,6 +1779,247 @@ EOF fi } +# Print manual Qwen CLI configuration guidance +print_qwen_manual_instructions() { + local python_cmd="$1" + local server_path="$2" + local script_dir="$3" + local config_path="$4" + local env_lines="$5" + + local env_array=() + if [[ -n "$env_lines" ]]; then + while IFS= read -r line; do + [[ -z "$line" ]] && continue + env_array+=("$line") + done <<< "$env_lines" + fi + + echo "Manual config location: $config_path" + echo "Add or update this entry:" + + local env_block="" + if [[ ${#env_array[@]} -gt 0 ]]; then + env_block=$' "env": {\n' + local first=true + for env_entry in "${env_array[@]}"; do + local key="${env_entry%%=*}" + local value="${env_entry#*=}" + value=${value//\\/\\\\} + value=${value//"/\\"} + if [[ "$first" == true ]]; then + first=false + env_block+=" \"$key\": \"$value\"" + else + env_block+=$',\n ' + env_block+="\"$key\": \"$value\"" + fi + done + env_block+=$'\n }' + fi + + if [[ -n "$env_block" ]]; then + cat << EOF +{ + "mcpServers": { + "zen": { + "command": "$python_cmd", + "args": ["$server_path"], + "cwd": "$script_dir", +$env_block + } + } +} +EOF + else + cat << EOF +{ + "mcpServers": { + "zen": { + "command": "$python_cmd", + "args": ["$server_path"], + "cwd": "$script_dir" + } + } +} +EOF + fi +} + +# Check and update Qwen Code CLI configuration +check_qwen_cli_integration() { + local python_cmd="$1" + local server_path="$2" + + if ! command -v qwen &> /dev/null; then + return 0 + fi + + local qwen_config="$HOME/.qwen/settings.json" + local script_dir + script_dir=$(dirname "$server_path") + + local env_vars + env_vars=$(parse_env_variables) + local env_array=() + if [[ -n "$env_vars" ]]; then + while IFS= read -r line; do + if [[ -n "$line" && "$line" =~ ^([^=]+)=(.*)$ ]]; then + env_array+=("${BASH_REMATCH[1]}=${BASH_REMATCH[2]}") + fi + done <<< "$env_vars" + fi + + local env_lines="" + if [[ ${#env_array[@]} -gt 0 ]]; then + env_lines=$(printf '%s\n' "${env_array[@]}") + fi + + local config_status=3 + if [[ -f "$qwen_config" ]]; then + if python3 - "$qwen_config" "$python_cmd" "$server_path" "$script_dir" <<'PYCONF' +import json +import sys + +config_path, expected_cmd, expected_arg, expected_cwd = sys.argv[1:5] +try: + with open(config_path, 'r', encoding='utf-8') as f: + data = json.load(f) +except FileNotFoundError: + sys.exit(1) +except Exception: + sys.exit(5) + +servers = data.get('mcpServers') +if not isinstance(servers, dict): + sys.exit(3) + +config = servers.get('zen') +if not isinstance(config, dict): + sys.exit(3) + +cmd = config.get('command') +args = config.get('args') or [] +cwd = config.get('cwd') + +cwd_matches = cwd in (None, "", expected_cwd) +if cmd == expected_cmd and len(args) == 1 and args[0] == expected_arg and cwd_matches: + sys.exit(0) + +sys.exit(4) +PYCONF + then + config_status=0 + else + config_status=$? + if [[ $config_status -eq 1 ]]; then + config_status=3 + fi + fi + fi + + if [[ $config_status -eq 0 ]]; then + return 0 + fi + + echo "" + + if [[ $config_status -eq 4 ]]; then + print_warning "Found existing Qwen CLI zen configuration with different settings." + elif [[ $config_status -eq 5 ]]; then + print_warning "Unable to parse Qwen CLI settings; replacing with a fresh entry may help." + fi + + local prompt="Configure Zen for Qwen CLI? (Y/n): " + if [[ $config_status -eq 4 || $config_status -eq 5 ]]; then + prompt="Update Qwen CLI zen configuration? (Y/n): " + fi + + read -p "$prompt" -n 1 -r + echo "" + if [[ $REPLY =~ ^[Nn]$ ]]; then + print_info "Skipping Qwen CLI integration" + print_qwen_manual_instructions "$python_cmd" "$server_path" "$script_dir" "$qwen_config" "$env_lines" + return 0 + fi + + mkdir -p "$(dirname "$qwen_config")" 2>/dev/null || true + if [[ -f "$qwen_config" && $config_status -ne 3 ]]; then + cp "$qwen_config" "${qwen_config}.backup_$(date +%Y%m%d_%H%M%S)" 2>/dev/null || true + fi + + local update_output + local update_status=0 + update_output=$(ZEN_QWEN_ENV="$env_lines" ZEN_QWEN_CMD="$python_cmd" ZEN_QWEN_ARG="$server_path" ZEN_QWEN_CWD="$script_dir" python3 - "$qwen_config" <<'PYUPDATE' +import json +import os +import pathlib +import sys + +config_path = pathlib.Path(sys.argv[1]) +cmd = os.environ['ZEN_QWEN_CMD'] +arg = os.environ['ZEN_QWEN_ARG'] +cwd = os.environ['ZEN_QWEN_CWD'] +env_lines = os.environ.get('ZEN_QWEN_ENV', '').splitlines() + +env_map = {} +for line in env_lines: + if not line.strip(): + continue + if '=' in line: + key, value = line.split('=', 1) + env_map[key] = value + +if config_path.exists(): + try: + with config_path.open('r', encoding='utf-8') as f: + data = json.load(f) + except Exception: + data = {} +else: + data = {} + +if not isinstance(data, dict): + data = {} + +servers = data.get('mcpServers') +if not isinstance(servers, dict): + servers = {} + data['mcpServers'] = servers + +zen_config = { + 'command': cmd, + 'args': [arg], + 'cwd': cwd, +} + +if env_map: + zen_config['env'] = env_map + +servers['zen'] = zen_config + +config_path.parent.mkdir(parents=True, exist_ok=True) +tmp_path = config_path.with_suffix(config_path.suffix + '.tmp') +with tmp_path.open('w', encoding='utf-8') as f: + json.dump(data, f, indent=2) + f.write('\n') +tmp_path.replace(config_path) +PYUPDATE + ) || update_status=$? + + if [[ $update_status -eq 0 ]]; then + print_success "Successfully configured Qwen CLI" + echo " Config: $qwen_config" + echo " Restart Qwen CLI to use Zen MCP Server" + else + print_error "Failed to update Qwen CLI config" + if [[ -n "$update_output" ]]; then + echo "$update_output" + fi + print_qwen_manual_instructions "$python_cmd" "$server_path" "$script_dir" "$qwen_config" "$env_lines" + fi +} + # Display configuration instructions display_config_instructions() { local python_cmd="$1" @@ -1792,7 +2033,7 @@ display_config_instructions() { echo "===== $config_header =====" printf '%*s\n' "$((${#config_header} + 12))" | tr ' ' '=' echo "" - echo "To use Zen MCP Server with your Claude clients:" + echo "To use Zen MCP Server with your CLI clients:" echo "" print_info "1. For Claude Code (CLI):" @@ -1833,19 +2074,34 @@ display_config_instructions() { done <<< "$env_vars" fi - cat << EOF + if [[ -n "$example_env" ]]; then + cat << EOF { "mcpServers": { "zen": { "command": "$python_cmd", - "args": ["$server_path"]$(if [[ -n "$example_env" ]]; then echo ","; fi)$(if [[ -n "$example_env" ]]; then echo " - \"env\": { + "args": ["$server_path"], + "cwd": "$script_dir", + "env": { $(echo -e "$example_env") - }"; fi) + } } } } EOF + else + cat << EOF + { + "mcpServers": { + "zen": { + "command": "$python_cmd", + "args": ["$server_path"], + "cwd": "$script_dir" + } + } + } +EOF + fi # Show platform-specific config location local config_path=$(get_claude_config_path) @@ -1873,6 +2129,39 @@ EOF EOF echo "" + print_info "For Qwen Code CLI:" + echo " Add this configuration to ~/.qwen/settings.json:" + echo "" + if [[ -n "$example_env" ]]; then + cat << EOF + { + "mcpServers": { + "zen": { + "command": "$python_cmd", + "args": ["$server_path"], + "cwd": "$script_dir", + "env": { +$(echo -e "$example_env") + } + } + } + } +EOF + else + cat << EOF + { + "mcpServers": { + "zen": { + "command": "$python_cmd", + "args": ["$server_path"], + "cwd": "$script_dir" + } + } + } +EOF + fi + echo "" + print_info "For Codex CLI:" echo " Add this configuration to ~/.codex/config.toml:" echo "" @@ -2131,12 +2420,15 @@ main() { # Step 11: Check Codex CLI integration check_codex_cli_integration - # Step 12: Display log information + # Step 12: Check Qwen CLI integration + check_qwen_cli_integration "$python_cmd" "$server_path" + + # Step 13: Display log information echo "" echo "Logs will be written to: $script_dir/$LOG_DIR/$LOG_FILE" echo "" - # Step 13: Handle command line arguments + # Step 14: Handle command line arguments if [[ "$arg" == "-f" ]] || [[ "$arg" == "--follow" ]]; then follow_logs else