Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 0 additions & 16 deletions claude-plugins/spring-tools/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,5 @@
"${CLAUDE_PLUGIN_ROOT}/launcher.js"
]
}
},
"lspServers": {
"spring-tools-lsp": {
"command": "node",
"args": [
"${CLAUDE_PLUGIN_ROOT}/proxy.js"
],
"extensionToLanguage": {
".java": "java",
".properties": "spring-boot-properties",
".yml": "spring-boot-properties-yaml",
".yaml": "spring-boot-properties-yaml",
".xml": "xml",
".factories": "spring-factories"
}
}
}
}
53 changes: 53 additions & 0 deletions claude-plugins/spring-tools/hooks/hooks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|StrReplace|Edit|Replace",
"hooks": [
{
"type": "mcp_tool",
"server": "plugin:spring-tools:spring-tools-mcp",
"tool": "fileChanged",
"input": {
"filePath": "${tool_input.file_path}"
}
}
]
},
{
"matcher": "Delete",
"hooks": [
{
"type": "mcp_tool",
"server": "plugin:spring-tools:spring-tools-mcp",
"tool": "fileDeleted",
"input": {
"filePath": "${tool_input.file_path}"
}
}
]
},
{
"matcher": "Bash",
"hooks": [
{
"type": "mcp_tool",
"if": "Bash(git *)",
"server": "plugin:spring-tools:spring-tools-mcp",
"tool": "refreshWorkspace",
"input": {}
}
]
},
{
"matcher": "Write|StrReplace|Delete|Edit|Replace",
"hooks": [
{
"type": "command",
"command": "echo \"=== PostToolUse Hook Fired ===\" >> hook-debug.log && cat >> hook-debug.log && echo \"\" >> hook-debug.log"
}
]
}
]
}
}
4 changes: 2 additions & 2 deletions claude-plugins/spring-tools/skills/quickfix/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ You have encountered the Spring Boot diagnostic error code: `$ARGUMENTS[0]`.
The problem is located in the file: `$ARGUMENTS[1]`.
The text range of the problem is: `$ARGUMENTS[2]` (if provided).

MUST DO: If the error code includes a prefix like `errorCode=` or `code=`, strip it out before you continue to use it anywhere.

To find the official explanation and potential fixes for this issue, you must read the explanation file located at:
`${CLAUDE_PLUGIN_ROOT}/explanations/$ARGUMENTS[0].md`

(Note: If the error code includes a prefix like `errorCode=`, strip it out before forming the file path).

If the file does not exist, use your general Spring Boot knowledge to fix the issue.

Based on the provided "Explanations" and "Fixes" in that file:
Expand Down
10 changes: 5 additions & 5 deletions claude-plugins/spring-tools/skills/validate/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
---
description: MUST be used whenever a user or agent asks to find problems, check for issues, or validate changes in a Spring Boot project. Uses the spring-tools LSP plugin to check for validation problems in the project's source files.
description: MUST be used whenever a user or agent asks to find problems, check for issues, or validate changes in a Spring Boot project. Uses the spring-tools MCP server to check for validation problems in the project's source files.
allowed-tools: View
---
CRITICAL REQUIREMENT: Whenever a user asks for problems in the project, or an agent is attempting to check if there are problems with the project, or attempts to validate changes to the project, IF the project is a Spring Boot project, then the `spring-tools` LSP plugin MUST be used to check for validation problems in each source file of the project.
CRITICAL REQUIREMENT: Whenever a user asks for problems or validations or diagnsotics or problem markers in the project, or an agent is attempting to check if there are problems with the project, or attempts to validate changes to the project, IF the project is a Spring Boot project, then the `spring-tools` plugin MCP server MUST be used to check for validation problems in each source file of the project.

To perform this validation:

