Enabling Custom (Bicep) Language Server support in OpenCode

I wanted Bicep diagnostics to show up in OpenCode with a custom LSP setup, as I noticed the models making a bunch of mistakes without it. OpenCode supports multiple LSP servers out of the box, as well as configuring custom ones.

This requires anomalyco/opencode#15570 to be merged before functioning correctly

Why bother with LSPs in OpenCode?

The main benefit is fast feedback in the place you already work:

In practice, this shifts mistakes left: less context switching, less failed pipeline runs, faster fixes.

So how do you actually get this running?

Option 1: Install Bicep Language Server manually

If you want a stable path independent from editor updates, install the language server under your own folder (for example ~/.opencode-lsp/bicep-langserver) and point OpenCode there.

# Install script
function Install-BicepLangServer {
    param([Parameter(Mandatory = $true)][string]$DestinationPath)
    $releaseUrl = 'https://github.com/Azure/bicep/releases/latest/download/bicep-langserver.zip'
    $tempZip = Join-Path ([System.IO.Path]::GetTempPath()) 'bicep-langserver.zip'
    $dllPath = Join-Path $DestinationPath 'Bicep.LangServer.dll'
    # Check if already installed
    if (Test-Path $dllPath) {
        $updateChoice = Read-Host 'Bicep Language Server already installed. Update? [y/N]'
        if ($updateChoice.Trim().ToUpperInvariant() -ne 'Y') {
            Write-Info 'Skipping Bicep Language Server update.'
            return $true
        }
    }
    Write-Info 'Downloading Bicep Language Server...'
    try {
        Invoke-WebRequest -Uri $releaseUrl -OutFile $tempZip -UseBasicParsing
        if (Test-Path $DestinationPath) {
            Remove-Item -Recurse -Force $DestinationPath
        }
        New-Item -ItemType Directory -Path $DestinationPath -Force | Out-Null
        Expand-Archive -Path $tempZip -DestinationPath $DestinationPath -Force
        Remove-Item $tempZip -ErrorAction SilentlyContinue
        Write-Success "Bicep Language Server installed to: $DestinationPath"
        return $true
    }
    catch {
        Write-Warn "Failed to install Bicep Language Server: $($_.Exception.Message)"
        Remove-Item $tempZip -ErrorAction SilentlyContinue
        return $false
    }
}

# Adding LSP config to opencode config JSON
function Add-LspConfigToOpenCodeConfig {
    param(
        [Parameter(Mandatory = $true)][string]$ConfigJson,
        [Parameter(Mandatory = $true)][string]$LspBasePath,
        [Parameter(Mandatory = $true)][bool]$BicepInstalled,
        [Parameter(Mandatory = $true)][bool]$PsesInstalled
    )
    $configObj = $ConfigJson | ConvertFrom-Json
    # Use forward slashes for cross-platform compatibility in JSON
    $lspBasePathNormalized = $LspBasePath -replace '\\', '/'
    $lspConfig = @{}
    if ($BicepInstalled) {
        $lspConfig['bicep'] = @{
            command    = @('dotnet', "$lspBasePathNormalized/bicep-langserver/Bicep.LangServer.dll")
            extensions = @('.bicep', '.bicepparam')
        }
    }
    if ($PsesInstalled) {
        $psesStartScript = "$lspBasePathNormalized/pses/PowerShellEditorServices/Start-EditorServices.ps1"
        $psesModulesPath = "$lspBasePathNormalized/pses"
        $psesLogsPath = "$lspBasePathNormalized/pses/logs"
        $lspConfig['powershell'] = @{
            command    = @(
                'pwsh',
                '-NoLogo',
                '-NoProfile',
                '-Command',
                "& '$psesStartScript' -Stdio -HostName OpenCode -HostVersion 1.0.0 -BundledModulesPath '$psesModulesPath' -LogPath '$psesLogsPath' -LogLevel Normal"
            )
            extensions = @('.ps1')
        }
    }
    if ($lspConfig.Count -gt 0) {
        $configObj | Add-Member -NotePropertyName 'lsp' -NotePropertyValue $lspConfig -Force
    }
    return ($configObj | ConvertTo-Json -Depth 10)
}

The resulting config:

{
  $schema: https://opencode.ai/config.json,
  lsp: {
    bicep: {
      extensions: [
        .bicep,
        .bicepparam
      ],
      command: [
        dotnet,
        /Users/pasi/.opencode-lsp/bicep-langserver/Bicep.LangServer.dll
      ]
    }
  }
}

Option 2: Reuse the VS Code extension's language server

If you already have Bicep extension installed in VS Code/Insiders, you can point OpenCode to that DLL instead of installing another copy.

Example locations on macOS:

  • /Users/pasi/.vscode-insiders/extensions/ms-azuretools.vscode-bicep-/bicepLanguageServer/Bicep.LangServer.dll
  • /Users/pasi/.vscode/extensions/ms-azuretools.vscode-bicep-/bicepLanguageServer/Bicep.LangServer.dll

Helper script to resolve latest installed extension DLL

function Get-VSCodeBicepLangServerPath {
    [CmdletBinding()]
    param(
        [ValidateSet('insiders', 'stable')]
        [string]$Channel = 'insiders'
    )
    $home = $HOME
    $basePath = if ($Channel -eq 'insiders') {
        Join-Path $home '.vscode-insiders/extensions'
    } else {
        Join-Path $home '.vscode/extensions'
    }
    if (-not (Test-Path $basePath)) {
        return $null
    }
    $candidate = Get-ChildItem -Path $basePath -Directory -Filter 'ms-azuretools.vscode-bicep-*' |
        Sort-Object Name -Descending |
        ForEach-Object {
            Join-Path $_.FullName 'bicepLanguageServer/Bicep.LangServer.dll'
        } |
        Where-Object { Test-Path $_ } |
        Select-Object -First 1
    return $candidate
}

Then use that resolved path in the same lsp.bicep.command array.

The caveat with VS Code paths

The extension folder has a version in its name, so the path changes when the extension updates. That means one of:

  • update opencode.json after extension updates,
  • script the path resolution and regenerate config, maybe updating via opencode plugin?
  • or keep the manual install path for stability.

For quick setup, VS Code reuse is convenient. For long-term predictability, manual install usually wins. I've been sticking with the manual version for now, and updating it now and then myself.

Quick verification

After setting config, run LSP diagnostics against an invalid .bicep file and verify you get Bicep diagnostic codes (for example BCP007).
That confirms both the language-ID mapping and language server wiring are working.

https://private-user-images.githubusercontent.com/22717844/556576696-d205b403-c0b5-4678-afa3-366927a3d11e.png?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NzIzNzE5NDUsIm5iZiI6MTc3MjM3MTY0NSwicGF0aCI6Ii8yMjcxNzg0NC81NTY1NzY2OTYtZDIwNWI0MDMtYzBiNS00Njc4LWFmYTMtMzY2OTI3YTNkMTFlLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNjAzMDElMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjYwMzAxVDEzMjcyNVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTczNWQzYWI1MjljODllMDRjNWY3MDY0MmNhMzkxZGE0MDcyNTE5MjYyZGU3MWNjNzI0ZTI0OTk2NjIwZTJlOWQmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.MdETdcGCzFYbyKKIBdQLujDlqzaSwC4RTVG2fHTkcto