AI DevelopmentSep 17, 202611 min read

How to build a Language Server Protocol (LSP) plugin for Claude Code

Roger Winter

Content Marketing Manager

Language servers give editors structured, real-time feedback: diagnostics, hover docs, autocomplete, and other guidance that would otherwise surface later. Language servers already exist for many of the languages and tools developers use every day, but Claude Code doesn’t automatically receive their feedback.

Claude Code writes syntactically valid code most of the time, but it can still miss the edit-time guidance a language server would surface: best-practice recommendations, deprecation warnings, and occasional errors that otherwise won’t appear until later.

In this post, we’ll show how to wrap an existing language server as a Claude Code plugin. We’ll use CircleCI’s open-source YAML language server as an example, then walk through the pieces you can reuse with other language servers.

Creating custom Claude Code plugins for LSPs

If a language server already exists for what you’re working on, you can hand Claude that same signal directly, inside its edit loop, by wrapping the LSP as a Claude Code plugin.

We did this for CircleCI config files using the official CircleCI language server. CircleCI open-sources the language server behind its VS Code extension, so instead of building bespoke validation logic, we wrapped that existing server as a plugin.

With the plugin, Claude now gets the same diagnostics and hover guidance a human editing .circleci/config.yml in VS Code would see.

Below, we walk through how we built it, and benchmark how much it improves Claude’s output compared to editing blind.

New to pairing a language server with Claude Code? Start with our prior post, Why you should use Language Server Protocol (LSP) with Claude Code, for the general case before diving into this implementation.

Note: This plugin is an example, not an officially supported CircleCI product. The CircleCI YAML language server it wraps is open-source and maintained by CircleCI, but the Claude Code plugin shown here was built for exploration and to demonstrate the approach. It isn’t officially supported, comes with no warranty, and may break as Claude Code or the language server evolve. Use it at your own discretion and risk.

How the Claude Code CircleCI LSP plugin works

The CircleCI config LSP plugin wraps CircleCI’s open-source YAML language server and scopes it to .circleci/config.yml. As Claude edits the file, it receives config diagnostics and best-practice guidance from the language server.

Claude Code connects to language servers through its Code intelligence plugins. Our example plugin connects Claude Code to the CircleCI language server for CircleCI config files. In the benchmark below, access to the language server’s guidance led Claude to make improvements such as adding store_test_results and keeping orbs on current versions.

CircleCI YAML LSP for Claude Code architecture diagram

The launcher is a bash script that gets a trustworthy language server binary onto the machine. It never vendors the binary. On first run it downloads the official prebuilt server from CircleCI’s GitHub Releases, verifies it against a pinned SHA-256, and caches it. Any mismatch aborts the run.

The scoping proxy is the interesting part. Claude Code routes a language server by file extension alone, so mapping .yml/.yaml would point every YAML file at a CircleCI-only server. The proxy sits in the JSON-RPC stream and forwards only .circleci/config.yml to the server, leaving your docker-compose.yml and Helm charts untouched.

The schema hover provides inline documentation for CircleCI config. When Claude requests information about a config key, the proxy looks it up in CircleCI’s pinned config schema and returns the relevant description.

Run the example plugin in three commands

You can run the example yourself. From a Claude Code session, use three commands:

/plugin marketplace add CIRCLECI-GWP/circleci-yaml-lsp-for-claude
/plugin install circleci-yaml-lsp@circleci-lsp
/reload-plugins

The plugin lives on GitHub at CIRCLECI-GWP/circleci-yaml-lsp-for-claude.

One thing to expect: the language server is push-based, so diagnostics appear when Claude edits the config, not when it merely reads one. Ask Claude to change .circleci/config.yml and errors, warnings, and hints surface in its loop. The hints are CircleCI’s own edit-time usage guidance.

If your config pulls in private orbs or contexts, set CIRCLECI_YAML_LSP_TOKEN so the server can resolve them. Without it, public orbs and the schema still work.

