diff --git a/README.md b/README.md index e957761..b62491d 100644 --- a/README.md +++ b/README.md @@ -762,6 +762,158 @@ Use the same configuration format as Claude Desktop above. Claude Code automatically detects MCP servers in the current directory. No additional configuration needed. +### OpenCode (Windows) + +OpenCode uses a different MCP configuration schema than Claude Desktop. Use +`setup-windows-opencode.ps1` to verify the local setup and write the correct +OpenCode `mcp` entry. + +OpenCode project configuration is written to `opencode.json` in the target +project root. The script keeps the KiCAD MCP server repository separate from the +target project: + +- `McpServerPath` is this repository, where `dist/index.js` is built +- `ProjectPath` is the project that should receive `opencode.json` + +**When this is useful:** + +- You use [OpenCode](https://opencode.ai/) as your MCP client on Windows +- You want a project-local MCP server available only in one project +- You want a global OpenCode MCP server available from any workspace +- You need to verify KiCAD Python (`pcbnew`), Node.js, and `dist/index.js` + before changing OpenCode configuration + +#### Verify setup without changes + +Use this first when diagnosing installation or path problems. It detects KiCAD, +tests `pcbnew`, checks Node.js, and verifies the built MCP entrypoint. + +```powershell +.\setup-windows-opencode.ps1 -Verify -SkipInstall -SkipBuild +``` + +#### Preview OpenCode configuration + +Use dry run mode when you want to inspect the exact JSON before writing it. + +```powershell +.\setup-windows-opencode.ps1 -DryRun -SkipInstall -SkipBuild +``` + +Example generated OpenCode shape: + +```json +{ + "$schema": "https://opencode.ai/config.json", + "mcp": { + "kicad": { + "type": "local", + "command": ["node", "C:\\path\\to\\KiCAD-MCP-Server\\dist\\index.js"], + "environment": { + "NODE_ENV": "production", + "LOG_LEVEL": "info", + "KICAD_AUTO_LAUNCH": "false", + "KICAD_MCP_DEV": "0", + "PYTHONPATH": "C:\\Program Files\\KiCad\\10.0\\bin\\Lib\\site-packages" + }, + "enabled": true, + "timeout": 30000 + } + } +} +``` + +A copyable template is also provided at `config/opencode.json`. Replace the +placeholder paths before using it directly. + +#### Apply project-local configuration + +Use this when you only want KiCAD MCP enabled for one project. The script writes +`opencode.json` in the target project root and backs up an existing file before +changing it. + +```powershell +.\setup-windows-opencode.ps1 -Apply -Scope project +``` + +By default, `ProjectPath` is the current working directory. + +To configure another project, pass `-ProjectPath`: + +```powershell +.\setup-windows-opencode.ps1 -Apply -Scope project -ProjectPath "C:\path\to\your-project" +``` + +If the setup script is not located in the KiCAD MCP Server repository, pass +`-McpServerPath` so the generated config points to the correct `dist/index.js`: + +```powershell +.\setup-windows-opencode.ps1 ` + -Apply ` + -Scope project ` + -ProjectPath "C:\path\to\your-project" ` + -McpServerPath "C:\path\to\KiCAD-MCP-Server" +``` + +#### Apply global OpenCode configuration + +Use this when you want the KiCAD MCP server available from any OpenCode +workspace. The script writes `%USERPROFILE%\.config\opencode\opencode.json`. + +```powershell +.\setup-windows-opencode.ps1 -Apply -Scope global +``` + +#### Use a custom MCP server name + +Use this when testing multiple forks or keeping separate development and stable +KiCAD MCP entries. + +```powershell +.\setup-windows-opencode.ps1 -Apply -Scope project -Name kicad-dev +``` + +#### Use a custom KiCAD installation path + +Use this when KiCAD is installed outside the standard Windows locations. + +```powershell +.\setup-windows-opencode.ps1 -Apply -Scope project -KiCadRoot "D:\Apps\KiCad\10.0" +``` + +#### Skip install or build steps + +Use these flags when dependencies are already installed or the project is +already built. + +```powershell +.\setup-windows-opencode.ps1 -Apply -Scope project -SkipInstall -SkipBuild +``` + +#### After applying configuration + +1. Fully quit OpenCode. +2. Start OpenCode again so it reloads `opencode.json`. +3. Ask OpenCode to use the `kicad` MCP server and run `check_kicad_ui`. + +#### Disable the OpenCode MCP server + +To disable the server without removing the full configuration, set the entry to +`enabled: false` and restart OpenCode. + +```json +{ + "mcp": { + "kicad": { + "enabled": false + } + } +} +``` + +If OpenCode is running, the MCP server process is managed by OpenCode and +normally stops when OpenCode exits. + ### JLCPCB Integration Setup (Optional) The JLCPCB integration provides two modes that can be used independently or together: diff --git a/config/opencode.json b/config/opencode.json new file mode 100644 index 0000000..92f1435 --- /dev/null +++ b/config/opencode.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://opencode.ai/config.json", + "mcp": { + "kicad": { + "type": "local", + "command": ["node", "C:\\path\\to\\KiCAD-MCP-Server\\dist\\index.js"], + "environment": { + "NODE_ENV": "production", + "LOG_LEVEL": "info", + "KICAD_AUTO_LAUNCH": "false", + "KICAD_MCP_DEV": "0", + "PYTHONPATH": "C:\\Program Files\\KiCad\\10.0\\bin\\Lib\\site-packages" + }, + "enabled": true, + "timeout": 30000 + } + } +} diff --git a/setup-windows-opencode.ps1 b/setup-windows-opencode.ps1 new file mode 100644 index 0000000..f638df3 --- /dev/null +++ b/setup-windows-opencode.ps1 @@ -0,0 +1,521 @@ +<# +.SYNOPSIS + KiCAD MCP Server - Windows OpenCode Configuration Script + +.DESCRIPTION + Verifies the local KiCAD MCP Server setup and creates or updates an + OpenCode configuration with a local MCP server entry. + + The MCP server path and OpenCode project path are intentionally separate: + - McpServerPath is this KiCAD MCP Server repository, where dist/index.js lives. + - ProjectPath is the project that should receive opencode.json. + + The script writes OpenCode's config shape, not Claude Desktop's: + - top-level key: mcp + - server type: local + - command: array containing executable and arguments + - environment: object containing environment variables + +.PARAMETER Apply + Write the generated MCP entry to the selected OpenCode config file. + +.PARAMETER DryRun + Print the generated OpenCode config without writing files. + +.PARAMETER Verify + Run detection and validation only. No files are modified. + +.PARAMETER Scope + Target OpenCode config scope when applying: project or global. + project writes opencode.json in ProjectPath. + global writes ~/.config/opencode/opencode.json. + +.PARAMETER Name + MCP server name to use in OpenCode config. Default: kicad. + +.PARAMETER KiCadRoot + Explicit KiCAD installation root, for example C:\Program Files\KiCad\9.0. + +.PARAMETER ProjectPath + Project directory that should receive opencode.json for project-only setup. + Defaults to the current working directory. + +.PARAMETER McpServerPath + KiCAD MCP Server repository directory. Defaults to the directory containing + this script. + +.PARAMETER ConfigPath + Explicit OpenCode config path. Overrides Scope. + +.PARAMETER SkipInstall + Skip npm install. + +.PARAMETER SkipBuild + Skip npm run build. + +.PARAMETER Force + Allow config generation even when KiCAD Python/pcbnew validation fails. + +.EXAMPLE + .\setup-windows-opencode.ps1 -Verify + +.EXAMPLE + .\setup-windows-opencode.ps1 -DryRun + +.EXAMPLE + .\setup-windows-opencode.ps1 -Apply -Scope project -ProjectPath C:\path\to\your-project + +.EXAMPLE + .\setup-windows-opencode.ps1 -Apply -Scope global -Name kicad-dev +#> + +param( + [switch]$Apply, + [switch]$DryRun, + [switch]$Verify, + [ValidateSet('project', 'global')] + [string]$Scope = 'project', + [string]$Name = 'kicad', + [string]$KiCadRoot = '', + [string]$ProjectPath = '', + [string]$McpServerPath = '', + [string]$ConfigPath = '', + [switch]$SkipInstall, + [switch]$SkipBuild, + [switch]$Force +) + +Set-StrictMode -Version 2.0 +$ErrorActionPreference = 'Stop' + +function Write-Success { param([string]$Message) Write-Host "[OK] $Message" -ForegroundColor Green } +function Write-Error-Custom { param([string]$Message) Write-Host "[ERROR] $Message" -ForegroundColor Red } +function Write-Warning-Custom { param([string]$Message) Write-Host "[WARN] $Message" -ForegroundColor Yellow } +function Write-Info { param([string]$Message) Write-Host "[INFO] $Message" -ForegroundColor Cyan } +function Write-Step { param([string]$Message) Write-Host "`n=== $Message ===" -ForegroundColor Magenta } + +function ConvertTo-HashtableDeep { + param([Parameter(ValueFromPipeline = $true)]$InputObject) + + if ($null -eq $InputObject) { + return $null + } + + if ($InputObject -is [System.Collections.IEnumerable] -and $InputObject -isnot [string] -and $InputObject -isnot [System.Collections.IDictionary] -and $InputObject -isnot [pscustomobject]) { + $items = @() + foreach ($item in $InputObject) { + $items += ConvertTo-HashtableDeep $item + } + return $items + } + + if ($InputObject -is [System.Collections.IDictionary]) { + $hash = [ordered]@{} + foreach ($key in $InputObject.Keys) { + $hash[$key] = ConvertTo-HashtableDeep $InputObject[$key] + } + return $hash + } + + if ($InputObject -is [pscustomobject]) { + $hash = [ordered]@{} + foreach ($property in $InputObject.PSObject.Properties) { + $hash[$property.Name] = ConvertTo-HashtableDeep $property.Value + } + return $hash + } + + return $InputObject +} + +function Find-KiCadInstallation { + param([string]$ExplicitRoot) + + if ($ExplicitRoot) { + $resolved = [System.Environment]::ExpandEnvironmentVariables($ExplicitRoot) + if (Test-Path -LiteralPath $resolved) { + return Get-KiCadInfo -Root $resolved + } + return $null + } + + $basePaths = @( + 'C:\Program Files\KiCad', + 'C:\Program Files (x86)\KiCad', + (Join-Path $env:LOCALAPPDATA 'Programs\KiCad') + ) + + $preferredVersions = @('10.0', '9.1', '9.0', '8.0') + foreach ($basePath in $basePaths) { + foreach ($version in $preferredVersions) { + $root = Join-Path $basePath $version + $info = Get-KiCadInfo -Root $root + if ($info) { + return $info + } + } + } + + foreach ($basePath in $basePaths) { + if (-not (Test-Path -LiteralPath $basePath)) { + continue + } + + $children = Get-ChildItem -LiteralPath $basePath -Directory -ErrorAction SilentlyContinue | Sort-Object Name -Descending + foreach ($child in $children) { + $info = Get-KiCadInfo -Root $child.FullName + if ($info) { + return $info + } + } + } + + return $null +} + +function Get-KiCadInfo { + param([string]$Root) + + if (-not $Root -or -not (Test-Path -LiteralPath $Root)) { + return $null + } + + $pythonExeCandidates = @( + (Join-Path $Root 'bin\python.exe'), + (Join-Path $Root 'bin\Python.exe') + ) + $pythonExe = $pythonExeCandidates | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1 + if (-not $pythonExe) { + return $null + } + + $pythonLibCandidates = @( + (Join-Path $Root 'lib\python3\dist-packages'), + (Join-Path $Root 'bin\Lib\site-packages') + ) + $pythonLib = $pythonLibCandidates | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1 + if (-not $pythonLib) { + $pythonLib = $pythonLibCandidates[0] + } + + return [ordered]@{ + Root = $Root + Version = Split-Path -Leaf $Root + PythonExe = $pythonExe + PythonLib = $pythonLib + } +} + +function Get-TargetConfigPath { + param( + [string]$ExplicitPath, + [string]$SelectedScope, + [string]$TargetProjectPath + ) + + if ($ExplicitPath) { + return [System.Environment]::ExpandEnvironmentVariables($ExplicitPath) + } + + if ($SelectedScope -eq 'global') { + return Join-Path $env:USERPROFILE '.config\opencode\opencode.json' + } + + return Join-Path $TargetProjectPath 'opencode.json' +} + +function Resolve-DirectoryPath { + param( + [string]$Path, + [string]$FallbackPath, + [string]$Label + ) + + $candidate = if ($Path) { [System.Environment]::ExpandEnvironmentVariables($Path) } else { $FallbackPath } + if (-not (Test-Path -LiteralPath $candidate -PathType Container)) { + throw "$Label directory does not exist: $candidate" + } + + return (Resolve-Path -LiteralPath $candidate).Path +} + +function Read-OpenCodeConfig { + param([string]$Path) + + if (-not (Test-Path -LiteralPath $Path)) { + return [ordered]@{ + '$schema' = 'https://opencode.ai/config.json' + } + } + + $raw = Get-Content -LiteralPath $Path -Raw + if ([string]::IsNullOrWhiteSpace($raw)) { + return [ordered]@{ + '$schema' = 'https://opencode.ai/config.json' + } + } + + try { + $parsed = $raw | ConvertFrom-Json + $config = ConvertTo-HashtableDeep $parsed + if (-not $config.Contains('$schema')) { + $schemaConfig = [ordered]@{ '$schema' = 'https://opencode.ai/config.json' } + foreach ($key in $config.Keys) { + $schemaConfig[$key] = $config[$key] + } + return $schemaConfig + } + return $config + } catch { + throw "Failed to parse existing OpenCode config '$Path': $($_.Exception.Message)" + } +} + +function Set-McpEntry { + param( + [System.Collections.IDictionary]$Config, + [string]$ServerName, + [string]$DistPath, + [string]$PythonPath + ) + + if (-not $Config.Contains('mcp') -or $null -eq $Config['mcp']) { + $Config['mcp'] = [ordered]@{} + } + + $environment = [ordered]@{ + NODE_ENV = 'production' + LOG_LEVEL = 'info' + KICAD_AUTO_LAUNCH = 'false' + KICAD_MCP_DEV = '0' + } + + if ($PythonPath) { + $environment['PYTHONPATH'] = $PythonPath + } + + $Config['mcp'][$ServerName] = [ordered]@{ + type = 'local' + command = @('node', $DistPath) + environment = $environment + enabled = $true + timeout = 30000 + } +} + +function Write-OpenCodeConfig { + param( + [System.Collections.IDictionary]$Config, + [string]$Path + ) + + $parent = Split-Path -Parent $Path + if ($parent -and -not (Test-Path -LiteralPath $parent)) { + New-Item -ItemType Directory -Path $parent -Force | Out-Null + } + + if (Test-Path -LiteralPath $Path) { + $timestamp = Get-Date -Format 'yyyyMMdd-HHmmss' + $backupPath = "$Path.bak-$timestamp" + Copy-Item -LiteralPath $Path -Destination $backupPath + Write-Success "Backup written: $backupPath" + } + + $json = $Config | ConvertTo-Json -Depth 20 + $json | Out-File -LiteralPath $Path -Encoding UTF8 +} + +if (-not $Apply -and -not $DryRun -and -not $Verify) { + $Verify = $true +} + +if (($Apply.IsPresent -and $DryRun.IsPresent) -or ($Apply.IsPresent -and $Verify.IsPresent) -or ($DryRun.IsPresent -and $Verify.IsPresent)) { + throw 'Choose only one of -Apply, -DryRun, or -Verify.' +} + +Write-Host @" +============================================================ + KiCAD MCP Server - OpenCode Windows Setup +============================================================ +"@ -ForegroundColor Cyan + +$script:Results = [ordered]@{ + KiCadFound = $false + PcbnewImport = $false + NodeFound = $false + NpmInstall = $false + ProjectBuilt = $false + DistFound = $false + ConfigReady = $false + Errors = @() +} + +$ScriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path +$McpServerRoot = Resolve-DirectoryPath -Path $McpServerPath -FallbackPath $ScriptRoot -Label 'MCP server' +$TargetProjectRoot = Resolve-DirectoryPath -Path $ProjectPath -FallbackPath (Get-Location).Path -Label 'Project' +$DistPath = Join-Path $McpServerRoot 'dist\index.js' +$TargetConfigPath = Get-TargetConfigPath -ExplicitPath $ConfigPath -SelectedScope $Scope -TargetProjectPath $TargetProjectRoot + +Write-Info "MCP server path: $McpServerRoot" +if ($Scope -eq 'project') { + Write-Info "OpenCode project path: $TargetProjectRoot" +} + +Write-Step 'Step 1: Detecting KiCAD Installation' +$kicad = Find-KiCadInstallation -ExplicitRoot $KiCadRoot +if ($kicad) { + $script:Results.KiCadFound = $true + Write-Success "Found KiCAD at: $($kicad.Root)" + Write-Info "KiCAD Python: $($kicad.PythonExe)" + Write-Info "KiCAD PYTHONPATH: $($kicad.PythonLib)" +} else { + Write-Error-Custom 'KiCAD was not found in standard Windows locations.' + Write-Warning-Custom 'Use -KiCadRoot "C:\Path\To\KiCad\9.0" if KiCAD is installed elsewhere.' + $script:Results.Errors += 'KiCAD not found' +} + +Write-Step 'Step 2: Testing KiCAD pcbnew Module' +if ($kicad) { + $testScript = "import pcbnew; print('SUCCESS:' + pcbnew.GetBuildVersion())" + $pcbnewResult = & $kicad.PythonExe -c $testScript 2>&1 + if ($LASTEXITCODE -eq 0 -and $pcbnewResult -match 'SUCCESS:(.+)') { + $script:Results.PcbnewImport = $true + Write-Success "pcbnew imported successfully: $($matches[1])" + } else { + Write-Error-Custom 'KiCAD Python could not import pcbnew.' + Write-Warning-Custom "Output: $pcbnewResult" + $script:Results.Errors += 'pcbnew import failed' + } +} else { + Write-Warning-Custom 'Skipping pcbnew test because KiCAD was not found.' +} + +Write-Step 'Step 3: Checking Node.js' +try { + $nodeVersion = node --version 2>$null + if ($LASTEXITCODE -eq 0) { + $script:Results.NodeFound = $true + Write-Success "Node.js found: $nodeVersion" + $major = [int]($nodeVersion -replace '^v(\d+)\..*$', '$1') + if ($major -lt 18) { + Write-Warning-Custom "Node.js 18+ is recommended. Found $nodeVersion." + } + } else { + throw 'node command failed' + } +} catch { + Write-Error-Custom 'Node.js was not found. Install Node.js 18+ from https://nodejs.org/.' + $script:Results.Errors += 'Node.js not found' +} + +Write-Step 'Step 4: Installing Node Dependencies' +if ($SkipInstall) { + Write-Info 'Skipping npm install because -SkipInstall was specified.' + $script:Results.NpmInstall = $true +} elseif ($script:Results.NodeFound) { + Push-Location $McpServerRoot + try { + npm install + if ($LASTEXITCODE -eq 0) { + $script:Results.NpmInstall = $true + Write-Success 'npm install completed.' + } else { + Write-Error-Custom 'npm install failed.' + $script:Results.Errors += 'npm install failed' + } + } finally { + Pop-Location + } +} else { + Write-Warning-Custom 'Skipping npm install because Node.js was not found.' +} + +Write-Step 'Step 5: Building TypeScript Project' +if ($SkipBuild) { + Write-Info 'Skipping npm run build because -SkipBuild was specified.' +} elseif ($script:Results.NodeFound) { + Push-Location $McpServerRoot + try { + npm run build + if ($LASTEXITCODE -eq 0) { + $script:Results.ProjectBuilt = $true + Write-Success 'npm run build completed.' + } else { + Write-Error-Custom 'npm run build failed.' + $script:Results.Errors += 'TypeScript build failed' + } + } finally { + Pop-Location + } +} else { + Write-Warning-Custom 'Skipping build because Node.js was not found.' +} + +if (Test-Path -LiteralPath $DistPath) { + $script:Results.DistFound = $true + Write-Success "Found MCP server entrypoint: $DistPath" +} else { + Write-Error-Custom "MCP server entrypoint was not found: $DistPath" + $script:Results.Errors += 'dist/index.js not found' +} + +Write-Step 'Step 6: Preparing OpenCode Configuration' +$canGenerate = $script:Results.NodeFound -and $script:Results.DistFound +if (-not $Force) { + $canGenerate = $canGenerate -and $script:Results.KiCadFound -and $script:Results.PcbnewImport +} + +if ($canGenerate) { + $config = Read-OpenCodeConfig -Path $TargetConfigPath + $pythonPath = if ($kicad) { $kicad.PythonLib } else { '' } + Set-McpEntry -Config $config -ServerName $Name -DistPath $DistPath -PythonPath $pythonPath + $script:Results.ConfigReady = $true + + Write-Success "OpenCode MCP entry prepared for server name '$Name'." + Write-Info "Target config: $TargetConfigPath" + Write-Host '' + Write-Host ($config | ConvertTo-Json -Depth 20) -ForegroundColor Gray + + if ($Apply) { + Write-OpenCodeConfig -Config $config -Path $TargetConfigPath + Write-Success "OpenCode config written: $TargetConfigPath" + } elseif ($DryRun) { + Write-Info 'Dry run only. No files were modified.' + } else { + Write-Info 'Verify mode only. No files were modified.' + } +} else { + Write-Warning-Custom 'OpenCode config was not generated because required checks failed.' + Write-Warning-Custom 'Use -Force to generate config anyway, or resolve the errors above and run again.' +} + +Write-Step 'Setup Summary' +Write-Host " KiCAD Installation: $(if ($script:Results.KiCadFound) { '[OK] Found' } else { '[ERROR] Not Found' })" +Write-Host " pcbnew Module: $(if ($script:Results.PcbnewImport) { '[OK] Working' } else { '[ERROR] Failed' })" +Write-Host " Node.js: $(if ($script:Results.NodeFound) { '[OK] Found' } else { '[ERROR] Not Found' })" +Write-Host " npm install: $(if ($script:Results.NpmInstall) { '[OK] Complete/Skipped' } else { '[WARN] Not Complete' })" +Write-Host " TypeScript Build: $(if ($script:Results.ProjectBuilt -or $SkipBuild) { '[OK] Complete/Skipped' } else { '[WARN] Not Complete' })" +Write-Host " dist/index.js: $(if ($script:Results.DistFound) { '[OK] Found' } else { '[ERROR] Missing' })" +Write-Host " OpenCode Config: $(if ($script:Results.ConfigReady) { '[OK] Ready' } else { '[ERROR] Not Ready' })" + +if ($script:Results.Errors.Count -gt 0) { + Write-Host '' + Write-Host 'Errors:' -ForegroundColor Red + foreach ($item in $script:Results.Errors) { + Write-Host " - $item" -ForegroundColor Red + } +} + +if ($Apply -and $script:Results.ConfigReady) { + Write-Host '' + Write-Success 'Next steps:' + Write-Host ' 1. Fully quit and restart OpenCode.' -ForegroundColor Green + Write-Host " 2. Ask OpenCode to use the '$Name' MCP server and run check_kicad_ui." -ForegroundColor Green +} elseif (-not $Apply) { + Write-Host '' + Write-Info 'Run with -Apply to write the config after checks pass.' +} + +if (-not $script:Results.ConfigReady -and -not $Verify) { + exit 1 +}