Skip to content

Commit 5732792

Browse files
committed
feat(config): Refactor configuration loading and validation with new McpConfigurationLoader and McpConfigurationChecker
1 parent e806ff9 commit 5732792

20 files changed

+631
-266
lines changed

src/main/java/com/github/codeboyzhou/mcp/declarative/McpServers.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package com.github.codeboyzhou.mcp.declarative;
22

3+
import com.github.codeboyzhou.mcp.declarative.configuration.McpConfigurationLoader;
34
import com.github.codeboyzhou.mcp.declarative.configuration.McpServerConfiguration;
4-
import com.github.codeboyzhou.mcp.declarative.configuration.YAMLConfigurationLoader;
55
import com.github.codeboyzhou.mcp.declarative.di.DependencyInjector;
66
import com.github.codeboyzhou.mcp.declarative.di.DependencyInjectorProvider;
77
import com.github.codeboyzhou.mcp.declarative.di.GuiceDependencyInjector;
@@ -106,13 +106,13 @@ public void startStreamableServer(McpStreamableServerInfo serverInfo) {
106106
*/
107107
public void startServer(String configFileName) {
108108
Assert.notNull(configFileName, "configFileName must not be null");
109-
YAMLConfigurationLoader configLoader = new YAMLConfigurationLoader(configFileName);
109+
McpConfigurationLoader configLoader = new McpConfigurationLoader(configFileName);
110110
doStartServer(configLoader.loadConfig());
111111
}
112112

113113
/** Starts a server with the default configuration file name. */
114114
public void startServer() {
115-
YAMLConfigurationLoader configLoader = new YAMLConfigurationLoader();
115+
McpConfigurationLoader configLoader = new McpConfigurationLoader();
116116
doStartServer(configLoader.loadConfig());
117117
}
118118

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
package com.github.codeboyzhou.mcp.declarative.configuration;
2+
3+
import com.github.codeboyzhou.mcp.declarative.enums.ServerMode;
4+
import com.github.codeboyzhou.mcp.declarative.exception.McpServerConfigurationException;
5+
import com.github.codeboyzhou.mcp.declarative.util.StringHelper;
6+
7+
/**
8+
* Utility class for validating MCP server configuration properties.
9+
*
10+
* <p>This class provides static methods to perform comprehensive validation of MCP server
11+
* configuration objects, ensuring that all required fields are present and properly configured. It
12+
* validates both base configuration properties and mode-specific settings for SSE and STREAMABLE
13+
* server modes.
14+
*
15+
* @see McpServerConfiguration
16+
* @author codeboyzhou
17+
*/
18+
public final class McpConfigurationChecker {
19+
/**
20+
* Private constructor to prevent instantiation of this utility class.
21+
*
22+
* @throws UnsupportedOperationException always thrown when attempting to instantiate
23+
*/
24+
private McpConfigurationChecker() {
25+
throw new UnsupportedOperationException("Utility class should not be instantiated");
26+
}
27+
28+
/**
29+
* Performs comprehensive validation of the MCP server configuration.
30+
*
31+
* <p>This method validates all required configuration properties including:
32+
*
33+
* <ul>
34+
* <li>Basic server information (enabled, mode, name, version, type, instructions)
35+
* <li>Timeout and capabilities settings
36+
* <li>Change notification configuration
37+
* <li>Mode-specific settings (SSE or STREAMABLE)
38+
* </ul>
39+
*
40+
* @param configuration the MCP server configuration to validate
41+
* @throws McpServerConfigurationException if any required configuration property is missing
42+
*/
43+
public static void check(McpServerConfiguration configuration) {
44+
checkNull("enabled", configuration.enabled());
45+
checkNull("mode", configuration.mode());
46+
checkBlank("name", configuration.name());
47+
checkBlank("version", configuration.version());
48+
checkNull("type", configuration.type());
49+
checkBlank("instructions", configuration.instructions());
50+
checkNull("request-timeout", configuration.requestTimeout());
51+
checkNull("capabilities", configuration.capabilities());
52+
checkNull("resource", configuration.capabilities().resource());
53+
checkNull("prompt", configuration.capabilities().prompt());
54+
checkNull("tool", configuration.capabilities().tool());
55+
checkNull("completion", configuration.capabilities().completion());
56+
checkNull("change-notification", configuration.changeNotification());
57+
checkNull("resource", configuration.changeNotification().resource());
58+
checkNull("prompt", configuration.changeNotification().prompt());
59+
checkNull("tool", configuration.changeNotification().tool());
60+
if (configuration.mode() == ServerMode.SSE) {
61+
checkNull("sse", configuration.sse());
62+
checkBlank("message-endpoint", configuration.sse().messageEndpoint());
63+
checkBlank("endpoint", configuration.sse().endpoint());
64+
checkBlank("base-url", configuration.sse().baseUrl());
65+
checkNull("port", configuration.sse().port());
66+
}
67+
if (configuration.mode() == ServerMode.STREAMABLE) {
68+
checkNull("streamable", configuration.streamable());
69+
checkBlank("mcp-endpoint", configuration.streamable().mcpEndpoint());
70+
checkNull("disallow-delete", configuration.streamable().disallowDelete());
71+
checkNull("keep-alive-interval", configuration.streamable().keepAliveInterval());
72+
checkNull("port", configuration.streamable().port());
73+
}
74+
}
75+
76+
/**
77+
* Validates that at least one of two configuration values is not null.
78+
*
79+
* <p>This method is typically used to validate profile-based configurations where a base value
80+
* and a profile-specific value can both provide the same configuration property. At least one of
81+
* them must be non-null.
82+
*
83+
* @param <T> the type of the configuration values
84+
* @param configKey the configuration key name used for error reporting
85+
* @param baseValue the base configuration value
86+
* @param profileValue the profile-specific configuration value
87+
* @throws McpServerConfigurationException if both values are null
88+
*/
89+
public static <T> void checkNull(String configKey, T baseValue, T profileValue) {
90+
if (baseValue == null && profileValue == null) {
91+
throw new McpServerConfigurationException(
92+
String.format("Missing config key '%s' in the configuration file.", configKey));
93+
}
94+
}
95+
96+
/**
97+
* Validates that a single configuration value is not null.
98+
*
99+
* <p>This method performs a simple null check on a configuration value and throws an exception if
100+
* the value is null, indicating that a required configuration property is missing.
101+
*
102+
* @param <T> the type of the configuration value
103+
* @param configKey the configuration key name used for error reporting
104+
* @param value the configuration value to validate
105+
* @throws McpServerConfigurationException if the value is null
106+
*/
107+
public static <T> void checkNull(String configKey, T value) {
108+
if (value == null) {
109+
throw new McpServerConfigurationException(
110+
String.format("Missing config key '%s' in the configuration file.", configKey));
111+
}
112+
}
113+
114+
/**
115+
* Validates that at least one of two string configuration values is not blank.
116+
*
117+
* <p>This method is typically used to validate profile-based configurations where a base value
118+
* and a profile-specific value can both provide the same string configuration property. At least
119+
* one of them must be non-blank. A value is considered blank if it is null, empty, or contains
120+
* only whitespace.
121+
*
122+
* @param configKey the configuration key name used for error reporting
123+
* @param baseValue the base configuration string value
124+
* @param profileValue the profile-specific configuration string value
125+
* @throws McpServerConfigurationException if both values are blank
126+
*/
127+
public static void checkBlank(String configKey, String baseValue, String profileValue) {
128+
if (StringHelper.isBlank(baseValue) && StringHelper.isBlank(profileValue)) {
129+
throw new McpServerConfigurationException(
130+
String.format("Missing config key '%s' in the configuration file.", configKey));
131+
}
132+
}
133+
134+
/**
135+
* Validates that a single string configuration value is not blank.
136+
*
137+
* <p>This method performs a blank check on a string configuration value and throws an exception
138+
* if the value is blank. A value is considered blank if it is null, empty, or contains only
139+
* whitespace.
140+
*
141+
* @param configKey the configuration key name used for error reporting
142+
* @param value the configuration string value to validate
143+
* @throws McpServerConfigurationException if the value is blank
144+
*/
145+
public static void checkBlank(String configKey, String value) {
146+
if (StringHelper.isBlank(value)) {
147+
throw new McpServerConfigurationException(
148+
String.format("Missing config key '%s' in the configuration file.", configKey));
149+
}
150+
}
151+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package com.github.codeboyzhou.mcp.declarative.configuration;
2+
3+
import com.github.codeboyzhou.mcp.declarative.exception.McpServerConfigurationException;
4+
import com.github.codeboyzhou.mcp.declarative.util.JacksonHelper;
5+
import com.github.codeboyzhou.mcp.declarative.util.StringHelper;
6+
import java.io.File;
7+
import java.net.URISyntaxException;
8+
import java.net.URL;
9+
import java.nio.file.Path;
10+
import java.nio.file.Paths;
11+
import org.slf4j.Logger;
12+
import org.slf4j.LoggerFactory;
13+
14+
/**
15+
* This record represents a YAML configuration loader for MCP (Model Context Protocol) server
16+
* configuration.
17+
*
18+
* <p>It loads the server configuration from a specified YAML file. If no file name is provided, the
19+
* default file name "mcp-server.yml" will be used.
20+
*
21+
* @see <a href="https://codeboyzhou.github.io/mcp-declarative-java-sdk/getting-started">MCP
22+
* Declarative Java SDK Documentation</a>
23+
* @author codeboyzhou
24+
*/
25+
public record McpConfigurationLoader(String configFileName) {
26+
27+
private static final Logger log = LoggerFactory.getLogger(McpConfigurationLoader.class);
28+
29+
/** The default file name for the MCP server configuration file. */
30+
private static final String DEFAULT_CONFIG_FILE_NAME = "mcp-server.yml";
31+
32+
/** Constructs a YAMLConfigurationLoader with the default configuration file name. */
33+
public McpConfigurationLoader() {
34+
this(DEFAULT_CONFIG_FILE_NAME);
35+
}
36+
37+
/**
38+
* Loads the MCP server configuration from the specified YAML file.
39+
*
40+
* @return the loaded MCP server configuration
41+
* @throws McpServerConfigurationException if the configuration file cannot be loaded
42+
*/
43+
public McpServerConfiguration loadConfig() {
44+
Path configFilePath = getConfigFilePath(configFileName);
45+
File file = configFilePath.toFile();
46+
McpServerConfiguration baseConfig = JacksonHelper.fromYaml(file, McpServerConfiguration.class);
47+
log.info("Configuration loaded successfully from file: {}", configFileName);
48+
49+
final String profile = baseConfig.profile();
50+
if (StringHelper.isBlank(profile)) {
51+
log.info("No profile specified in configuration file: {}", configFileName);
52+
McpConfigurationChecker.check(baseConfig);
53+
return baseConfig;
54+
}
55+
56+
final String profileConfigFileName = configFileName.replace(".yml", "-" + profile + ".yml");
57+
Path profileConfigFilePath = getConfigFilePath(profileConfigFileName);
58+
File profileConfigFile = profileConfigFilePath.toFile();
59+
McpServerConfiguration profileConfig =
60+
JacksonHelper.fromYaml(profileConfigFile, McpServerConfiguration.class);
61+
log.info("Profile configuration loaded successfully from file: {}", profileConfigFileName);
62+
63+
return McpConfigurationMerger.merge(baseConfig, profileConfig);
64+
}
65+
66+
/**
67+
* Returns the file path of the configuration file.
68+
*
69+
* @param fileName the name of the configuration file
70+
* @return the file path of the configuration file
71+
* @throws McpServerConfigurationException if the configuration file cannot be found
72+
*/
73+
private Path getConfigFilePath(String fileName) {
74+
try {
75+
ClassLoader classLoader = McpConfigurationLoader.class.getClassLoader();
76+
URL configFileUrl = classLoader.getResource(fileName);
77+
if (configFileUrl == null) {
78+
throw new McpServerConfigurationException("Configuration file not found: " + fileName);
79+
}
80+
return Paths.get(configFileUrl.toURI());
81+
} catch (URISyntaxException e) {
82+
// should never happen
83+
throw new McpServerConfigurationException("Invalid configuration file: " + fileName, e);
84+
}
85+
}
86+
}

0 commit comments

Comments
 (0)