From ab2567ed83506a23f87ce97f32c71c22efabfb0b Mon Sep 17 00:00:00 2001 From: Lucio Lelii Date: Sun, 22 Mar 2026 17:02:06 +0100 Subject: [PATCH] Make MCP servers catalog path configurable --- .../manager/mcp/MCPServersProvider.java | 38 +++++++++-- src/main/resources/application.properties | 1 + .../manager/mcp/MCPServersProviderTest.java | 66 +++++++++++++++++++ 3 files changed, 100 insertions(+), 5 deletions(-) create mode 100644 src/test/java/it/cnr/isti/workflow/manager/mcp/MCPServersProviderTest.java diff --git a/src/main/java/it/cnr/isti/workflow/manager/mcp/MCPServersProvider.java b/src/main/java/it/cnr/isti/workflow/manager/mcp/MCPServersProvider.java index 47e6eba..9c5bcab 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/mcp/MCPServersProvider.java +++ b/src/main/java/it/cnr/isti/workflow/manager/mcp/MCPServersProvider.java @@ -4,8 +4,12 @@ import java.io.InputStream; import java.util.List; import java.util.Objects; +import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.databind.JsonNode; @@ -16,8 +20,13 @@ public class MCPServersProvider { private final List servers; - public MCPServersProvider(ObjectMapper objectMapper) { - this.servers = loadServers(Objects.requireNonNull(objectMapper, "objectMapper cannot be null")); + public MCPServersProvider(ObjectMapper objectMapper, + ResourceLoader resourceLoader, + @Value("${app.mcp.servers.file:}") String catalogLocation) { + this.servers = loadServers( + Objects.requireNonNull(objectMapper, "objectMapper cannot be null"), + Objects.requireNonNull(resourceLoader, "resourceLoader cannot be null"), + catalogLocation); } public List getServers() { @@ -31,9 +40,10 @@ public class MCPServersProvider { .orElseThrow(() -> new IllegalArgumentException("Unknown MCP server: " + id)); } - private List loadServers(ObjectMapper objectMapper) { + private List loadServers(ObjectMapper objectMapper, ResourceLoader resourceLoader, + String catalogLocation) { try { - ClassPathResource resource = new ClassPathResource("mcp-servers.json"); + Resource resource = resolveResource(resourceLoader, catalogLocation); try (InputStream inputStream = resource.getInputStream()) { MCPServersCatalog catalog = objectMapper.readValue(inputStream, MCPServersCatalog.class); if (catalog.servers() == null) { @@ -42,10 +52,28 @@ public class MCPServersProvider { return List.copyOf(catalog.servers()); } } catch (Exception ex) { - throw new IllegalStateException("Unable to load MCP servers catalog from classpath:mcp-servers.json", ex); + throw new IllegalStateException("Unable to load MCP servers catalog from " + describeLocation(catalogLocation), ex); } } + private Resource resolveResource(ResourceLoader resourceLoader, String catalogLocation) { + if (!StringUtils.hasText(catalogLocation)) { + return new ClassPathResource("mcp-servers.json"); + } + String resolvedLocation = catalogLocation.startsWith("classpath:") || catalogLocation.startsWith("file:") + ? catalogLocation + : "file:" + catalogLocation; + Resource resource = resourceLoader.getResource(resolvedLocation); + if (!resource.exists()) { + throw new IllegalStateException("MCP servers catalog not found at " + resolvedLocation); + } + return resource; + } + + private String describeLocation(String catalogLocation) { + return StringUtils.hasText(catalogLocation) ? catalogLocation : "classpath:mcp-servers.json"; + } + @JsonIgnoreProperties(ignoreUnknown = true) private record MCPServersCatalog(List servers) { } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 7e3b4fa..bc6f6ea 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -32,6 +32,7 @@ app.security.key=${WFEDITOR_SECRET_KEY:088c65fd2a5ca418a79cd10df5dff15c0a79781c0 app.ollama.internal.key=${OLLAMA_INTERNAL_KEY:ollama} app.ollama.internal.url=${OLLAMA_INTERNAL_URL:https://ollama.internal/api} app.mcp.bridge.url=${MCP_BRIDGE_URL:http://localhost:8000} +app.mcp.servers.file=${MCP_SERVERS_FILE:} app.assistant.default-model=${ASSISTANT_DEFAULT_MODEL:gemma3:12b} cors.allowed-origins=${CORS_ALLOWED_ORIGINS:http://localhost:4200} app.import.path=${IMPORT_PATH:/workflow-editor-init} diff --git a/src/test/java/it/cnr/isti/workflow/manager/mcp/MCPServersProviderTest.java b/src/test/java/it/cnr/isti/workflow/manager/mcp/MCPServersProviderTest.java new file mode 100644 index 0000000..b0abebb --- /dev/null +++ b/src/test/java/it/cnr/isti/workflow/manager/mcp/MCPServersProviderTest.java @@ -0,0 +1,66 @@ +package it.cnr.isti.workflow.manager.mcp; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.springframework.core.io.DefaultResourceLoader; + +import com.fasterxml.jackson.databind.ObjectMapper; + +public class MCPServersProviderTest { + + @TempDir + Path tempDir; + + @Test + void loadsCustomCatalogFromConfiguredFile() throws Exception { + Path customCatalog = tempDir.resolve("custom-mcp-servers.json"); + Files.writeString(customCatalog, """ + { + "servers": [ + { + "id": "custom", + "name": "Custom", + "description": "Custom test server", + "transport": "stdio", + "command": "echo", + "args": ["hello"], + "configurationSchema": { + "type": "object", + "properties": {} + } + } + ] + } + """); + + MCPServersProvider provider = new MCPServersProvider( + new ObjectMapper(), + new DefaultResourceLoader(), + customCatalog.toString()); + + List servers = provider.getServers(); + assertEquals(1, servers.size()); + assertEquals("custom", servers.getFirst().id()); + assertEquals("echo", servers.getFirst().command()); + } + + @Test + void fallsBackToClasspathCatalogWhenPropertyIsBlank() { + MCPServersProvider provider = new MCPServersProvider( + new ObjectMapper(), + new DefaultResourceLoader(), + ""); + + List servers = provider.getServers(); + assertFalse(servers.isEmpty()); + assertTrue(servers.stream().anyMatch(server -> server.id().equals("filesystem"))); + } +}