1. Use the `View` tool to open the modified files (or the files you want to check). In Claude Code, opening a file triggers the Language Server Protocol (LSP), and any errors or warnings will automatically appear in your context as diagnostics.
2. Carefully review the returned diagnostics for any diagnostic that originated from the `spring-tools` LSP plugin.
1. Use the `getDiagnostics` tool from the spring tools MCP server to get a list of diagnostics for a specific project. You need to pass in the name of the project. If you are not sure about the exact name, you can invoke `getProjectList` from the spring tools MCP server to get the exact names of the projects in the current workspace.
2. Carefully review the returned diagnostics.
3. Identify the specific error code, file path, and text range for each Spring-related diagnostic (e.g. error code "WEB_SECURITY_CONFIGURER_ADAPTER").
4. For every Spring-specific error code you encounter, you MUST invoke the `/spring-tools:quickfix` skill, passing the error code, the file path, and the text range as arguments, to retrieve the official explanation and fix instructions.
5. Apply the appropriate fix based on the instructions, or ask the user if a choice needs to be made.
6. Also ensure any standard Java compilation or build errors are addressed.
6. Also ensure any standard Java compilation or build errors are addressed.
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
* @param <K> key class
* @param <P> project class
*/
public abstract class AbstractJavaProjectCache<K, P extends IJavaProject> implements JavaProjectCache<K, P> {
public abstract class AbstractJavaProjectCache<K, P extends IJavaProject> implements JavaProjectCache<K, P>, ProjectChangeNotifier {

private static final Logger log = LoggerFactory.getLogger(AbstractJavaProjectCache.class);

Expand Down Expand Up @@ -121,6 +121,13 @@ final protected void notifyProjectDeleted(P project) {
listeners.forEach(l -> l.deleted(project));
}

@Override
public void notifyProjectsChanged() {
for (P project : cache.asMap().values()) {
notifyProjectChanged(project);
}
}

final protected FileObserver getFileObserver() {
return server.getWorkspaceService().getFileObserver();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
* @author Alex Boyko
*
*/
public class CompositeProjectOvserver implements ProjectObserver {
public class CompositeProjectOvserver implements ProjectObserver, ProjectChangeNotifier {

private List<ProjectObserver> observers;

Expand All @@ -41,4 +41,13 @@ public boolean isSupported() {
return observers.stream().map(o -> o.isSupported()).reduce(false, (a, b) -> a || b);
}

@Override
public void notifyProjectsChanged() {
for (ProjectObserver o : observers) {
if (o instanceof ProjectChangeNotifier) {
((ProjectChangeNotifier) o).notifyProjectsChanged();
}
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.springframework.ide.vscode.commons.languageserver.java;

/**
* Interface for notifying that projects have changed.
*/
public interface ProjectChangeNotifier {

void notifyProjectsChanged();

}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
* @author Alex Boyko
*
*/
public class BasicFileObserver implements FileObserver {
public class BasicFileObserver implements FileObserver, FileChangeNotifier {

protected ConcurrentHashMap<String, ImmutablePair<List<PathMatcher>, Consumer<String[]>>> createRegistry = new ConcurrentHashMap<>();
protected ConcurrentHashMap<String, ImmutablePair<List<PathMatcher>, Consumer<String[]>>> deleteRegistry = new ConcurrentHashMap<>();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.springframework.ide.vscode.commons.util;

/**
* Interface for notifying about file system changes.
*
* @author Alex Boyko
*/
public interface FileChangeNotifier {

void notifyFileCreated(String uri);

void notifyFileChanged(String uri);

void notifyFileDeleted(String uri);

}
Original file line number Diff line number Diff line change
Expand Up @@ -255,11 +255,12 @@ public void removeSymbols(IJavaProject project, String docURI) {
log.info("update settings of spring indexer - done");
});

server.doOnInitialized(this::serverInitialized);
// server.doOnInitialized(this::registerFileListeners);
registerFileListeners();
server.onShutdown(this::shutdown);
}

public void serverInitialized() {
public void registerFileListeners() {
List<String> globPattern = Stream.concat(Arrays.stream(springIndexerJava.getFileWatchPatterns()), Arrays.stream(factoriesIndexer.getFileWatchPatterns()))
.collect(Collectors.toList());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public static DocumentDescriptor createFromUri(String docUri) {
try {
File file = new File(new URI(docUri));
long lastModified = file.lastModified();
return new DocumentDescriptor(file.getAbsolutePath(), docUri, lastModified);
return new DocumentDescriptor(file.getAbsolutePath(), UriUtil.toUri(file).toASCIIString(), lastModified);
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ public static record ProjectDiagnostic(
) {}

@Tool(description = """
Returns all current Spring Tools diagnostics (errors, warnings, hints) for a specific project.
Returns all current Spring Tools diagnostics or problems or validations (errors, warnings, infos, hints) for a specific project.
Diagnostics are produced by the Spring Tools language server during indexing and validation.
Each entry identifies the source file, the exact source location, the severity, and the message.
Only diagnostics known to Spring Tools are included; general Java compiler errors are not.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package org.springframework.ide.vscode.boot.mcp;

import java.io.File;
import java.net.URI;

import java.util.Optional;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.ide.vscode.commons.languageserver.java.ProjectChangeNotifier;
import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer;
import org.springframework.ide.vscode.commons.util.FileChangeNotifier;
import org.springframework.stereotype.Component;

/**
* MCP tools for notifying the language server about file changes on disk.
* This bridges the gap when running in MCP-only mode without full LSP file watching.
*/
@Component
//@ConditionalOnBean({FileChangeNotifier.class})
public class FileChangesMcpTools {

private static final Logger logger = LoggerFactory.getLogger(FileChangesMcpTools.class);

private final FileChangeNotifier fileChangeNotifier;
private final Optional<ProjectChangeNotifier> projectChangeNotifier;
private final SimpleLanguageServer server;

public FileChangesMcpTools(FileChangeNotifier fileChangeNotifier, Optional<ProjectChangeNotifier> projectChangeNotifier, SimpleLanguageServer server) {

logger.info("FileChangesMcpTools constructor called");

this.fileChangeNotifier = fileChangeNotifier;
this.projectChangeNotifier = projectChangeNotifier;
this.server = server;
}

@Tool(description = "Notifies the MCP Server that a file has been created or modified on disk so it can update its internal index and diagnostics.")
public void fileChanged(
@ToolParam(description = "Absolute path to the file") String filePath) {

if (filePath == null || filePath.startsWith("$")) {
logger.warn("fileChanged called but could not extract file path. filePath={}", filePath);
return;
}

if (server.getClient() != null) {
logger.info("LSP client connected, skipping fileChanged MCP notification for: {}", filePath);
return;
}

logger.info("MCP fileChanged called for file: {}", filePath);

try {
URI uri = new File(filePath).toURI();
fileChangeNotifier.notifyFileChanged(uri.toASCIIString());
} catch (Exception e) {
logger.error("Failed to notify file changed for: " + filePath, e);
}
}

@Tool(description = "Notifies the MCP Server that a file has been deleted on disk so it can update its internal index and diagnostics.")
public void fileDeleted(
@ToolParam(description = "Absolute path to the file") String filePath) {

if (filePath == null || filePath.startsWith("$")) {
logger.warn("fileDeleted called but could not extract file path. filePath={}", filePath);
return;
}

if (server.getClient() != null) {
logger.info("LSP client connected, skipping fileDeleted MCP notification for: {}", filePath);
return;
}

logger.info("MCP fileDeleted called for file: {}", filePath);

try {
URI uri = new File(filePath).toURI();
fileChangeNotifier.notifyFileDeleted(uri.toASCIIString());
} catch (Exception e) {
logger.error("Failed to notify file deleted for: " + filePath, e);
}
}

@Tool(description = "Notifies the MCP Server to refresh the entire workspace (e.g., after a git checkout or pull).")
public void refreshWorkspace() {
logger.info("MCP refreshWorkspace called");

if (server.getClient() != null) {
logger.info("LSP client connected, skipping refreshWorkspace MCP notification.");
return;
}

projectChangeNotifier.ifPresent(notifier -> {
try {
notifier.notifyProjectsChanged();
} catch (Exception e) {
logger.error("Failed to notify workspace refresh", e);
}
});
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@
*******************************************************************************/
package org.springframework.ide.vscode.boot.mcp;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;

import org.springframework.ai.support.ToolCallbacks;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import org.springframework.ide.vscode.boot.mcp.prompts.Prompts;

import io.modelcontextprotocol.server.McpServerFeatures;
Expand All @@ -36,9 +38,10 @@ ToolCallbackProvider registerTools(
StereotypeInformation stereotypeInformation,
RequestMappingMcpTools requestMappingMcpTools,
ComponentAnalysisMcpTools componentAnalysisMcpTools,
DiagnosticsMcpTools diagnosticsMcpTools) {
DiagnosticsMcpTools diagnosticsMcpTools,
Optional<FileChangesMcpTools> fileChangesMcpTools) {

return ToolCallbackProvider.from(ToolCallbacks.from(
List<Object> tools = new ArrayList<>(Arrays.asList(
springVersionsAndGenerations,
springIoApiMcpTools,
springIndexAccess,
Expand All @@ -47,6 +50,10 @@ ToolCallbackProvider registerTools(
requestMappingMcpTools,
componentAnalysisMcpTools,
diagnosticsMcpTools));

fileChangesMcpTools.ifPresent(tools::add);

return ToolCallbackProvider.from(ToolCallbacks.from(tools.toArray()));
}

/**
Expand Down
Loading
Loading