The plugin is not an official CircleCI product. The language server it wraps is the official circleci-yaml-language-server.

Creating a Claude Code LSP plugin from scratch

Most of what follows is the recipe for any Claude Code LSP plugin, not just this one. If you want to wrap a language server of your own, the first three steps are the same; only the last is specific to CircleCI’s server.

The plugin is open source under the MIT license and documented, so the snippets below are trimmed to the shape of each idea. The full files are on GitHub, and docs/DESIGN.md walks the internals. The links below point at the real code.

Wire the language server in

A Claude Code LSP plugin is a plugin.json whose lspServers field points at a .lsp.json that maps a server name to a command and an extensionToLanguage table. The plugins reference documents the full schema. Here’s ours:

{
  "circleci-yaml": {
    "command": "${CLAUDE_PLUGIN_ROOT}/bin/circleci-yaml-lsp",
    "args": [],
    "transport": "stdio",
    "extensionToLanguage": { ".yml": "yaml", ".yaml": "yaml" },
    "startupTimeout": 180000
  }
}

${CLAUDE_PLUGIN_ROOT} resolves against the installed copy, and the 180-second startupTimeout covers the first-run binary download.

Get the binary on the machine, safely

If your language server isn’t already on the user’s PATH, the plugin has to fetch it, and anything that downloads and runs a binary should prove it got the right one.

Our launcher, bin/circleci-yaml-lsp, does an atomic, fail-closed download: it checks the file’s byte size, then its SHA-256 against pins baked into the script, and only moves it into a version-keyed cache once both pass. With no sha256 tool present, it refuses to run rather than execute an unverified binary. That pattern is reusable as-is for any server distributed as a release binary.

Scope the server to the files it understands

This is the most reusable idea here, and the one genuinely tricky part. Claude Code routes to a language server by file extension alone, with no path or glob filter, and many domain-specific servers assume every document they receive is theirs to validate. CircleCI’s does.

Left unscoped, it feeds the model wrong diagnostics on unrelated YAML, which is worse than no LSP at all. The fix is a small Node stdio proxy, bin/lsp-proxy.mjs, that sits in the JSON-RPC stream and drops document-sync notifications for any file that doesn’t match one regex:

/(^|\/)\.circleci\/([^/]*_)?config\.ya?ml$/i

The regex lets config.yml, config.yaml, and _config.yml through while excluding test-suites.yml, subdirectory files, and unrelated YAML. The proxy forwards the rest of the stream unchanged and drops only out-of-scope sync notifications and diagnostics.

The same approach works for other config- or dialect-specific language servers that support a broad file extension but should only receive a narrower set of files.

Those three steps are enough for a well-behaved server.

A final step specific to the CircleCI LSP is our hover proxy. The proxy answers hover itself (bin/lsp-hover.mjs), from a table generated out of CircleCI’s config schema. That table is the one piece that can go stale as CircleCI evolves its schema, so we created a CircleCI pipeline to keep it up-to-date.

Keeping the plugin current with CircleCI

The hover table is generated from a specific version of CircleCI’s schema, and the launcher pins a specific server binary by SHA-256. Both have to move when CircleCI ships a new language-server release.

Rather than track those releases by hand, the example project uses its own CircleCI pipeline to keep them in sync. You can see the full pipeline in the project’s .circleci/config.yml file.

The config separates the project’s normal CI from a maintenance workflow that handles upstream updates:

workflows:
  ci:
    jobs:
      - lint
      - test

  maintenance:
    when: << pipeline.parameters.run-maintenance >>
    jobs:
      - upstream-update:
          context:
            - gh-bot

CircleCI maintenance pipeline keeping LSP plugin hover definitions up to date

When the maintenance job runs, it compares the upstream language server’s latest stable release to the version pinned in the launcher. If there’s nothing new, or a fresh release’s binaries and schema haven’t finished uploading yet, the job exits cleanly and a later run retries.

