Make MCP servers catalog path configurable

This commit is contained in:
Lucio Lelii 2026-03-22 17:02:06 +01:00
parent f4dbf306b8
commit ab2567ed83
3 changed files with 100 additions and 5 deletions

View File

@ -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<MCPServerDefinition> 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<MCPServerDefinition> getServers() {
@ -31,9 +40,10 @@ public class MCPServersProvider {
.orElseThrow(() -> new IllegalArgumentException("Unknown MCP server: " + id));
}
private List<MCPServerDefinition> loadServers(ObjectMapper objectMapper) {
private List<MCPServerDefinition> 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<MCPServerDefinition> servers) {
}

View File

@ -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}

View File

@ -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<MCPServersProvider.MCPServerDefinition> 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<MCPServersProvider.MCPServerDefinition> servers = provider.getServers();
assertFalse(servers.isEmpty());
assertTrue(servers.stream().anyMatch(server -> server.id().equals("filesystem")));
}
}