vue-ts-lsp
v0.3.2
Published
Unified Vue + TypeScript LSP proxy for Claude Code
Maintainers
Readme
vue-ts-lsp
A unified LSP proxy that gives Claude Code full TypeScript intelligence across .ts files and .vue files — including <template> expressions.
Why
Claude Code's LSP integration supports go-to-definition, hover, find-references, document symbols, workspace symbols, call hierarchy, and diagnostics. Getting all of these to work across .ts and .vue files requires two language servers:
- vtsls — TypeScript language server (handles
.ts,.js, and.vue<script>blocks via@vue/typescript-plugin) - vue-language-server (v3) — handles
.vue<template>expressions, providing type-aware diagnostics and navigation
The problem: in Vue Language Tools v3, the LSP client must forward tsserver/request notifications from vue-language-server to vtsls and return responses. Claude Code's plugin system can't do this — it just spawns a stdio process.
This proxy fills the gap by acting as both the LSP server (to Claude Code) and the LSP client (to both child servers), implementing the v3 request forwarding protocol internally.
What you get
| Scenario | Without Proxy | With Proxy |
| ----------------------------------------------- | -------------- | ---------- |
| Go-to-definition in .ts | ✅ | ✅ |
| Go-to-definition into .vue component | ⚠️ file only | ✅ symbol |
| Hover on {{ ref.value }} in <template> | ❌ | ✅ |
| Diagnostics for wrong prop type in <template> | ❌ | ✅ |
| Find references across .ts and .vue | ⚠️ partial | ✅ |
| Symbols in .vue file | ⚠️ script only | ✅ |
Installation
1. Install the language server
npm install -g vue-ts-lspThis installs the vue-ts-lsp binary along with vtsls, @vue/language-server, and typescript as bundled dependencies.
Verify the binary is in your PATH:
which vue-ts-lsp2. Install the Claude Code plugin
The plugin tells Claude Code to use vue-ts-lsp for Vue and TypeScript files.
From a marketplace:
claude plugin install vue-ts-lsp@vue-ts-lspManually — create an .lsp.json file in a plugin directory loaded via claude --plugin-dir (Claude Code discovers LSP servers through plugins only — an .lsp.json at your project root is not picked up):
{
"vue-ts-lsp": {
"command": "vue-ts-lsp",
"extensionToLanguage": {
".vue": "vue",
".ts": "typescript",
".tsx": "typescriptreact",
".js": "javascript",
".jsx": "javascriptreact"
},
"startupTimeout": 30000
}
}Claude Code also accepts these optional fields in .lsp.json (as of Claude Code 2.1.226; behavior is version-dependent):
| Field | Type | Default | Description |
| ----------------------- | ---------- | ------- | ------------------------------------------------------------------------------------------- |
| args | string[] | [] | Extra CLI arguments — e.g. ["--log-level=debug"] to enable proxy debug logging |
| diagnostics | boolean | true | Set false to keep code navigation but suppress automatic diagnostic injection after edits |
| maxRestarts | number | 3 | Crash-recovery attempts for the proxy process before Claude Code gives up |
| restartOnCrash | boolean | — | Client-level restart of the whole proxy on crash (implemented since ~2.1.2xx) |
| shutdownTimeout | number | — | How long Claude Code waits for a graceful shutdown before killing the server |
| env | object | — | Extra environment variables for the server process |
| initializationOptions | object | — | Passed through in the LSP initialize request |
| settings | object | — | Pushed to the server via workspace/didChangeConfiguration |
Note: On Claude Code 2.1.204 and earlier,
restartOnCrashandshutdownTimeoutwere recognized but rejected ("not yet implemented") and would prevent the server from starting. Both work on 2.1.226+. Client-level restart is complementary to the proxy's own crash recovery for its child servers — the proxy also self-heals the client's no-didOpen-replay behavior after a restart.
Note: The proxy itself also handles
.mts/.cts/.mjs/.cjsfiles; the plugin'sextensionToLanguagemap advertises only the common five extensions, so files with the rarer extensions get LSP features when reached through cross-file navigation but do not activate the server on open. Extend the map in a manual install if you need that.
Note: This proxy replaces separate vtsls and vue-volar plugins. Do not enable them simultaneously: Claude Code assigns each file extension to the first plugin that claims it, so a competing TypeScript plugin (e.g.
typescript-lsp) can capture.ts/.tsxfiles while this proxy serves.vue— splitting the two sides of cross-file navigation across unrelated servers.
3. Verify
Start Claude Code (claude --debug for verbose output) and open a .vue or .ts file. You should see diagnostics, hover information, and go-to-definition working.
Architecture
┌─────────────┐ stdio ┌──────────────────────────────┐
│ Claude Code │◄───────────────►│ vue-ts-lsp (proxy) │
│ (LSP client)│ │ │
└─────────────┘ │ ┌──────────┐ ┌───────────┐ │
│ │ vtsls │ │ vue_ls │ │
│ │ (child) │ │ (child) │ │
│ └────▲─────┘ └─────▲─────┘ │
│ │ │ │
│ │ tsserver/ │ │
│ │◄─request ────┘ │
│ │──response──► │
└──────────────────────────────┘Request routing
The proxy currently exposes the Claude Code request surface implemented in this repository. It does not currently advertise completion, rename, or formatting support.
| File Type | Supported Operation | Routed To |
| ---------------------------- | ------------------------------------------------------------------------------- | ------------------------ |
| .ts, .js, .tsx, .jsx | definition, implementation, hover, references, document symbols, call hierarchy | vtsls |
| .vue | definition, implementation, hover, references, call hierarchy | vtsls |
| .vue | document symbols | vue_ls |
| .vue | diagnostics | Merged from both servers |
| Any | workspace/symbol | vtsls |
For architecture details, see AGENTS.md.
How library definitions work
Go-to-definition on a symbol from node_modules returns a URI under ~/.cache/vue-ts-lsp/definition-mirrors/ rather than the raw node_modules path: the proxy copies the target declaration file into that cache and rewrites the location. This keeps results stable and readable for Claude Code. The cache is safe to delete at any time (it is rebuilt on demand), and its root can be moved with the VUE_TS_LSP_DEFINITION_MIRROR_ROOT environment variable.
TypeScript versions
vtsls ships with its own bundled TypeScript, but the proxy sets autoUseWorkspaceTsdk: true, so when your project has typescript installed, that version's tsserver does the analysis — your diagnostics match your project's compiler. The proxy's own internal AST helpers (used by fallback scans) run on its bundled TypeScript 6, independent of your workspace version. Note: the proxy pins typescript@^6.0.3 deliberately — TypeScript 7 has no programmatic API for vtsls or @vue/typescript-plugin to use.
CLI usage
vue-ts-lsp [--log-level=<error|warn|info|debug>]| Flag | Description |
| ------------- | ------------------------------------------------------------- |
| --log-level | Logging verbosity: error (default), warn, info, debug |
All logging goes to stderr and to ~/.cache/vue-ts-lsp/vue-ts-lsp.log. stdout is reserved for the JSON-RPC protocol.
Project config
The proxy optionally reads project-local settings from .claude/vue-ts-lsp.json in the workspace root.
{
"ignoreDirectories": ["vendor", "public"],
"logLevel": "debug"
}| Option | Type | Default | Description |
| ------------------- | -------------------------------------- | ------- | ---------------------------------------------------------------------------------------------- |
| ignoreDirectories | string[] | [] | Workspace-root-relative directories to skip when the proxy runs local workspace fallback scans |
| logLevel | error | warn | info | debug | unset | Sets the proxy log level without needing to pass a CLI flag |
Notes:
- The config file is optional.
ignoreDirectoriesdefaults to an empty list.ignoreDirectoriesonly affects proxy-managed workspace fallback scans; it does not change your TypeScript project configuration.--log-levelon the CLI overrideslogLevelfrom.claude/vue-ts-lsp.json.
Known limitations
- Forwarded diagnostics preserve the downstream document version when available, and the proxy prevents known older versions and cached pre-edit results from being merged into newer diagnostics. vtsls does not report a version, so its publishes use the document version at arrival; a late unversioned result can still be attributed to the current edit. A diagnostic can also land one turn late if the type check outruns Claude Code's read of it.
- Cross-file diagnostics after changing exported function signatures, store APIs, or narrowed unions are improved but not guaranteed to appear immediately in every dependent file, especially in large mixed workspaces.
- If your workflow depends on catching downstream breakage after API changes, do not treat "no diagnostic appeared" as proof that all callers are valid. Prefer
findReferences, a targeted typecheck, or project-specific verification hooks for high-confidence changes. - Go-to-definition can be sensitive to the exact character position in complex TypeScript expressions such as
keyof typeof .... If a lookup misses unexpectedly, retry with the cursor placed directly on the referenced symbol. - Template definition support is strongest for standard bound expressions such as
:prop="value". Member expressions insidev-model(e.g.v-model="charge.name") resolve for hover and definition (covered by the smoke suite); what remains unreliable is shorthand boolean props and resolving thev-modeldirective itself to the child component's prop declaration. textDocument/implementationfollows downstream TypeScript and Vue language-server behavior. Interfaces or structural types backed by object literals may legitimately return no implementation result.- Project hooks that rewrite files after
EditorWritetool calls, such as running Prettier, can add an extra mutation step and make diagnostic timing harder to reason about. - After reverting a cross-file type break, dependent-file diagnostics may occasionally clear a beat later than the edit that fixed them.
ignoreDirectoriescan reduce noisy fallback scanning in projects with generated, vendored, or public asset trees, but it only affects proxy fallback scans, not TypeScript's own project graph.- Workspace reference fallbacks match identifier spelling and can include unrelated symbols with the same name. Their alias resolution reads root
tsconfig.jsonandjsconfig.json, including inherited settings, but does not discover separate configurations through project references.
Troubleshooting
| Issue | Solution |
| ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Server not starting | Verify which vue-ts-lsp returns a path. Check claude --debug output for initialization errors. |
| No diagnostics or hover | Ensure your project has a tsconfig.json and typescript is installed in the project. |
| Slow startup | The first initialization can take time on large projects. Increase startupTimeout in .lsp.json if needed. |
| Server crashes repeatedly | Check ~/.cache/vue-ts-lsp/vue-ts-lsp.log for errors. The proxy auto-restarts up to 3 times per 30 seconds. |
| Debug logging | Set --log-level=debug in the .lsp.json args, or set "logLevel": "debug" in .claude/vue-ts-lsp.json, to see full JSON-RPC payloads in the log file. |
| tsserver-level debugging | Set the VUE_TS_LSP_TSSERVER_LOG environment variable (terse, normal, or verbose) to make tsserver write its own log files. Off by default. |
| Cross-file diagnostics missing in noisy workspaces | Add project-specific ignores in .claude/vue-ts-lsp.json, for example "ignoreDirectories": ["vendor", "public"], to keep fallback scans focused on real source files. |
| Go-to-definition misses on a valid TS symbol | Retry with the cursor placed directly on the symbol token. Complex type expressions can be character-position-sensitive. |
| Template prop or v-model definition does not resolve | Prefer testing the bound script symbol or a standard :prop="..." binding first. Shorthand boolean attrs and directive targets are less reliable today. |
| Go-to-implementation returns no result for an interface | This can be expected for structural typing or object-literal implementations. Use references or definitions instead if you need to trace usage sites. |
Contributing
See CONTRIBUTING.md for development guidelines and how to submit changes.
git clone https://github.com/tylersatre/vue-ts-lsp.git
cd vue-ts-lsp
npm install| Command | Description |
| ------------------------------- | ------------------------------------------------------------------------------------- |
| npm run build | Build with tsup (src/index.ts → dist/index.js) |
| npm run format | Format the repository with Prettier |
| npm run format:check | Check repository formatting with Prettier |
| npm run typecheck | Type-check source and tests |
| npm test | Run unit, integration, and smoke tests (vitest; smoke self-skips without the fixture) |
| npm run install:smoke-fixture | Install smoke test fixture dependencies |
Note: The repo includes
.claude/settings.jsonwhich enables thetypescript-lspplugin for Claude Code. This gives contributors TypeScript LSP support while working on this project. If it conflicts with your setup, you can override it in.claude/settings.local.json.
Releasing
This project uses changesets for versioning and npm publishing. Do not create GitHub releases manually — the CI handles it.
1. Add a changeset with your PR
When submitting a PR with user-facing changes, add a changeset:
npx changesetYou'll be prompted to select the version bump type (major, minor, or patch) and a summary. Commit the generated .changeset/*.md file with your PR.
2. Merge to main
When the PR merges, the Release workflow runs automatically and does one of two things:
- Pending changesets exist: the
changesets/actionopens (or updates) a "Version Packages" PR that bumpspackage.json, updatesCHANGELOG.md, and removes consumed changeset files. - No pending changesets (i.e., the "Version Packages" PR was just merged): the action runs
npm run release, which builds the package and publishes to npm, then creates a GitHub release.