If a new release is ready, the job bumps the pinned version, refreshes the SHA-256 and size pins for all four platform binaries, regenerates the hover-doc table from the new schema, bumps the plugin version, and runs the tests.

If everything passes, it opens a pull request with the diff. It never merges to main; a person reviews the bump and merges.

You can explore the full implementation in the circleci-yaml-lsp-for-claude repository, including the pipeline, update scripts, proxy, and plugin source.

Benchmarking the plugin’s performance

We ran a small, directional benchmark to see whether the plugin changes what Claude produces.

Six common config edits (a deploy workflow, an orb command, repairing a broken config, a matrix job, caching, an executor) were each attempted by headless Claude across four arms: a no-plugin baseline, a plain self-review steer, and two plugin arms.

Each ran in a fresh throwaway project, on both Opus 4.8 and Sonnet 5, with the sturdier tasks repeated, for 84 runs total. Every output config was judged by circleci config validate, CircleCI’s server-side compiler, which each arm was blocked from calling mid-task.

Finding 1: no correctness gap

All 84 output configs compiled cleanly in every arm, on both models. In this benchmark, both models produced valid CircleCI config with or without the plugin.

They also avoided the compiler-level mistakes the tasks were designed to invite, including hallucinated orb commands, dangling job references, and matrix type errors.

Finding 2: the plugin got Claude to improve configs

circleci config validate and a green CI run both stay silent about the CircleCI LSP’s edit-time warnings and hints. Two showed up across these tasks: the store_test_results hint, which asks you to expose your test results in CircleCI, and stale-orb-version notices.

When Claude was told to act on the language server’s guidance, it made those improvements. When it was told to fix errors only, or ran without the plugin, it generally skipped them.

With the plugin acting on its guidance, Claude added store_test_results in 5 of 16 test-running configs. No plugin-free run added it. It also kept the Node orb on a current version in all 6 plugin-guided orb configs, while plugin-free runs left a stale version in 7 of 8.

Every config compiled clean either way, so the difference was not compiler-level correctness. The LSP helped Claude apply best-practice guidance the compiler does not enforce.

Benchmarking data for the Claude Code CircleCI YAML LSP plugin

The instruction mattered as much as the plugin. Installing it but telling Claude to fix only errors produced no additional improvements. The gains appeared when Claude was told to act on the language server’s guidance.

Setup Added store_test_results Kept the orb current
No plugin 0 of 24 test configs 1 of 8 orb configs
Plugin, told to fix errors only 0 of 16 2 of 6
Plugin, told to act on its guidance 5 of 16 6 of 6

Counts of configs Claude actively improved. Every config compiled clean in every case; these are best-practice changes the compiler never required.

We saw the same pattern while building the plugin. Its own .circleci/config.yml compiled clean, but the language server suggested adding store_test_results so test results would appear in CircleCI. After we added a JUnit report and the corresponding step, the config reached zero diagnostics.

Separately, the lint job’s shellcheck caught a real shell issue in the launcher.

Claude was already capable of writing correct configs, but access to the LSP’s guidance made it more likely to follow CircleCI best practices.

Build your own Claude Code LSP plugin

The example plugin shows how to bring a language server’s edit-time feedback into Claude Code. In our benchmark, the models already handled compiler-level correctness well, but access to the LSP helped them apply best practices the compiler does not enforce.

The same implementation pattern works beyond CircleCI. For many domain-specific language servers, the core pieces are a verified way to launch the server, a way to scope it to the files it understands, and a proxy for any integration-specific behavior Claude Code does not support directly.

The full circleci-yaml-lsp-for-claude example is open source, so you can inspect the implementation, review the CircleCI pipeline that maintains it, or adapt the same pattern for another language server.

Edit-time feedback covers one part of the development loop. CI handles the broader validation available once code is pushed, including tests, integration checks, security policies, and the rest of the delivery workflow.

Sign up for a free CircleCI account and start building today.