feat: add OpenCode Windows setup script

This commit is contained in:
romybompart
2026-05-23 02:00:32 -04:00
parent 9c672d40d3
commit 95705669ca
3 changed files with 691 additions and 0 deletions

152
README.md
View File

@@ -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:

18
config/opencode.json Normal file
View File

@@ -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
}
}
}

521
setup-windows-opencode.ps1 Normal file
View File

@@ -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
}