From 5b116750cf8f3780eb860f98c718bac742fc4032 Mon Sep 17 00:00:00 2001 From: BoykoAlex Date: Wed, 15 Jul 2026 13:50:31 -0700 Subject: [PATCH] Remove LSP contributions leftovers from Boot Claude plugin Signed-off-by: BoykoAlex --- claude-plugins/spring-tools/README.md | 32 +++++------ claude-plugins/spring-tools/common.js | 16 ------ claude-plugins/spring-tools/launcher.js | 31 +--------- claude-plugins/spring-tools/proxy.js | 57 ------------------- .../starter/LanguageServerRunnerAutoConf.java | 2 + 5 files changed, 20 insertions(+), 118 deletions(-) delete mode 100644 claude-plugins/spring-tools/common.js delete mode 100644 claude-plugins/spring-tools/proxy.js diff --git a/claude-plugins/spring-tools/README.md b/claude-plugins/spring-tools/README.md index c702e3fde1..88bde4ee53 100644 --- a/claude-plugins/spring-tools/README.md +++ b/claude-plugins/spring-tools/README.md @@ -1,6 +1,6 @@ # Spring Tools Language Server — Claude Code Plugin -A [Claude Code](https://code.claude.com) plugin that contributes the Spring Tools Language Server, providing real-time diagnostics, completions, and navigation for Spring Boot projects. +A [Claude Code](https://code.claude.com) plugin that contributes the Spring Tools Language Server, exposing Spring Boot diagnostics, bean/request-mapping lookups, and other project insights to Claude Code via MCP tools. Unlike the VS Code extension, this plugin uses the **standalone** variant of the language server which operates **without** JDT Language Server. Project classpath is computed directly via Maven and Gradle tooling; type indexing uses Jandex. @@ -48,19 +48,19 @@ claude plugin marketplace update claude plugin update spring-tools ``` -### 4. Testing the LSP Plugin +### 4. Testing the Plugin -To verify that the Spring Boot Language Server is correctly booting up and providing diagnostics to Claude Code, you must run Claude Code **interactively** (don't use the `-p` single-shot flag, as it will kill the CLI before the LSP finishes initializing). +To verify that the Spring Boot Language Server is correctly booting up and serving MCP tools to Claude Code, you must run Claude Code **interactively** (don't use the `-p` single-shot flag, as it will kill the CLI before the language server finishes initializing). Open a Spring Boot project and start Claude Code: ```bash claude ``` -Then, ask Claude a test query to verify the LSP integration. For example: -> "Open CoffeeController.java and tell me what the Spring Boot LSP says about the @GetMapping version attribute." +Then, ask Claude a test query to verify the MCP integration. For example: +> "Check the project diagnostics for CoffeeController.java." -Claude will wait for the LSP to initialize, read the file, and then summarize the exact Spring Boot warnings and quick fixes provided by the Language Server. +Claude will wait for the language server to initialize, call the `getProjectDiagnostics` MCP tool, and summarize the exact Spring Boot warnings and quick fixes it reports. ### 5. Local Testing @@ -140,21 +140,22 @@ Settings are applied once at startup. You must restart the language server (and ## What the language server provides -- **Diagnostics** — Spring-specific warnings and quick fixes (missing annotations, incorrect bean wiring, etc.) -- **Completions** — Spring Boot properties (`application.properties` / `application.yml`), annotation values, bean references -- **Navigation** — Go to definition for Spring beans, `@Value` expressions, request mappings -- **Inlay hints** — Cron expressions, JPA/JPQL queries, SpEL expressions +Via MCP tools: + +- **Diagnostics** — Spring-specific warnings and quick fixes (missing annotations, incorrect bean wiring, etc.), including version validation results +- **Project insight** — bean, component, and request-mapping lookups; resolved project classpath + +Via hooks (`hooks/hooks.json`), the plugin also tracks file and project changes on disk to keep its internal index up to date, and exposes a command to refresh the index manually. ## Plugin structure ``` spring-tools/ ├── .claude-plugin/ -│ └── plugin.json # Plugin manifest (includes MCP and LSP server configs) -├── proxy.js # Node.js script to pipe stdio to the Java LSP socket +│ └── plugin.json # Plugin manifest (MCP server config) ├── launcher.js # Node.js script that downloads the JAR (if missing) and starts Java ├── install.js # Node.js script that downloads the JAR -├── common.js # Shared Node.js logic +├── hooks/ # Hooks that notify the language server of file/project changes ├── language-server/ # Populated by install.js on first run (gitignored) │ └── spring-boot-language-server-standalone-exec.jar ├── skills/ # Claude Code skills @@ -166,7 +167,4 @@ spring-tools/ ## How it works -To eliminate race conditions and avoid booting multiple heavy Java processes, this plugin configures Claude Code to share a single JVM for both MCP and LSP: - -1. **MCP starts the server:** Claude Code parses the MCP configuration in `plugin.json` at startup. This triggers `launcher.js`, which checks if the heavy Java JAR is downloaded. If not, it executes `install.js` to download it from Spring's CDN. Then it boots the standalone Spring Tools Language Server, instructing it to expose its MCP tools over `stdio` and its LSP over a local TCP socket (randomly allocated port). -2. **LSP connects via proxy:** When you open a relevant file (e.g. `.java`, `.properties`), Claude Code parses the LSP configuration in `plugin.json` and starts `proxy.js` as its "LSP process". This lightweight Node.js script simply forwards Claude Code's standard input/output streams to the already-running Java process on the dynamically allocated port, avoiding the need to spawn a second JVM. +Claude Code parses the MCP configuration in `plugin.json` at startup. This triggers `launcher.js`, which checks if the heavy Java JAR is downloaded. If not, it executes `install.js` to download it from Spring's CDN. Then it boots the standalone Spring Tools Language Server, instructing it to expose its MCP tools over `stdio` (the language server's own LSP socket transport is disabled, since nothing in this plugin connects to it). diff --git a/claude-plugins/spring-tools/common.js b/claude-plugins/spring-tools/common.js deleted file mode 100644 index 68483fdc65..0000000000 --- a/claude-plugins/spring-tools/common.js +++ /dev/null @@ -1,16 +0,0 @@ -const path = require('path'); -const os = require('os'); -const fs = require('fs'); -const crypto = require('crypto'); - -// Since we can't get CLAUDE_SESSION_ID from args or env reliably across all contexts, -// we will hash the current working directory to create a unique port file per project. -// This ensures multiple Claude Code instances in different projects won't collide. -const projectDir = process.cwd(); -const projectHash = crypto.createHash('md5').update(projectDir).digest('hex').substring(0, 8); - -const portFile = path.join(os.tmpdir(), `spring-tools-lsp-port-${projectHash}`); - -module.exports = { - portFile, -}; diff --git a/claude-plugins/spring-tools/launcher.js b/claude-plugins/spring-tools/launcher.js index 5ea1c9bfbf..a96aa113fc 100644 --- a/claude-plugins/spring-tools/launcher.js +++ b/claude-plugins/spring-tools/launcher.js @@ -1,26 +1,10 @@ const fs = require('fs'); const path = require('path'); const { spawn, spawnSync } = require('child_process'); -const net = require('net'); -const common = require('./common'); const jarDir = path.join(__dirname, 'language-server'); const jarPath = path.join(jarDir, 'spring-boot-language-server-standalone-exec.jar'); -function getFreePort() { - return new Promise((resolve, reject) => { - const srv = net.createServer(); - srv.listen(0, () => { - const port = srv.address().port; - srv.close((err) => { - if (err) reject(err); - else resolve(port); - }); - }); - srv.on('error', reject); - }); -} - async function start() { // 1. Download the JAR if it doesn't exist if (!fs.existsSync(jarPath)) { @@ -33,11 +17,8 @@ async function start() { } } - // 2. Find a free port and save it for the proxy - const port = await getFreePort(); - fs.writeFileSync(common.portFile, port.toString(), 'utf8'); - - // 3. Launch the Java process + // 2. Launch the Java process. The plugin only exposes MCP tools over stdio - + // the LSP socket transport is disabled since nothing connects to it. const javaArgs = [ "-Xmx1024m", "-Djdk.util.zip.disableZip64ExtraFieldValidation=true", @@ -46,8 +27,7 @@ async function start() { `-Dlogging.file.name=${path.join(__dirname, 'boot-ls.log')}`, "-Dlogging.level.root=INFO", "-Dspring.ai.mcp.server.stdio=true", - "-Dlanguageserver.standalone=true", - `-Dlanguageserver.standalone-port=${port}`, + "-Dlanguageserver.enabled=false", `-Dspring.boot.ls.project.dir=${process.cwd()}`, "-jar", jarPath @@ -63,21 +43,16 @@ async function start() { if (!child.killed) { child.kill('SIGTERM'); } - if (fs.existsSync(common.portFile)) { - fs.unlinkSync(common.portFile); - } process.exit(0); }); }); child.on('error', (err) => { console.error('Failed to start Java process:', err); - if (fs.existsSync(common.portFile)) fs.unlinkSync(common.portFile); process.exit(1); }); child.on('close', (code) => { - if (fs.existsSync(common.portFile)) fs.unlinkSync(common.portFile); process.exit(code); }); } diff --git a/claude-plugins/spring-tools/proxy.js b/claude-plugins/spring-tools/proxy.js deleted file mode 100644 index e8934bcdb6..0000000000 --- a/claude-plugins/spring-tools/proxy.js +++ /dev/null @@ -1,57 +0,0 @@ -const net = require('net'); -const fs = require('fs'); -const common = require('./common'); - -function waitForPort(retries = 60) { - return new Promise((resolve, reject) => { - const check = (attemptsLeft) => { - if (fs.existsSync(common.portFile)) { - const portStr = fs.readFileSync(common.portFile, 'utf8'); - const port = parseInt(portStr, 10); - if (!isNaN(port)) { - resolve(port); - return; - } - } - if (attemptsLeft > 0) { - setTimeout(() => check(attemptsLeft - 1), 500); - } else { - reject(new Error("Timeout waiting for .lsp-port file to be created by launcher.js")); - } - }; - check(retries); - }); -} - -async function start() { - try { - const port = await waitForPort(); - - function connect(retries = 30) { - const client = net.createConnection({ port: port, host: '127.0.0.1' }, () => { - process.stdin.pipe(client); - client.pipe(process.stdout); - }); - - client.on('error', (err) => { - if (retries > 0) { - setTimeout(() => connect(retries - 1), 1000); - } else { - console.error(`Failed to connect to LSP socket on port ${port}:`, err); - process.exit(1); - } - }); - - client.on('close', () => { - process.exit(0); - }); - } - - connect(); - } catch (err) { - console.error(err.message); - process.exit(1); - } -} - -start(); \ No newline at end of file diff --git a/headless-services/commons/language-server-starter/src/main/java/org/springframework/ide/vscode/languageserver/starter/LanguageServerRunnerAutoConf.java b/headless-services/commons/language-server-starter/src/main/java/org/springframework/ide/vscode/languageserver/starter/LanguageServerRunnerAutoConf.java index fb0c3a6249..29275c6dbb 100644 --- a/headless-services/commons/language-server-starter/src/main/java/org/springframework/ide/vscode/languageserver/starter/LanguageServerRunnerAutoConf.java +++ b/headless-services/commons/language-server-starter/src/main/java/org/springframework/ide/vscode/languageserver/starter/LanguageServerRunnerAutoConf.java @@ -17,6 +17,7 @@ import org.eclipse.lsp4j.jsonrpc.MessageConsumer; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.ide.vscode.commons.languageserver.LanguageServerRunner; import org.springframework.ide.vscode.commons.languageserver.config.LanguageServerProperties; @@ -38,6 +39,7 @@ Function messageConsumer(SimpleLanguageServer } @ConditionalOnMissingClass("org.springframework.ide.vscode.languageserver.testharness.LanguageServerHarness") + @ConditionalOnProperty(name = "languageserver.enabled", matchIfMissing = true) @Bean LanguageServerRunner serverApp( LanguageServerProperties properties,