From 90ec64f70d4878bf2868dabf1a10bec56a0ca1d3 Mon Sep 17 00:00:00 2001 From: codeboyzhou Date: Sun, 20 Apr 2025 10:50:35 +0800 Subject: [PATCH 01/57] chore(pom): Bump version to 0.3.0-SNAPSHOT --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 7c1ba39..88946e2 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ io.github.codeboyzhou mcp-declarative-java-sdk - 0.2.0 + 0.3.0-SNAPSHOT MCP Declarative Java SDK Annotation-driven MCP (Model Context Protocol) Development with Java - No Spring Framework Required From 2c23af7cae32504cae82af4f5ffdd8b4c3834973 Mon Sep 17 00:00:00 2001 From: codeboyzhou Date: Mon, 14 Apr 2025 20:08:25 +0800 Subject: [PATCH 02/57] docs(README): Clarify MCP resource and tool annotations --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9fd3d50..091c9f8 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ Just put this one line code in your `main` method: ```java // You can use this annotation to specify the base package // to scan for MCP resources, prompts, tools, but it's optional. +// If not specified, it will scan the package where the main method is located. @McpComponentScan(basePackage = "com.github.codeboyzhou.mcp.examples") public class MyMcpServer { @@ -42,7 +43,7 @@ No need to care about the low-level details of native MCP Java SDK and how to cr public class MyMcpResources { // This method defines a MCP resource to expose the OS env variables - @McpResource(uri = "env://variables", name = "env", description = "OS env variables") + @McpResource(uri = "env://variables", description = "OS env variables") public String getSystemEnv() { // Just put your logic code here, forget about the MCP SDK details. return System.getenv().toString(); @@ -57,7 +58,7 @@ public class MyMcpResources { public class MyMcpTools { // This method defines a MCP tool to read a file - @McpTool(name = "read_file", description = "Read complete file contents with UTF-8 encoding") + @McpTool(description = "Read complete file contents with UTF-8 encoding") public String readFile( @McpToolParam(name = "path", description = "filepath", required = true) String path) { // Just put your logic code here, forget about the MCP SDK details. From 5c1329944b20d4993cbf127b76829e6d14ec3c2d Mon Sep 17 00:00:00 2001 From: codeboyzhou Date: Tue, 15 Apr 2025 00:20:44 +0800 Subject: [PATCH 03/57] refactor(server): Remove deprecated methods in v0.2.0 --- .../mcp/declarative/McpServers.java | 38 +------------------ .../mcp/declarative/McpServersTest.java | 24 ++++++++++-- 2 files changed, 22 insertions(+), 40 deletions(-) diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/McpServers.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/McpServers.java index 909f06e..c2b1ef0 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/McpServers.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/McpServers.java @@ -16,9 +16,6 @@ import io.modelcontextprotocol.spec.McpServerTransportProvider; import org.reflections.Reflections; -import static io.modelcontextprotocol.server.transport.HttpServletSseServerTransportProvider.DEFAULT_BASE_URL; -import static io.modelcontextprotocol.server.transport.HttpServletSseServerTransportProvider.DEFAULT_SSE_ENDPOINT; - public class McpServers { private static final McpServers INSTANCE = new McpServers(); @@ -57,9 +54,8 @@ public void startSyncStdioServer(String name, String version, String instruction McpServerComponentRegisters.registerAllTo(server, reflections); } - @Deprecated(since = "0.2.0") - public void startSyncStdioServer(String name, String version) { - startSyncStdioServer(name, version, "You are using a deprecated API with default server instructions"); + public void startSyncStdioServer(McpServerInfo serverInfo) { + startSyncStdioServer(serverInfo.name(), serverInfo.version(), serverInfo.instructions()); } public void startSyncSseServer(McpSseServerInfo serverInfo, McpHttpServerStatusListener listener) { @@ -77,34 +73,4 @@ public void startSyncSseServer(McpSseServerInfo serverInfo) { startSyncSseServer(serverInfo, new DefaultMcpSyncHttpServerStatusListener()); } - @Deprecated(since = "0.2.0") - public void startSyncSseServer(String name, String version, String messageEndpoint, String sseEndpoint, int port) { - McpSseServerInfo serverInfo = McpSseServerInfo.builder().name(name).version(version) - .instructions("You are using a deprecated API with default server instructions") - .baseUrl(DEFAULT_BASE_URL).messageEndpoint(messageEndpoint) - .sseEndpoint(sseEndpoint).port(port) - .build(); - startSyncSseServer(serverInfo); - } - - @Deprecated(since = "0.2.0") - public void startSyncSseServer(String name, String version, int port) { - McpSseServerInfo serverInfo = McpSseServerInfo.builder().name(name).version(version) - .instructions("You are using a deprecated API with default server instructions") - .baseUrl(DEFAULT_BASE_URL).messageEndpoint(DEFAULT_MESSAGE_ENDPOINT) - .sseEndpoint(DEFAULT_SSE_ENDPOINT).port(port) - .build(); - startSyncSseServer(serverInfo); - } - - @Deprecated(since = "0.2.0") - public void startSyncSseServer(String name, String version) { - McpSseServerInfo serverInfo = McpSseServerInfo.builder().name(name).version(version) - .instructions("You are using a deprecated API with default server instructions") - .baseUrl(DEFAULT_BASE_URL).messageEndpoint(DEFAULT_MESSAGE_ENDPOINT) - .sseEndpoint(DEFAULT_SSE_ENDPOINT).port(DEFAULT_HTTP_SERVER_PORT) - .build(); - startSyncSseServer(serverInfo); - } - } diff --git a/src/test/java/com/github/codeboyzhou/mcp/declarative/McpServersTest.java b/src/test/java/com/github/codeboyzhou/mcp/declarative/McpServersTest.java index 4598a7d..cdb62c5 100644 --- a/src/test/java/com/github/codeboyzhou/mcp/declarative/McpServersTest.java +++ b/src/test/java/com/github/codeboyzhou/mcp/declarative/McpServersTest.java @@ -1,6 +1,8 @@ package com.github.codeboyzhou.mcp.declarative; import com.github.codeboyzhou.mcp.declarative.annotation.McpTools; +import com.github.codeboyzhou.mcp.declarative.server.McpServerInfo; +import com.github.codeboyzhou.mcp.declarative.server.McpSseServerInfo; import com.github.codeboyzhou.mcp.declarative.server.TestMcpComponentScanBasePackageClass; import com.github.codeboyzhou.mcp.declarative.server.TestMcpComponentScanBasePackageString; import com.github.codeboyzhou.mcp.declarative.server.TestMcpComponentScanDefault; @@ -53,7 +55,12 @@ void testRun(Class applicationMainClass) { void testStartSyncStdioServer() { assertDoesNotThrow(() -> { McpServers servers = McpServers.run(TestMcpComponentScanIsNull.class, EMPTY_ARGS); - servers.startSyncStdioServer("test-mcp-sync-stdio-server", "1.0.0"); + McpServerInfo serverInfo = McpServerInfo.builder() + .instructions("test-mcp-sync-stdio-server-instructions") + .name("test-mcp-sync-stdio-server") + .version("1.0.0") + .build(); + servers.startSyncStdioServer(serverInfo); }); } @@ -61,9 +68,18 @@ void testStartSyncStdioServer() { void testStartSyncSseServer() { System.setProperty("mcp.declarative.java.sdk.testing", "true"); McpServers servers = McpServers.run(TestMcpComponentScanIsNull.class, EMPTY_ARGS); - assertDoesNotThrow(() -> servers.startSyncSseServer("test-mcp-sync-sse-server", "1.0.0")); - assertDoesNotThrow(() -> servers.startSyncSseServer("test-mcp-sync-sse-server", "1.0.0", 9118)); - assertDoesNotThrow(() -> servers.startSyncSseServer("test-mcp-sync-sse-server", "1.0.0", "/message", "/sse", 9119)); + assertDoesNotThrow(() -> { + McpSseServerInfo serverInfo = McpSseServerInfo.builder() + .instructions("test-mcp-sync-sse-server-instructions") + .baseUrl("http://127.0.0.1:8080") + .messageEndpoint("/message") + .sseEndpoint("/sse") + .port(8080) + .name("test-mcp-sync-sse-server") + .version("1.0.0") + .build(); + servers.startSyncSseServer(serverInfo); + }); } private Reflections getReflectionsField() throws NoSuchFieldException, IllegalAccessException { From 60c12d78929bd1467456b07986d98ab04a4423c7 Mon Sep 17 00:00:00 2001 From: codeboyzhou Date: Tue, 15 Apr 2025 01:25:44 +0800 Subject: [PATCH 04/57] feat(annotation): Introduce the annotations related to MCP prompt --- .../mcp/declarative/annotation/McpPrompt.java | 15 ++++ .../annotation/McpPromptParam.java | 17 +++++ .../declarative/annotation/McpPrompts.java | 11 +++ .../server/McpServerComponentRegisters.java | 4 + .../server/McpSyncServerPromptRegister.java | 75 +++++++++++++++++++ .../declarative/util/ReflectionHelper.java | 5 ++ 6 files changed, 127 insertions(+) create mode 100644 src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpPrompt.java create mode 100644 src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpPromptParam.java create mode 100644 src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpPrompts.java create mode 100644 src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerPromptRegister.java diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpPrompt.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpPrompt.java new file mode 100644 index 0000000..bf63491 --- /dev/null +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpPrompt.java @@ -0,0 +1,15 @@ +package com.github.codeboyzhou.mcp.declarative.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface McpPrompt { + + String name() default ""; + + String description(); +} diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpPromptParam.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpPromptParam.java new file mode 100644 index 0000000..6ab2500 --- /dev/null +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpPromptParam.java @@ -0,0 +1,17 @@ +package com.github.codeboyzhou.mcp.declarative.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface McpPromptParam { + + String name(); + + String description(); + + boolean required() default false; +} diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpPrompts.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpPrompts.java new file mode 100644 index 0000000..e675a06 --- /dev/null +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpPrompts.java @@ -0,0 +1,11 @@ +package com.github.codeboyzhou.mcp.declarative.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface McpPrompts { +} diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpServerComponentRegisters.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpServerComponentRegisters.java index 6a016d1..940021a 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpServerComponentRegisters.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpServerComponentRegisters.java @@ -1,5 +1,6 @@ package com.github.codeboyzhou.mcp.declarative.server; +import com.github.codeboyzhou.mcp.declarative.annotation.McpPrompts; import com.github.codeboyzhou.mcp.declarative.annotation.McpResources; import com.github.codeboyzhou.mcp.declarative.annotation.McpTools; import io.modelcontextprotocol.server.McpSyncServer; @@ -13,6 +14,9 @@ public static void registerAllTo(McpSyncServer server, Reflections reflections) Set> resourceClasses = reflections.getTypesAnnotatedWith(McpResources.class); new McpSyncServerResourceRegister(resourceClasses).registerTo(server); + Set> promptClasses = reflections.getTypesAnnotatedWith(McpPrompts.class); + new McpSyncServerPromptRegister(promptClasses).registerTo(server); + Set> toolClasses = reflections.getTypesAnnotatedWith(McpTools.class); new McpSyncServerToolRegister(toolClasses).registerTo(server); } diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerPromptRegister.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerPromptRegister.java new file mode 100644 index 0000000..520b637 --- /dev/null +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerPromptRegister.java @@ -0,0 +1,75 @@ +package com.github.codeboyzhou.mcp.declarative.server; + +import com.github.codeboyzhou.mcp.declarative.annotation.McpPrompt; +import com.github.codeboyzhou.mcp.declarative.annotation.McpPromptParam; +import com.github.codeboyzhou.mcp.declarative.util.ReflectionHelper; +import io.modelcontextprotocol.server.McpServerFeatures; +import io.modelcontextprotocol.server.McpSyncServer; +import io.modelcontextprotocol.spec.McpSchema; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +public class McpSyncServerPromptRegister + implements McpServerComponentRegister { + + private static final Logger logger = LoggerFactory.getLogger(McpSyncServerPromptRegister.class); + + private final Set> promptClasses; + + public McpSyncServerPromptRegister(Set> promptClasses) { + this.promptClasses = promptClasses; + } + + @Override + public void registerTo(McpSyncServer server) { + for (Class promptClass : promptClasses) { + Set methods = ReflectionHelper.getMethodsAnnotatedWith(promptClass, McpPrompt.class); + for (Method method : methods) { + McpServerFeatures.SyncPromptSpecification prompt = createComponentFrom(promptClass, method); + server.addPrompt(prompt); + } + } + } + + @Override + public McpServerFeatures.SyncPromptSpecification createComponentFrom(Class clazz, Method method) { + McpPrompt promptMethod = method.getAnnotation(McpPrompt.class); + final String name = promptMethod.name().isBlank() ? method.getName() : promptMethod.name(); + final String description = promptMethod.description(); + List promptArguments = createPromptArguments(method); + McpSchema.Prompt prompt = new McpSchema.Prompt(name, description, promptArguments); + return new McpServerFeatures.SyncPromptSpecification(prompt, (exchange, request) -> { + Object result; + try { + result = ReflectionHelper.invokeMethod(clazz, method, request.arguments()); + } catch (Throwable e) { + logger.error("Error invoking prompt method", e); + result = e + ": " + e.getMessage(); + } + McpSchema.Content content = new McpSchema.TextContent(result.toString()); + McpSchema.PromptMessage message = new McpSchema.PromptMessage(McpSchema.Role.USER, content); + return new McpSchema.GetPromptResult(description, List.of(message)); + }); + } + + private List createPromptArguments(Method method) { + Set parameters = ReflectionHelper.getParametersAnnotatedWith(method, McpPromptParam.class); + List promptArguments = new ArrayList<>(parameters.size()); + for (Parameter parameter : parameters) { + McpPromptParam promptParam = parameter.getAnnotation(McpPromptParam.class); + final String name = promptParam.name(); + final String description = promptParam.description(); + final boolean required = promptParam.required(); + McpSchema.PromptArgument promptArgument = new McpSchema.PromptArgument(name, description, required); + promptArguments.add(promptArgument); + } + return promptArguments; + } + +} diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/util/ReflectionHelper.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/util/ReflectionHelper.java index e53e30b..efa3e79 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/util/ReflectionHelper.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/util/ReflectionHelper.java @@ -28,6 +28,11 @@ public static Object invokeMethod(Class clazz, Method method) throws Exceptio return method.invoke(object); } + public static Object invokeMethod(Class clazz, Method method, Map parameters) throws Exception { + Object object = clazz.getDeclaredConstructor().newInstance(); + return method.invoke(object, parameters.values().toArray()); + } + public static Object invokeMethod(Class clazz, Method method, McpSchema.JsonSchema schema, Map parameters) throws Exception { Object object = clazz.getDeclaredConstructor().newInstance(); Map typedParameters = asTypedParameters(schema, parameters); From 2f166d29a7177bd6a1b09780af00ee7257447de9 Mon Sep 17 00:00:00 2001 From: codeboyzhou Date: Sat, 19 Apr 2025 13:49:27 +0800 Subject: [PATCH 05/57] docs(README): Update examples repository link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 091c9f8..ef21c15 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ Add the following Maven dependency to your project: ### Examples -You can find more examples and usages in this [repository](https://github.com/codeboyzhou/mcp-declarative-java-sdk-examples). +You can find more examples and usages in this [repository](https://github.com/codeboyzhou/mcp-java-sdk-examples). ## What is MCP? From 315938941ca6c9a016f4b4126d94e2cfe1cf8c24 Mon Sep 17 00:00:00 2001 From: codeboyzhou Date: Sun, 20 Apr 2025 11:44:20 +0800 Subject: [PATCH 06/57] docs(server): Update README.md with new server configuration examples --- README.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ef21c15..3605bdd 100644 --- a/README.md +++ b/README.md @@ -23,14 +23,18 @@ Just put this one line code in your `main` method: // You can use this annotation to specify the base package // to scan for MCP resources, prompts, tools, but it's optional. // If not specified, it will scan the package where the main method is located. -@McpComponentScan(basePackage = "com.github.codeboyzhou.mcp.examples") +@McpComponentScan(basePackage = "com.github.codeboyzhou.mcp.server.examples") public class MyMcpServer { public static void main(String[] args) { // Start a STDIO MCP server - McpServers.run(MyMcpServer.class, args).startSyncStdioServer("mcp-server", "1.0.0"); + McpServers.run(MyMcpServer.class, args).startSyncStdioServer( + McpServerInfo.builder().name("mcp-server").version("1.0.0").build() + ); // or a HTTP SSE MCP server - McpServers.run(MyMcpServer.class, args).startSyncSseServer("mcp-server", "1.0.0"); + McpServers.run(MyMcpServer.class, args).startSyncSseServer( + McpSseServerInfo.builder().name("mcp-server").version("1.0.0").port(8080).build() + ); } } From 4393ab755f74e07ff8d50a60172a636275a4b01c Mon Sep 17 00:00:00 2001 From: codeboyzhou Date: Sun, 20 Apr 2025 11:48:39 +0800 Subject: [PATCH 07/57] docs(README): Add example for McpPrompts annotation --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index 3605bdd..88d2d77 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,20 @@ public class MyMcpResources { } ``` +```java +@McpPrompts +public class MyMcpPrompts { + + @McpPrompt(description = "A simple prompt to read a file") + public String readFile( + @McpPromptParam(name = "path", description = "filepath", required = true) String path) { + // Just put your logic code here, forget about the MCP SDK details. + return String.format("What is the complete contents of the file: %s", path); + } + +} +``` + ```java @McpTools public class MyMcpTools { From b2bc3e70ccb837e2f6e60a4a9d0cd6739cabfce7 Mon Sep 17 00:00:00 2001 From: codeboyzhou Date: Sun, 20 Apr 2025 12:57:05 +0800 Subject: [PATCH 08/57] chore(release): v0.3.0 released --- README.md | 2 +- pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 88d2d77..7d67082 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ Add the following Maven dependency to your project: io.github.codeboyzhou mcp-declarative-java-sdk - 0.2.0 + 0.3.0 ``` diff --git a/pom.xml b/pom.xml index 88946e2..3f4d119 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ io.github.codeboyzhou mcp-declarative-java-sdk - 0.3.0-SNAPSHOT + 0.3.0 MCP Declarative Java SDK Annotation-driven MCP (Model Context Protocol) Development with Java - No Spring Framework Required From 2f62e0b0aabab668fed78119a8d354dd3977952a Mon Sep 17 00:00:00 2001 From: codeboyzhou Date: Sun, 20 Apr 2025 13:19:00 +0800 Subject: [PATCH 09/57] chore(pom): Bump version to 0.4.0-SNAPSHOT --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 3f4d119..97dc455 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ io.github.codeboyzhou mcp-declarative-java-sdk - 0.3.0 + 0.4.0-SNAPSHOT MCP Declarative Java SDK Annotation-driven MCP (Model Context Protocol) Development with Java - No Spring Framework Required @@ -54,7 +54,7 @@ 12.0.18 5.10.2 1.5.18 - 0.9.0 + 0.10.0-SNAPSHOT 0.10.2 From ce85a7ae1594b1add7a161c458a4786338ccb8e9 Mon Sep 17 00:00:00 2001 From: codeboyzhou Date: Sun, 20 Apr 2025 13:29:55 +0800 Subject: [PATCH 10/57] feat(server): Add requestTimeout field to McpServerInfo --- .../mcp/declarative/McpServers.java | 18 +++++++++--------- .../mcp/declarative/server/McpServerInfo.java | 16 ++++++++++++++++ .../server/McpSyncServerFactory.java | 3 ++- .../mcp/declarative/McpServersTest.java | 3 +++ 4 files changed, 30 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/McpServers.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/McpServers.java index c2b1ef0..2474bfa 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/McpServers.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/McpServers.java @@ -16,16 +16,14 @@ import io.modelcontextprotocol.spec.McpServerTransportProvider; import org.reflections.Reflections; +import java.time.Duration; + public class McpServers { private static final McpServers INSTANCE = new McpServers(); private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); - private static final String DEFAULT_MESSAGE_ENDPOINT = "/message"; - - private static final int DEFAULT_HTTP_SERVER_PORT = 8080; - private static Reflections reflections; public static McpServers run(Class applicationMainClass, String[] args) { @@ -46,18 +44,20 @@ private static String determineBasePackage(McpComponentScan scan, Class appli return applicationMainClass.getPackageName(); } + @Deprecated(since = "0.4.0") public void startSyncStdioServer(String name, String version, String instructions) { + McpServerInfo serverInfo = McpServerInfo.builder().name(name).version(version) + .instructions(instructions).requestTimeout(Duration.ofSeconds(10)).build(); + startSyncStdioServer(serverInfo); + } + + public void startSyncStdioServer(McpServerInfo serverInfo) { McpServerFactory factory = new McpSyncServerFactory(); - McpServerInfo serverInfo = McpServerInfo.builder().name(name).version(version).instructions(instructions).build(); McpServerTransportProvider transportProvider = new StdioServerTransportProvider(); McpSyncServer server = factory.create(serverInfo, transportProvider); McpServerComponentRegisters.registerAllTo(server, reflections); } - public void startSyncStdioServer(McpServerInfo serverInfo) { - startSyncStdioServer(serverInfo.name(), serverInfo.version(), serverInfo.instructions()); - } - public void startSyncSseServer(McpSseServerInfo serverInfo, McpHttpServerStatusListener listener) { McpServerFactory factory = new McpSyncServerFactory(); HttpServletSseServerTransportProvider transportProvider = new HttpServletSseServerTransportProvider( diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpServerInfo.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpServerInfo.java index 6674e1c..1ac266a 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpServerInfo.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpServerInfo.java @@ -1,5 +1,7 @@ package com.github.codeboyzhou.mcp.declarative.server; +import java.time.Duration; + public class McpServerInfo { private final String name; @@ -8,10 +10,13 @@ public class McpServerInfo { private final String instructions; + private final Duration requestTimeout; + protected McpServerInfo(Builder builder) { this.name = builder.name; this.version = builder.version; this.instructions = builder.instructions; + this.requestTimeout = builder.requestTimeout; } public static Builder builder() { @@ -30,6 +35,10 @@ public String instructions() { return instructions; } + public Duration requestTimeout() { + return requestTimeout; + } + @SuppressWarnings("unchecked") public static class Builder> { @@ -39,6 +48,8 @@ public static class Builder> { protected String instructions; + protected Duration requestTimeout; + protected T self() { return (T) this; } @@ -62,6 +73,11 @@ public T instructions(String instructions) { return self(); } + public T requestTimeout(Duration requestTimeout) { + this.requestTimeout = requestTimeout; + return self(); + } + } } diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerFactory.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerFactory.java index b05da27..646bbef 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerFactory.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerFactory.java @@ -12,7 +12,8 @@ public McpSyncServer create(McpServerInfo serverInfo, McpServerTransportProvider .instructions(serverInfo.instructions()) .capabilities(configureServerCapabilities()) .serverInfo(serverInfo.name(), serverInfo.version()) + .requestTimeout(serverInfo.requestTimeout()) .build(); } - + } diff --git a/src/test/java/com/github/codeboyzhou/mcp/declarative/McpServersTest.java b/src/test/java/com/github/codeboyzhou/mcp/declarative/McpServersTest.java index cdb62c5..e602853 100644 --- a/src/test/java/com/github/codeboyzhou/mcp/declarative/McpServersTest.java +++ b/src/test/java/com/github/codeboyzhou/mcp/declarative/McpServersTest.java @@ -16,6 +16,7 @@ import org.reflections.scanners.Scanners; import java.lang.reflect.Field; +import java.time.Duration; import java.util.Map; import java.util.Set; @@ -57,6 +58,7 @@ void testStartSyncStdioServer() { McpServers servers = McpServers.run(TestMcpComponentScanIsNull.class, EMPTY_ARGS); McpServerInfo serverInfo = McpServerInfo.builder() .instructions("test-mcp-sync-stdio-server-instructions") + .requestTimeout(Duration.ofSeconds(10)) .name("test-mcp-sync-stdio-server") .version("1.0.0") .build(); @@ -71,6 +73,7 @@ void testStartSyncSseServer() { assertDoesNotThrow(() -> { McpSseServerInfo serverInfo = McpSseServerInfo.builder() .instructions("test-mcp-sync-sse-server-instructions") + .requestTimeout(Duration.ofSeconds(10)) .baseUrl("http://127.0.0.1:8080") .messageEndpoint("/message") .sseEndpoint("/sse") From c1bfe12cab91c44f2af868c57ca6b355c20f0b2d Mon Sep 17 00:00:00 2001 From: codeboyzhou Date: Sun, 20 Apr 2025 19:40:48 +0800 Subject: [PATCH 11/57] feat(server): Add support for MCP server configuration --- pom.xml | 6 ++ .../mcp/declarative/McpServers.java | 64 +++++++++++++++++++ .../configuration/McpServerConfiguration.java | 45 +++++++++++++ .../YamlConfigurationLoader.java | 49 ++++++++++++++ .../mcp/declarative/enums/ServerType.java | 6 ++ .../exception/McpServerException.java | 13 ++++ 6 files changed, 183 insertions(+) create mode 100644 src/main/java/com/github/codeboyzhou/mcp/declarative/configuration/McpServerConfiguration.java create mode 100644 src/main/java/com/github/codeboyzhou/mcp/declarative/configuration/YamlConfigurationLoader.java create mode 100644 src/main/java/com/github/codeboyzhou/mcp/declarative/enums/ServerType.java create mode 100644 src/main/java/com/github/codeboyzhou/mcp/declarative/exception/McpServerException.java diff --git a/pom.xml b/pom.xml index 97dc455..13e50b0 100644 --- a/pom.xml +++ b/pom.xml @@ -51,6 +51,7 @@ 3.2.3 4.0.0 + 2.18.3 12.0.18 5.10.2 1.5.18 @@ -78,6 +79,11 @@ ${logback.version} test + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + ${jackson-dataformat-yaml.version} + io.modelcontextprotocol.sdk mcp diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/McpServers.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/McpServers.java index 2474bfa..3256988 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/McpServers.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/McpServers.java @@ -2,6 +2,10 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.github.codeboyzhou.mcp.declarative.annotation.McpComponentScan; +import com.github.codeboyzhou.mcp.declarative.configuration.McpServerConfiguration; +import com.github.codeboyzhou.mcp.declarative.configuration.YamlConfigurationLoader; +import com.github.codeboyzhou.mcp.declarative.enums.ServerType; +import com.github.codeboyzhou.mcp.declarative.exception.McpServerException; import com.github.codeboyzhou.mcp.declarative.listener.DefaultMcpSyncHttpServerStatusListener; import com.github.codeboyzhou.mcp.declarative.listener.McpHttpServerStatusListener; import com.github.codeboyzhou.mcp.declarative.server.McpHttpServer; @@ -15,11 +19,16 @@ import io.modelcontextprotocol.server.transport.StdioServerTransportProvider; import io.modelcontextprotocol.spec.McpServerTransportProvider; import org.reflections.Reflections; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.io.IOException; import java.time.Duration; public class McpServers { + private static final Logger logger = LoggerFactory.getLogger(McpServers.class); + private static final McpServers INSTANCE = new McpServers(); private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); @@ -73,4 +82,59 @@ public void startSyncSseServer(McpSseServerInfo serverInfo) { startSyncSseServer(serverInfo, new DefaultMcpSyncHttpServerStatusListener()); } + public void startServer(String configFileName) { + McpServerConfiguration configuration = loadConfiguration(configFileName); + if (configuration.enabled()) { + startServerWith(configuration); + } else { + logger.info("MCP server is disabled."); + } + } + + public void startServer() { + // Load configuration from default file + startServer(null); + } + + private McpServerConfiguration loadConfiguration(String configFileName) { + YamlConfigurationLoader configurationLoader = new YamlConfigurationLoader(); + McpServerConfiguration configuration; + try { + if (configFileName == null || configFileName.isBlank()) { + configuration = configurationLoader.loadConfiguration(); + } else { + configuration = configurationLoader.load(configFileName); + } + } catch (IOException e) { + throw new McpServerException("Error loading configuration file", e); + } + return configuration; + } + + private void startServerWith(McpServerConfiguration configuration) { + if (ServerType.SYNC.name().equalsIgnoreCase(configuration.type())) { + if (configuration.stdio()) { + McpServerInfo serverInfo = McpServerInfo.builder() + .name(configuration.name()) + .version(configuration.version()) + .instructions(configuration.instructions()) + .requestTimeout(Duration.ofSeconds(configuration.requestTimeout())) + .build(); + startSyncStdioServer(serverInfo); + } else { + McpSseServerInfo serverInfo = McpSseServerInfo.builder() + .name(configuration.name()) + .version(configuration.version()) + .instructions(configuration.instructions()) + .requestTimeout(Duration.ofSeconds(configuration.requestTimeout())) + .baseUrl(configuration.baseUrl()) + .messageEndpoint(configuration.sseMessageEndpoint()) + .sseEndpoint(configuration.sseEndpoint()) + .port(configuration.ssePort()) + .build(); + startSyncSseServer(serverInfo); + } + } + } + } diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/configuration/McpServerConfiguration.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/configuration/McpServerConfiguration.java new file mode 100644 index 0000000..a5c00a0 --- /dev/null +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/configuration/McpServerConfiguration.java @@ -0,0 +1,45 @@ +package com.github.codeboyzhou.mcp.declarative.configuration; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Map; + +public record McpServerConfiguration( + @JsonProperty("enabled") boolean enabled, + @JsonProperty("stdio") boolean stdio, + @JsonProperty("name") String name, + @JsonProperty("version") String version, + @JsonProperty("instructions") String instructions, + @JsonProperty("request-timeout") long requestTimeout, + @JsonProperty("type") String type, + @JsonProperty("resource-change-notification") boolean resourceChangeNotification, + @JsonProperty("prompt-change-notification") boolean promptChangeNotification, + @JsonProperty("tool-change-notification") boolean toolChangeNotification, + @JsonProperty("tool-response-mime-type") Map toolResponseMimeType, + @JsonProperty("sse-message-endpoint") String sseMessageEndpoint, + @JsonProperty("sse-endpoint") String sseEndpoint, + @JsonProperty("base-url") String baseUrl, + @JsonProperty("sse-port") int ssePort +) { + + public static McpServerConfiguration defaultConfiguration() { + return new McpServerConfiguration( + true, + false, + "mcp-server", + "1.0.0", + "mcp-server", + 10000, + "SYNC", + true, + true, + true, + Map.of(), + "/mcp/message", + "/sse", + "", + 8080 + ); + } + +} diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/configuration/YamlConfigurationLoader.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/configuration/YamlConfigurationLoader.java new file mode 100644 index 0000000..9d4c565 --- /dev/null +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/configuration/YamlConfigurationLoader.java @@ -0,0 +1,49 @@ +package com.github.codeboyzhou.mcp.declarative.configuration; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.file.NoSuchFileException; +import java.util.Objects; + +import static java.util.stream.Collectors.joining; + +public class YamlConfigurationLoader { + + private static final Logger logger = LoggerFactory.getLogger(YamlConfigurationLoader.class); + + private final ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); + + public McpServerConfiguration loadConfiguration() { + try { + McpServerConfiguration configuration = load("mcp-server.yml"); + if (configuration == null) { + configuration = load("mcp-server.yaml"); + } + return Objects.requireNonNullElseGet(configuration, McpServerConfiguration::defaultConfiguration); + } catch (IOException e) { + logger.error("Error loading configuration file, will use default configuration", e); + return McpServerConfiguration.defaultConfiguration(); + } + } + + public McpServerConfiguration load(String configFileName) throws IOException { + ClassLoader classLoader = YamlConfigurationLoader.class.getClassLoader(); + try (InputStream inputStream = classLoader.getResourceAsStream(configFileName)) { + if (inputStream == null) { + throw new NoSuchFileException(configFileName); + } + InputStreamReader inputStreamReader = new InputStreamReader(inputStream); + BufferedReader bufferedReader = new BufferedReader(inputStreamReader); + final String content = bufferedReader.lines().collect(joining(System.lineSeparator())); + return mapper.readValue(content, McpServerConfiguration.class); + } + } + +} diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/enums/ServerType.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/enums/ServerType.java new file mode 100644 index 0000000..f159f61 --- /dev/null +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/enums/ServerType.java @@ -0,0 +1,6 @@ +package com.github.codeboyzhou.mcp.declarative.enums; + +public enum ServerType { + SYNC, + ASYNC +} diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/exception/McpServerException.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/exception/McpServerException.java new file mode 100644 index 0000000..ee9b39f --- /dev/null +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/exception/McpServerException.java @@ -0,0 +1,13 @@ +package com.github.codeboyzhou.mcp.declarative.exception; + +public class McpServerException extends RuntimeException { + + public McpServerException(String message) { + super(message); + } + + public McpServerException(String message, Throwable cause) { + super(message, cause); + } + +} From 746f43b7f64e37b7ecbc58420e9675c9bbae3149 Mon Sep 17 00:00:00 2001 From: codeboyzhou Date: Sun, 20 Apr 2025 19:50:30 +0800 Subject: [PATCH 12/57] docs(README): Update README.md to introduce server configuration --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 7d67082..c141072 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ Declarative [MCP Java SDK](https://github.com/modelcontextprotocol/java-sdk) Dev - No need to write more SDK low-level codes. - Get rid of complex and lengthy JSON schema definitions. - Just focus on your core logic (resources/prompts/tools). +- Configuration file compatible with the Spring AI framework. ## Showcase @@ -35,6 +36,8 @@ public class MyMcpServer { McpServers.run(MyMcpServer.class, args).startSyncSseServer( McpSseServerInfo.builder().name("mcp-server").version("1.0.0").port(8080).build() ); + // or start with yaml configuration file compatible with the Spring AI framework + McpServers.run(MyMcpServer.class, args).startServer(); } } From 066fd2708d745a28cdc6c7dee5c6dec5a52da1ec Mon Sep 17 00:00:00 2001 From: codeboyzhou Date: Mon, 21 Apr 2025 00:00:44 +0800 Subject: [PATCH 13/57] feat(test): Add unit tests for McpServers startServer method --- .../mcp/declarative/McpServers.java | 30 +++++--------- .../mcp/declarative/McpServersTest.java | 40 ++++++++++++++++++- src/test/resources/mcp-server-async.yml | 7 ++++ src/test/resources/mcp-server-not-enabled.yml | 7 ++++ src/test/resources/mcp-server-sse-mode.yml | 7 ++++ src/test/resources/mcp-server.yml | 7 ++++ 6 files changed, 78 insertions(+), 20 deletions(-) create mode 100644 src/test/resources/mcp-server-async.yml create mode 100644 src/test/resources/mcp-server-not-enabled.yml create mode 100644 src/test/resources/mcp-server-sse-mode.yml create mode 100644 src/test/resources/mcp-server.yml diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/McpServers.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/McpServers.java index 3256988..0cf5c0b 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/McpServers.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/McpServers.java @@ -18,6 +18,7 @@ import io.modelcontextprotocol.server.transport.HttpServletSseServerTransportProvider; import io.modelcontextprotocol.server.transport.StdioServerTransportProvider; import io.modelcontextprotocol.spec.McpServerTransportProvider; +import io.modelcontextprotocol.util.Assert; import org.reflections.Reflections; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -83,32 +84,23 @@ public void startSyncSseServer(McpSseServerInfo serverInfo) { } public void startServer(String configFileName) { - McpServerConfiguration configuration = loadConfiguration(configFileName); - if (configuration.enabled()) { - startServerWith(configuration); - } else { - logger.info("MCP server is disabled."); - } - } - - public void startServer() { - // Load configuration from default file - startServer(null); - } - - private McpServerConfiguration loadConfiguration(String configFileName) { + Assert.notNull(configFileName, "configFileName must not be null"); YamlConfigurationLoader configurationLoader = new YamlConfigurationLoader(); McpServerConfiguration configuration; try { - if (configFileName == null || configFileName.isBlank()) { - configuration = configurationLoader.loadConfiguration(); + configuration = configurationLoader.load(configFileName); + if (configuration.enabled()) { + startServerWith(configuration); } else { - configuration = configurationLoader.load(configFileName); + logger.info("MCP server is disabled."); } } catch (IOException e) { - throw new McpServerException("Error loading configuration file", e); + throw new McpServerException("Error loading configuration file: " + e.getMessage(), e); } - return configuration; + } + + public void startServer() { + startServer("mcp-server.yml"); } private void startServerWith(McpServerConfiguration configuration) { diff --git a/src/test/java/com/github/codeboyzhou/mcp/declarative/McpServersTest.java b/src/test/java/com/github/codeboyzhou/mcp/declarative/McpServersTest.java index e602853..3df8426 100644 --- a/src/test/java/com/github/codeboyzhou/mcp/declarative/McpServersTest.java +++ b/src/test/java/com/github/codeboyzhou/mcp/declarative/McpServersTest.java @@ -1,6 +1,7 @@ package com.github.codeboyzhou.mcp.declarative; import com.github.codeboyzhou.mcp.declarative.annotation.McpTools; +import com.github.codeboyzhou.mcp.declarative.exception.McpServerException; import com.github.codeboyzhou.mcp.declarative.server.McpServerInfo; import com.github.codeboyzhou.mcp.declarative.server.McpSseServerInfo; import com.github.codeboyzhou.mcp.declarative.server.TestMcpComponentScanBasePackageClass; @@ -9,6 +10,7 @@ import com.github.codeboyzhou.mcp.declarative.server.TestMcpComponentScanIsNull; import com.github.codeboyzhou.mcp.declarative.server.TestMcpTools; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -23,6 +25,7 @@ import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; class McpServersTest { @@ -30,6 +33,11 @@ class McpServersTest { Reflections reflections; + @BeforeEach + void setUp() { + System.setProperty("mcp.declarative.java.sdk.testing", "true"); + } + @AfterEach void tearDown() throws NoSuchFieldException, IllegalAccessException { reflections = getReflectionsField(); @@ -68,7 +76,6 @@ void testStartSyncStdioServer() { @Test void testStartSyncSseServer() { - System.setProperty("mcp.declarative.java.sdk.testing", "true"); McpServers servers = McpServers.run(TestMcpComponentScanIsNull.class, EMPTY_ARGS); assertDoesNotThrow(() -> { McpSseServerInfo serverInfo = McpSseServerInfo.builder() @@ -85,6 +92,37 @@ void testStartSyncSseServer() { }); } + @Test + void testStartServer() { + assertDoesNotThrow(() -> { + McpServers servers = McpServers.run(TestMcpComponentScanIsNull.class, EMPTY_ARGS); + servers.startServer(); + }); + } + + @ParameterizedTest + @ValueSource(strings = { + "mcp-server.yml", + "mcp-server-async.yml", + "mcp-server-sse-mode.yml", + "mcp-server-not-enabled.yml" + }) + void testStartServerWithConfigFileName(String configFileName) { + assertDoesNotThrow(() -> { + McpServers servers = McpServers.run(TestMcpComponentScanIsNull.class, EMPTY_ARGS); + servers.startServer(configFileName); + }); + } + + @Test + void testStartServerWithInvalidConfigFileName() { + McpServerException e = assertThrows(McpServerException.class, () -> { + McpServers servers = McpServers.run(TestMcpComponentScanIsNull.class, EMPTY_ARGS); + servers.startServer("mcp-server-not-exist.yml"); + }); + assertEquals("Error loading configuration file: mcp-server-not-exist.yml", e.getMessage()); + } + private Reflections getReflectionsField() throws NoSuchFieldException, IllegalAccessException { Field reflectionsField = McpServers.class.getDeclaredField("reflections"); reflectionsField.setAccessible(true); diff --git a/src/test/resources/mcp-server-async.yml b/src/test/resources/mcp-server-async.yml new file mode 100644 index 0000000..ae432b3 --- /dev/null +++ b/src/test/resources/mcp-server-async.yml @@ -0,0 +1,7 @@ +enabled: true +stdio: true +name: mcp-stdio-server +version: 1.0.0 +instructions: A simple server that uses stdio to communicate with the client +request-timeout: 30000 +type: ASYNC diff --git a/src/test/resources/mcp-server-not-enabled.yml b/src/test/resources/mcp-server-not-enabled.yml new file mode 100644 index 0000000..9e6acf0 --- /dev/null +++ b/src/test/resources/mcp-server-not-enabled.yml @@ -0,0 +1,7 @@ +enabled: false +stdio: true +name: mcp-stdio-server +version: 1.0.0 +instructions: A simple server that uses stdio to communicate with the client +request-timeout: 30000 +type: SYNC diff --git a/src/test/resources/mcp-server-sse-mode.yml b/src/test/resources/mcp-server-sse-mode.yml new file mode 100644 index 0000000..4491168 --- /dev/null +++ b/src/test/resources/mcp-server-sse-mode.yml @@ -0,0 +1,7 @@ +enabled: true +stdio: false +name: mcp-stdio-server +version: 1.0.0 +instructions: A simple server that uses stdio to communicate with the client +request-timeout: 30000 +type: SYNC diff --git a/src/test/resources/mcp-server.yml b/src/test/resources/mcp-server.yml new file mode 100644 index 0000000..c8a120b --- /dev/null +++ b/src/test/resources/mcp-server.yml @@ -0,0 +1,7 @@ +enabled: true +stdio: true +name: mcp-stdio-server +version: 1.0.0 +instructions: A simple server that uses stdio to communicate with the client +request-timeout: 30000 +type: SYNC From 1daca394ceb7deb3ca82b03de79434c2aaea47fe Mon Sep 17 00:00:00 2001 From: codeboyzhou Date: Mon, 21 Apr 2025 00:07:39 +0800 Subject: [PATCH 14/57] feat(test): Add TestMcpPrompts class --- .../declarative/server/TestMcpPrompts.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 src/test/java/com/github/codeboyzhou/mcp/declarative/server/TestMcpPrompts.java diff --git a/src/test/java/com/github/codeboyzhou/mcp/declarative/server/TestMcpPrompts.java b/src/test/java/com/github/codeboyzhou/mcp/declarative/server/TestMcpPrompts.java new file mode 100644 index 0000000..8c0b49e --- /dev/null +++ b/src/test/java/com/github/codeboyzhou/mcp/declarative/server/TestMcpPrompts.java @@ -0,0 +1,26 @@ +package com.github.codeboyzhou.mcp.declarative.server; + +import com.github.codeboyzhou.mcp.declarative.annotation.McpPrompt; +import com.github.codeboyzhou.mcp.declarative.annotation.McpPromptParam; +import com.github.codeboyzhou.mcp.declarative.annotation.McpPrompts; + +@McpPrompts +public class TestMcpPrompts { + + @McpPrompt(name = "prompt1", description = "prompt1") + public static String prompt1( + @McpPromptParam(name = "name", description = "name", required = true) String name, + @McpPromptParam(name = "version", description = "version", required = true) String version + ) { + return "Hello " + name + ", I am " + version; + } + + @McpPrompt(description = "prompt2") + public static String prompt2( + @McpPromptParam(name = "name", description = "name") String name, + @McpPromptParam(name = "version", description = "version") String version + ) { + return "Hello " + name + ", I am " + version; + } + +} From 84f70f4920c74102a78c80f4c749f035b49a3980 Mon Sep 17 00:00:00 2001 From: codeboyzhou Date: Mon, 21 Apr 2025 00:28:46 +0800 Subject: [PATCH 15/57] refactor(configuration): Improve server default configuration loading --- .../codeboyzhou/mcp/declarative/McpServers.java | 4 +++- .../configuration/YamlConfigurationLoader.java | 15 +++++++-------- .../mcp/declarative/McpServersTest.java | 1 - src/test/resources/mcp-server.yml | 7 ------- 4 files changed, 10 insertions(+), 17 deletions(-) delete mode 100644 src/test/resources/mcp-server.yml diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/McpServers.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/McpServers.java index 0cf5c0b..4231b4a 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/McpServers.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/McpServers.java @@ -100,7 +100,9 @@ public void startServer(String configFileName) { } public void startServer() { - startServer("mcp-server.yml"); + YamlConfigurationLoader configurationLoader = new YamlConfigurationLoader(); + McpServerConfiguration configuration = configurationLoader.loadConfiguration(); + startServerWith(configuration); } private void startServerWith(McpServerConfiguration configuration) { diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/configuration/YamlConfigurationLoader.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/configuration/YamlConfigurationLoader.java index 9d4c565..5dcfec5 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/configuration/YamlConfigurationLoader.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/configuration/YamlConfigurationLoader.java @@ -10,7 +10,6 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.nio.file.NoSuchFileException; -import java.util.Objects; import static java.util.stream.Collectors.joining; @@ -22,14 +21,14 @@ public class YamlConfigurationLoader { public McpServerConfiguration loadConfiguration() { try { - McpServerConfiguration configuration = load("mcp-server.yml"); - if (configuration == null) { - configuration = load("mcp-server.yaml"); - } - return Objects.requireNonNullElseGet(configuration, McpServerConfiguration::defaultConfiguration); + return load("mcp-server.yml"); } catch (IOException e) { - logger.error("Error loading configuration file, will use default configuration", e); - return McpServerConfiguration.defaultConfiguration(); + try { + return load("mcp-server.yaml"); + } catch (IOException ex) { + logger.warn("The mcp-server.yml and mcp-server.yaml were not found, will use default configuration"); + return McpServerConfiguration.defaultConfiguration(); + } } } diff --git a/src/test/java/com/github/codeboyzhou/mcp/declarative/McpServersTest.java b/src/test/java/com/github/codeboyzhou/mcp/declarative/McpServersTest.java index 3df8426..4453af9 100644 --- a/src/test/java/com/github/codeboyzhou/mcp/declarative/McpServersTest.java +++ b/src/test/java/com/github/codeboyzhou/mcp/declarative/McpServersTest.java @@ -102,7 +102,6 @@ void testStartServer() { @ParameterizedTest @ValueSource(strings = { - "mcp-server.yml", "mcp-server-async.yml", "mcp-server-sse-mode.yml", "mcp-server-not-enabled.yml" diff --git a/src/test/resources/mcp-server.yml b/src/test/resources/mcp-server.yml deleted file mode 100644 index c8a120b..0000000 --- a/src/test/resources/mcp-server.yml +++ /dev/null @@ -1,7 +0,0 @@ -enabled: true -stdio: true -name: mcp-stdio-server -version: 1.0.0 -instructions: A simple server that uses stdio to communicate with the client -request-timeout: 30000 -type: SYNC From 53515ae3b447e276433a6b32d145f7b599afdc1d Mon Sep 17 00:00:00 2001 From: codeboyzhou Date: Tue, 22 Apr 2025 21:32:27 +0800 Subject: [PATCH 16/57] feat(server): Add default values for MCP server info --- .../codeboyzhou/mcp/declarative/server/McpServerInfo.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpServerInfo.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpServerInfo.java index 1ac266a..d59317d 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpServerInfo.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpServerInfo.java @@ -42,13 +42,13 @@ public Duration requestTimeout() { @SuppressWarnings("unchecked") public static class Builder> { - protected String name; + protected String name = "mcp-server"; - protected String version; + protected String version = "1.0.0"; - protected String instructions; + protected String instructions = ""; - protected Duration requestTimeout; + protected Duration requestTimeout = Duration.ofSeconds(10); protected T self() { return (T) this; From ea0b4689a88622df82a30a08827fcde3dea2ba47 Mon Sep 17 00:00:00 2001 From: codeboyzhou Date: Fri, 16 May 2025 23:37:03 +0800 Subject: [PATCH 17/57] chore(pom): Update mcp-sdk.version from 0.10.0-SNAPSHOT to 0.10.0 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 13e50b0..cc6fd9b 100644 --- a/pom.xml +++ b/pom.xml @@ -55,7 +55,7 @@ 12.0.18 5.10.2 1.5.18 - 0.10.0-SNAPSHOT + 0.10.0 0.10.2 From b3abf884ab6ac8347c28b16cd5a8417153b34309 Mon Sep 17 00:00:00 2001 From: codeboyzhou Date: Sun, 18 May 2025 14:55:42 +0800 Subject: [PATCH 18/57] feat(tool): Add support for complex JSON schema definitions --- .../annotation/McpJsonSchemaDefinition.java | 11 ++++ .../McpJsonSchemaDefinitionProperty.java | 18 ++++++ .../server/McpSyncServerToolRegister.java | 55 ++++++++++++++++--- .../declarative/util/ReflectionHelper.java | 23 ++++++-- .../mcp/declarative/util/StringHelper.java | 7 +++ .../server/TestMcpToolComplexJsonSchema.java | 13 +++++ .../mcp/declarative/server/TestMcpTools.java | 11 ++++ 7 files changed, 125 insertions(+), 13 deletions(-) create mode 100644 src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpJsonSchemaDefinition.java create mode 100644 src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpJsonSchemaDefinitionProperty.java create mode 100644 src/main/java/com/github/codeboyzhou/mcp/declarative/util/StringHelper.java create mode 100644 src/test/java/com/github/codeboyzhou/mcp/declarative/server/TestMcpToolComplexJsonSchema.java diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpJsonSchemaDefinition.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpJsonSchemaDefinition.java new file mode 100644 index 0000000..f3fdc70 --- /dev/null +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpJsonSchemaDefinition.java @@ -0,0 +1,11 @@ +package com.github.codeboyzhou.mcp.declarative.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface McpJsonSchemaDefinition { +} diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpJsonSchemaDefinitionProperty.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpJsonSchemaDefinitionProperty.java new file mode 100644 index 0000000..f994ba6 --- /dev/null +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpJsonSchemaDefinitionProperty.java @@ -0,0 +1,18 @@ +package com.github.codeboyzhou.mcp.declarative.annotation; + +import com.github.codeboyzhou.mcp.declarative.util.StringHelper; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface McpJsonSchemaDefinitionProperty { + String name() default StringHelper.EMPTY; + + String description(); + + boolean required() default false; +} diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerToolRegister.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerToolRegister.java index a10d80c..dc61469 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerToolRegister.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerToolRegister.java @@ -1,5 +1,7 @@ package com.github.codeboyzhou.mcp.declarative.server; +import com.github.codeboyzhou.mcp.declarative.annotation.McpJsonSchemaDefinition; +import com.github.codeboyzhou.mcp.declarative.annotation.McpJsonSchemaDefinitionProperty; import com.github.codeboyzhou.mcp.declarative.annotation.McpTool; import com.github.codeboyzhou.mcp.declarative.annotation.McpToolParam; import com.github.codeboyzhou.mcp.declarative.util.ReflectionHelper; @@ -64,26 +66,65 @@ public McpServerFeatures.SyncToolSpecification createComponentFrom(Class claz private McpSchema.JsonSchema createJsonSchema(Method method) { Map properties = new HashMap<>(); + Map definitions = new HashMap<>(); List required = new ArrayList<>(); Set parameters = ReflectionHelper.getParametersAnnotatedWith(method, McpToolParam.class); for (Parameter parameter : parameters) { McpToolParam toolParam = parameter.getAnnotation(McpToolParam.class); final String parameterName = toolParam.name(); - final String parameterType = parameter.getType().getName().toLowerCase(); + Class parameterType = parameter.getType(); + Map property = new HashMap<>(); - Map parameterProperties = Map.of( - "type", parameterType, - "description", toolParam.description() - ); - properties.put(parameterName, parameterProperties); + if (parameterType.getAnnotation(McpJsonSchemaDefinition.class) == null) { + property.put("type", parameterType.getName().toLowerCase()); + property.put("description", toolParam.description()); + } else { + final String parameterTypeSimpleName = parameterType.getSimpleName(); + property.put("$ref", "#/definitions/" + parameterTypeSimpleName); + Map definition = createJsonSchemaDefinition(parameterType); + definitions.put(parameterTypeSimpleName, definition); + } + properties.put(parameterName, property); if (toolParam.required()) { required.add(parameterName); } } - return new McpSchema.JsonSchema(OBJECT_TYPE_NAME, properties, required, false); + final boolean hasAdditionalProperties = false; + return new McpSchema.JsonSchema(OBJECT_TYPE_NAME, properties, required, hasAdditionalProperties, definitions, definitions); + } + + private Map createJsonSchemaDefinition(Class definitionClass) { + Map definitionJsonSchema = new HashMap<>(); + definitionJsonSchema.put("type", OBJECT_TYPE_NAME); + + Map properties = new HashMap<>(); + List required = new ArrayList<>(); + + ReflectionHelper.doWithFields(definitionClass, field -> { + McpJsonSchemaDefinitionProperty property = field.getAnnotation(McpJsonSchemaDefinitionProperty.class); + if (property == null) { + return; + } + + Map fieldProperties = new HashMap<>(); + fieldProperties.put("type", field.getType().getName().toLowerCase()); + fieldProperties.put("description", property.description()); + + final String fieldName = property.name().isBlank() ? field.getName() : property.name(); + properties.put(fieldName, fieldProperties); + + if (property.required()) { + required.add(fieldName); + } + }); + + definitionJsonSchema.put("properties", properties); + definitionJsonSchema.put("required", required); + + return definitionJsonSchema; } } diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/util/ReflectionHelper.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/util/ReflectionHelper.java index efa3e79..40ce6d0 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/util/ReflectionHelper.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/util/ReflectionHelper.java @@ -3,11 +3,13 @@ import io.modelcontextprotocol.spec.McpSchema; import java.lang.annotation.Annotation; +import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Parameter; import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; +import java.util.function.Consumer; import static java.util.stream.Collectors.toSet; @@ -23,6 +25,13 @@ public static Set getParametersAnnotatedWith(Method method, Class p.isAnnotationPresent(annotation)).collect(toSet()); } + public static void doWithFields(Class clazz, Consumer consumer) { + Field[] declaredFields = clazz.getDeclaredFields(); + for (Field field : declaredFields) { + consumer.accept(field); + } + } + public static Object invokeMethod(Class clazz, Method method) throws Exception { Object object = clazz.getDeclaredConstructor().newInstance(); return method.invoke(object); @@ -48,14 +57,16 @@ private static Map asTypedParameters(McpSchema.JsonSchema schema Object parameterValue = parameters.get(parameterName); if (parameterValue == null) { Map map = (Map) parameterProperties; - final String parameterType = map.get("type").toString(); - if (isTypeOf(String.class, parameterType)) { - typedParameters.put(parameterName, ""); - } else if (isTypeOf(Integer.class, parameterType)) { + final String jsonSchemaType = map.getOrDefault("type", StringHelper.EMPTY).toString(); + if (jsonSchemaType.isEmpty()) { + typedParameters.put(parameterName, null); + } else if (isTypeOf(String.class, jsonSchemaType)) { + typedParameters.put(parameterName, StringHelper.EMPTY); + } else if (isTypeOf(Integer.class, jsonSchemaType)) { typedParameters.put(parameterName, 0); - } else if (isTypeOf(Number.class, parameterType)) { + } else if (isTypeOf(Number.class, jsonSchemaType)) { typedParameters.put(parameterName, 0.0); - } else if (isTypeOf(Boolean.class, parameterType)) { + } else if (isTypeOf(Boolean.class, jsonSchemaType)) { typedParameters.put(parameterName, false); } } else { diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/util/StringHelper.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/util/StringHelper.java new file mode 100644 index 0000000..a1915e0 --- /dev/null +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/util/StringHelper.java @@ -0,0 +1,7 @@ +package com.github.codeboyzhou.mcp.declarative.util; + +public final class StringHelper { + + public static final String EMPTY = ""; + +} diff --git a/src/test/java/com/github/codeboyzhou/mcp/declarative/server/TestMcpToolComplexJsonSchema.java b/src/test/java/com/github/codeboyzhou/mcp/declarative/server/TestMcpToolComplexJsonSchema.java new file mode 100644 index 0000000..dcb11bb --- /dev/null +++ b/src/test/java/com/github/codeboyzhou/mcp/declarative/server/TestMcpToolComplexJsonSchema.java @@ -0,0 +1,13 @@ +package com.github.codeboyzhou.mcp.declarative.server; + +import com.github.codeboyzhou.mcp.declarative.annotation.McpJsonSchemaDefinition; +import com.github.codeboyzhou.mcp.declarative.annotation.McpJsonSchemaDefinitionProperty; + +@McpJsonSchemaDefinition +public record TestMcpToolComplexJsonSchema( + @McpJsonSchemaDefinitionProperty(name = "username", description = "username", required = true) String name, + @McpJsonSchemaDefinitionProperty(description = "country") String country, + String school // for testing property without annotation +) { + +} diff --git a/src/test/java/com/github/codeboyzhou/mcp/declarative/server/TestMcpTools.java b/src/test/java/com/github/codeboyzhou/mcp/declarative/server/TestMcpTools.java index 571d7c9..39e2a83 100644 --- a/src/test/java/com/github/codeboyzhou/mcp/declarative/server/TestMcpTools.java +++ b/src/test/java/com/github/codeboyzhou/mcp/declarative/server/TestMcpTools.java @@ -7,6 +7,7 @@ @McpTools public class TestMcpTools { + @SuppressWarnings("unused") @McpTool(name = "tool1", description = "tool1") public static String tool1( @McpToolParam(name = "name", description = "name", required = true) String name, @@ -15,6 +16,7 @@ public static String tool1( return "Hello " + name + ", I am " + version; } + @SuppressWarnings("unused") @McpTool(description = "tool2") public static String tool2( @McpToolParam(name = "name", description = "name") String name, @@ -23,4 +25,13 @@ public static String tool2( return "Hello " + name + ", I am " + version; } + @SuppressWarnings("unused") + @McpTool(description = "tool3") + public static String tool3( + @McpToolParam(name = "complexJsonSchema", description = "complexJsonSchema") + TestMcpToolComplexJsonSchema complexJsonSchema + ) { + return String.format("Hello, my name is %s, I am from %s", complexJsonSchema.name(), complexJsonSchema.country()); + } + } From a8d47889033b3d121a4e5acfe6001f9111b1bf6a Mon Sep 17 00:00:00 2001 From: codeboyzhou Date: Sun, 18 May 2025 15:04:25 +0800 Subject: [PATCH 19/57] feat(server): Add JSON serialization utility and improve debug logging --- .../server/McpSyncServerPromptRegister.java | 2 ++ .../server/McpSyncServerResourceRegister.java | 2 ++ .../server/McpSyncServerToolRegister.java | 2 ++ .../mcp/declarative/util/JsonHelper.java | 19 +++++++++++++++++++ 4 files changed, 25 insertions(+) create mode 100644 src/main/java/com/github/codeboyzhou/mcp/declarative/util/JsonHelper.java diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerPromptRegister.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerPromptRegister.java index 520b637..6fa09cd 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerPromptRegister.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerPromptRegister.java @@ -2,6 +2,7 @@ import com.github.codeboyzhou.mcp.declarative.annotation.McpPrompt; import com.github.codeboyzhou.mcp.declarative.annotation.McpPromptParam; +import com.github.codeboyzhou.mcp.declarative.util.JsonHelper; import com.github.codeboyzhou.mcp.declarative.util.ReflectionHelper; import io.modelcontextprotocol.server.McpServerFeatures; import io.modelcontextprotocol.server.McpSyncServer; @@ -44,6 +45,7 @@ public McpServerFeatures.SyncPromptSpecification createComponentFrom(Class cl final String description = promptMethod.description(); List promptArguments = createPromptArguments(method); McpSchema.Prompt prompt = new McpSchema.Prompt(name, description, promptArguments); + logger.debug("Registering prompt: {}", JsonHelper.toJson(prompt)); return new McpServerFeatures.SyncPromptSpecification(prompt, (exchange, request) -> { Object result; try { diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerResourceRegister.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerResourceRegister.java index ae715ca..dad8176 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerResourceRegister.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerResourceRegister.java @@ -1,6 +1,7 @@ package com.github.codeboyzhou.mcp.declarative.server; import com.github.codeboyzhou.mcp.declarative.annotation.McpResource; +import com.github.codeboyzhou.mcp.declarative.util.JsonHelper; import com.github.codeboyzhou.mcp.declarative.util.ReflectionHelper; import io.modelcontextprotocol.server.McpServerFeatures; import io.modelcontextprotocol.server.McpSyncServer; @@ -42,6 +43,7 @@ public McpServerFeatures.SyncResourceSpecification createComponentFrom(Class res.uri(), name, res.description(), res.mimeType(), new McpSchema.Annotations(List.of(res.roles()), res.priority()) ); + logger.debug("Registering resource: {}", JsonHelper.toJson(resource)); return new McpServerFeatures.SyncResourceSpecification(resource, (exchange, request) -> { Object result; try { diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerToolRegister.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerToolRegister.java index dc61469..56c6fd9 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerToolRegister.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerToolRegister.java @@ -4,6 +4,7 @@ import com.github.codeboyzhou.mcp.declarative.annotation.McpJsonSchemaDefinitionProperty; import com.github.codeboyzhou.mcp.declarative.annotation.McpTool; import com.github.codeboyzhou.mcp.declarative.annotation.McpToolParam; +import com.github.codeboyzhou.mcp.declarative.util.JsonHelper; import com.github.codeboyzhou.mcp.declarative.util.ReflectionHelper; import io.modelcontextprotocol.server.McpServerFeatures; import io.modelcontextprotocol.server.McpSyncServer; @@ -49,6 +50,7 @@ public McpServerFeatures.SyncToolSpecification createComponentFrom(Class claz McpSchema.JsonSchema paramSchema = createJsonSchema(method); final String name = toolMethod.name().isBlank() ? method.getName() : toolMethod.name(); McpSchema.Tool tool = new McpSchema.Tool(name, toolMethod.description(), paramSchema); + logger.debug("Registering tool: {}", JsonHelper.toJson(tool)); return new McpServerFeatures.SyncToolSpecification(tool, (exchange, params) -> { Object result; boolean isError = false; diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/util/JsonHelper.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/util/JsonHelper.java new file mode 100644 index 0000000..29258a4 --- /dev/null +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/util/JsonHelper.java @@ -0,0 +1,19 @@ +package com.github.codeboyzhou.mcp.declarative.util; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.codeboyzhou.mcp.declarative.exception.McpServerException; + +public final class JsonHelper { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + public static String toJson(Object object) { + try { + return MAPPER.writeValueAsString(object); + } catch (JsonProcessingException e) { + throw new McpServerException("Error converting object to JSON", e); + } + } + +} From c4d94637ecd8b36f1bab70e846d49442354f2e7a Mon Sep 17 00:00:00 2001 From: codeboyzhou Date: Sun, 18 May 2025 15:37:27 +0800 Subject: [PATCH 20/57] polish(main): Some minor enhancements --- .../annotation/McpComponentScan.java | 4 +++- .../mcp/declarative/annotation/McpPrompt.java | 4 +++- .../declarative/annotation/McpResource.java | 3 ++- .../mcp/declarative/annotation/McpTool.java | 4 +++- .../configuration/McpServerConfiguration.java | 3 ++- .../mcp/declarative/server/McpServerInfo.java | 4 +++- .../mcp/declarative/McpServersTest.java | 19 +++++++++++++++++-- .../declarative/server/TestMcpPrompts.java | 14 ++++++++------ .../declarative/server/TestMcpResources.java | 6 ++++-- .../mcp/declarative/server/TestMcpTools.java | 15 ++++++++------- 10 files changed, 53 insertions(+), 23 deletions(-) diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpComponentScan.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpComponentScan.java index b6dacee..b4d1d28 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpComponentScan.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpComponentScan.java @@ -1,5 +1,7 @@ package com.github.codeboyzhou.mcp.declarative.annotation; +import com.github.codeboyzhou.mcp.declarative.util.StringHelper; + import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -8,7 +10,7 @@ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface McpComponentScan { - String basePackage() default ""; + String basePackage() default StringHelper.EMPTY; Class basePackageClass() default Object.class; } diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpPrompt.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpPrompt.java index bf63491..afc21a5 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpPrompt.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpPrompt.java @@ -1,5 +1,7 @@ package com.github.codeboyzhou.mcp.declarative.annotation; +import com.github.codeboyzhou.mcp.declarative.util.StringHelper; + import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -9,7 +11,7 @@ @Retention(RetentionPolicy.RUNTIME) public @interface McpPrompt { - String name() default ""; + String name() default StringHelper.EMPTY; String description(); } diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpResource.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpResource.java index a81e5b6..0b3caf7 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpResource.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpResource.java @@ -1,5 +1,6 @@ package com.github.codeboyzhou.mcp.declarative.annotation; +import com.github.codeboyzhou.mcp.declarative.util.StringHelper; import io.modelcontextprotocol.spec.McpSchema; import java.lang.annotation.ElementType; @@ -12,7 +13,7 @@ public @interface McpResource { String uri(); - String name() default ""; + String name() default StringHelper.EMPTY; String description(); diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpTool.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpTool.java index ea52b23..307a551 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpTool.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpTool.java @@ -1,5 +1,7 @@ package com.github.codeboyzhou.mcp.declarative.annotation; +import com.github.codeboyzhou.mcp.declarative.util.StringHelper; + import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -9,7 +11,7 @@ @Retention(RetentionPolicy.RUNTIME) public @interface McpTool { - String name() default ""; + String name() default StringHelper.EMPTY; String description(); } diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/configuration/McpServerConfiguration.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/configuration/McpServerConfiguration.java index a5c00a0..c0bcac0 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/configuration/McpServerConfiguration.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/configuration/McpServerConfiguration.java @@ -1,6 +1,7 @@ package com.github.codeboyzhou.mcp.declarative.configuration; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.codeboyzhou.mcp.declarative.util.StringHelper; import java.util.Map; @@ -37,7 +38,7 @@ public static McpServerConfiguration defaultConfiguration() { Map.of(), "/mcp/message", "/sse", - "", + StringHelper.EMPTY, 8080 ); } diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpServerInfo.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpServerInfo.java index d59317d..0e6b13c 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpServerInfo.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpServerInfo.java @@ -1,5 +1,7 @@ package com.github.codeboyzhou.mcp.declarative.server; +import com.github.codeboyzhou.mcp.declarative.util.StringHelper; + import java.time.Duration; public class McpServerInfo { @@ -46,7 +48,7 @@ public static class Builder> { protected String version = "1.0.0"; - protected String instructions = ""; + protected String instructions = StringHelper.EMPTY; protected Duration requestTimeout = Duration.ofSeconds(10); diff --git a/src/test/java/com/github/codeboyzhou/mcp/declarative/McpServersTest.java b/src/test/java/com/github/codeboyzhou/mcp/declarative/McpServersTest.java index 4453af9..7ea954b 100644 --- a/src/test/java/com/github/codeboyzhou/mcp/declarative/McpServersTest.java +++ b/src/test/java/com/github/codeboyzhou/mcp/declarative/McpServersTest.java @@ -1,5 +1,7 @@ package com.github.codeboyzhou.mcp.declarative; +import com.github.codeboyzhou.mcp.declarative.annotation.McpPrompts; +import com.github.codeboyzhou.mcp.declarative.annotation.McpResources; import com.github.codeboyzhou.mcp.declarative.annotation.McpTools; import com.github.codeboyzhou.mcp.declarative.exception.McpServerException; import com.github.codeboyzhou.mcp.declarative.server.McpServerInfo; @@ -8,6 +10,8 @@ import com.github.codeboyzhou.mcp.declarative.server.TestMcpComponentScanBasePackageString; import com.github.codeboyzhou.mcp.declarative.server.TestMcpComponentScanDefault; import com.github.codeboyzhou.mcp.declarative.server.TestMcpComponentScanIsNull; +import com.github.codeboyzhou.mcp.declarative.server.TestMcpPrompts; +import com.github.codeboyzhou.mcp.declarative.server.TestMcpResources; import com.github.codeboyzhou.mcp.declarative.server.TestMcpTools; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -42,10 +46,21 @@ void setUp() { void tearDown() throws NoSuchFieldException, IllegalAccessException { reflections = getReflectionsField(); assertNotNull(reflections); + Map> scannedClasses = reflections.getStore().get(Scanners.TypesAnnotated.name()); + + Set scannedPromptClass = scannedClasses.get(McpPrompts.class.getName()); + assertEquals(1, scannedPromptClass.size()); + assertEquals(scannedPromptClass.iterator().next(), TestMcpPrompts.class.getName()); + + Set scannedResourceClass = scannedClasses.get(McpResources.class.getName()); + assertEquals(1, scannedResourceClass.size()); + assertEquals(scannedResourceClass.iterator().next(), TestMcpResources.class.getName()); + Set scannedToolClass = scannedClasses.get(McpTools.class.getName()); assertEquals(1, scannedToolClass.size()); assertEquals(scannedToolClass.iterator().next(), TestMcpTools.class.getName()); + reflections = null; } @@ -81,10 +96,10 @@ void testStartSyncSseServer() { McpSseServerInfo serverInfo = McpSseServerInfo.builder() .instructions("test-mcp-sync-sse-server-instructions") .requestTimeout(Duration.ofSeconds(10)) - .baseUrl("http://127.0.0.1:8080") + .baseUrl("http://127.0.0.1:8081") .messageEndpoint("/message") .sseEndpoint("/sse") - .port(8080) + .port(8081) .name("test-mcp-sync-sse-server") .version("1.0.0") .build(); diff --git a/src/test/java/com/github/codeboyzhou/mcp/declarative/server/TestMcpPrompts.java b/src/test/java/com/github/codeboyzhou/mcp/declarative/server/TestMcpPrompts.java index 8c0b49e..b805bc9 100644 --- a/src/test/java/com/github/codeboyzhou/mcp/declarative/server/TestMcpPrompts.java +++ b/src/test/java/com/github/codeboyzhou/mcp/declarative/server/TestMcpPrompts.java @@ -7,20 +7,22 @@ @McpPrompts public class TestMcpPrompts { + @SuppressWarnings("unused") @McpPrompt(name = "prompt1", description = "prompt1") public static String prompt1( - @McpPromptParam(name = "name", description = "name", required = true) String name, - @McpPromptParam(name = "version", description = "version", required = true) String version + @McpPromptParam(name = "argument1", description = "argument1", required = true) String argument1, + @McpPromptParam(name = "argument2", description = "argument2", required = true) String argument2 ) { - return "Hello " + name + ", I am " + version; + return String.format("This is prompt1, required argument1: %s, required argument2: %s", argument1, argument2); } + @SuppressWarnings("unused") @McpPrompt(description = "prompt2") public static String prompt2( - @McpPromptParam(name = "name", description = "name") String name, - @McpPromptParam(name = "version", description = "version") String version + @McpPromptParam(name = "argument1", description = "argument1") String argument1, + @McpPromptParam(name = "argument2", description = "argument2") String argument2 ) { - return "Hello " + name + ", I am " + version; + return String.format("This is prompt2, optional argument1: %s, optional argument2: %s", argument1, argument2); } } diff --git a/src/test/java/com/github/codeboyzhou/mcp/declarative/server/TestMcpResources.java b/src/test/java/com/github/codeboyzhou/mcp/declarative/server/TestMcpResources.java index 3316772..57be6c8 100644 --- a/src/test/java/com/github/codeboyzhou/mcp/declarative/server/TestMcpResources.java +++ b/src/test/java/com/github/codeboyzhou/mcp/declarative/server/TestMcpResources.java @@ -6,14 +6,16 @@ @McpResources public class TestMcpResources { + @SuppressWarnings("unused") @McpResource(uri = "test://resource1", name = "resource1", description = "resource1") public String resource1() { - return System.lineSeparator(); + return "This is resource1 content"; } + @SuppressWarnings("unused") @McpResource(uri = "test://resource2", description = "resource2") public String resource2() { - return System.lineSeparator(); + return "This is resource2 content"; } } diff --git a/src/test/java/com/github/codeboyzhou/mcp/declarative/server/TestMcpTools.java b/src/test/java/com/github/codeboyzhou/mcp/declarative/server/TestMcpTools.java index 39e2a83..c1a6e2b 100644 --- a/src/test/java/com/github/codeboyzhou/mcp/declarative/server/TestMcpTools.java +++ b/src/test/java/com/github/codeboyzhou/mcp/declarative/server/TestMcpTools.java @@ -10,19 +10,19 @@ public class TestMcpTools { @SuppressWarnings("unused") @McpTool(name = "tool1", description = "tool1") public static String tool1( - @McpToolParam(name = "name", description = "name", required = true) String name, - @McpToolParam(name = "version", description = "version", required = true) String version + @McpToolParam(name = "argument1", description = "argument1", required = true) String argument1, + @McpToolParam(name = "argument2", description = "argument2", required = true) String argument2 ) { - return "Hello " + name + ", I am " + version; + return String.format("This is tool1, required argument1: %s, required argument2: %s", argument1, argument2); } @SuppressWarnings("unused") @McpTool(description = "tool2") public static String tool2( - @McpToolParam(name = "name", description = "name") String name, - @McpToolParam(name = "version", description = "version") String version + @McpToolParam(name = "argument1", description = "argument1") String argument1, + @McpToolParam(name = "argument2", description = "argument2") String argument2 ) { - return "Hello " + name + ", I am " + version; + return String.format("This is tool2, optional argument1: %s, optional argument2: %s", argument1, argument2); } @SuppressWarnings("unused") @@ -31,7 +31,8 @@ public static String tool3( @McpToolParam(name = "complexJsonSchema", description = "complexJsonSchema") TestMcpToolComplexJsonSchema complexJsonSchema ) { - return String.format("Hello, my name is %s, I am from %s", complexJsonSchema.name(), complexJsonSchema.country()); + return String.format("This is tool3 for testing complex json schema: my name is %s, I am from %s", + complexJsonSchema.name(), complexJsonSchema.country()); } } From ea483cc3fcd39f3937c6a2248580b2926466366d Mon Sep 17 00:00:00 2001 From: steveGuRen <529543738@qq.com> Date: Mon, 19 May 2025 09:59:06 +0800 Subject: [PATCH 21/57] fix(main): The parameters order problems when invoke in MCP Inspector v0.12.0 (#2) --- .../server/McpSyncServerPromptRegister.java | 4 +-- .../server/McpSyncServerResourceRegister.java | 2 +- .../server/McpSyncServerToolRegister.java | 9 ++++--- .../declarative/util/ReflectionHelper.java | 26 ++++++++++++++----- 4 files changed, 27 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerPromptRegister.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerPromptRegister.java index 520b637..7698e8b 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerPromptRegister.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerPromptRegister.java @@ -29,7 +29,7 @@ public McpSyncServerPromptRegister(Set> promptClasses) { @Override public void registerTo(McpSyncServer server) { for (Class promptClass : promptClasses) { - Set methods = ReflectionHelper.getMethodsAnnotatedWith(promptClass, McpPrompt.class); + List methods = ReflectionHelper.getMethodsAnnotatedWith(promptClass, McpPrompt.class); for (Method method : methods) { McpServerFeatures.SyncPromptSpecification prompt = createComponentFrom(promptClass, method); server.addPrompt(prompt); @@ -59,7 +59,7 @@ public McpServerFeatures.SyncPromptSpecification createComponentFrom(Class cl } private List createPromptArguments(Method method) { - Set parameters = ReflectionHelper.getParametersAnnotatedWith(method, McpPromptParam.class); + List parameters = ReflectionHelper.getParametersAnnotatedWith(method, McpPromptParam.class); List promptArguments = new ArrayList<>(parameters.size()); for (Parameter parameter : parameters) { McpPromptParam promptParam = parameter.getAnnotation(McpPromptParam.class); diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerResourceRegister.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerResourceRegister.java index ae715ca..c8b8edf 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerResourceRegister.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerResourceRegister.java @@ -26,7 +26,7 @@ public McpSyncServerResourceRegister(Set> resourceClasses) { @Override public void registerTo(McpSyncServer server) { for (Class resourceClass : resourceClasses) { - Set methods = ReflectionHelper.getMethodsAnnotatedWith(resourceClass, McpResource.class); + List methods = ReflectionHelper.getMethodsAnnotatedWith(resourceClass, McpResource.class); for (Method method : methods) { McpServerFeatures.SyncResourceSpecification resource = createComponentFrom(resourceClass, method); server.addResource(resource); diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerToolRegister.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerToolRegister.java index a10d80c..8d1a7c5 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerToolRegister.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerToolRegister.java @@ -12,7 +12,7 @@ import java.lang.reflect.Method; import java.lang.reflect.Parameter; import java.util.ArrayList; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -33,7 +33,7 @@ public McpSyncServerToolRegister(Set> toolClasses) { @Override public void registerTo(McpSyncServer server) { for (Class toolClass : toolClasses) { - Set methods = ReflectionHelper.getMethodsAnnotatedWith(toolClass, McpTool.class); + List methods = ReflectionHelper.getMethodsAnnotatedWith(toolClass, McpTool.class); for (Method method : methods) { McpServerFeatures.SyncToolSpecification tool = createComponentFrom(toolClass, method); server.addTool(tool); @@ -63,10 +63,11 @@ public McpServerFeatures.SyncToolSpecification createComponentFrom(Class claz } private McpSchema.JsonSchema createJsonSchema(Method method) { - Map properties = new HashMap<>(); + //has to use linkedhashmap to make order correct + Map properties = new LinkedHashMap<>(); List required = new ArrayList<>(); - Set parameters = ReflectionHelper.getParametersAnnotatedWith(method, McpToolParam.class); + List parameters = ReflectionHelper.getParametersAnnotatedWith(method, McpToolParam.class); for (Parameter parameter : parameters) { McpToolParam toolParam = parameter.getAnnotation(McpToolParam.class); final String parameterName = toolParam.name(); diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/util/ReflectionHelper.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/util/ReflectionHelper.java index efa3e79..b876cb1 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/util/ReflectionHelper.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/util/ReflectionHelper.java @@ -5,22 +5,34 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.lang.reflect.Parameter; +import java.util.Arrays; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; -import java.util.Set; - -import static java.util.stream.Collectors.toSet; +import java.util.stream.Stream; public final class ReflectionHelper { - public static Set getMethodsAnnotatedWith(Class clazz, Class annotation) { + /** + * has to use list as result to make order correct + * @param clazz + * @param annotation + * @return + */ + public static List getMethodsAnnotatedWith(Class clazz, Class annotation) { Method[] methods = clazz.getMethods(); - return Set.of(methods).stream().filter(m -> m.isAnnotationPresent(annotation)).collect(toSet()); + return Stream.of(methods).filter(m -> m.isAnnotationPresent(annotation)).toList(); } - public static Set getParametersAnnotatedWith(Method method, Class annotation) { + /** + * has to use list as result to make order correct + * @param method + * @param annotation + * @return + */ + public static List getParametersAnnotatedWith(Method method, Class annotation) { Parameter[] parameters = method.getParameters(); - return Set.of(parameters).stream().filter(p -> p.isAnnotationPresent(annotation)).collect(toSet()); + return Stream.of(parameters).filter(p -> p.isAnnotationPresent(annotation)).toList(); } public static Object invokeMethod(Class clazz, Method method) throws Exception { From af50fdea4b59d6afe0340d3d7a5f81825abdeb9f Mon Sep 17 00:00:00 2001 From: codeboyzhou Date: Tue, 20 May 2025 17:26:33 +0800 Subject: [PATCH 22/57] feat(server): Add default values for MCP SSE server info --- .../mcp/declarative/server/McpSseServerInfo.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSseServerInfo.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSseServerInfo.java index 6886b46..4b9c150 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSseServerInfo.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSseServerInfo.java @@ -1,5 +1,7 @@ package com.github.codeboyzhou.mcp.declarative.server; +import com.github.codeboyzhou.mcp.declarative.util.StringHelper; + public class McpSseServerInfo extends McpServerInfo { private final String baseUrl; @@ -40,13 +42,13 @@ public int port() { public static class Builder extends McpServerInfo.Builder { - private String baseUrl; + private String baseUrl = StringHelper.EMPTY; - private String messageEndpoint; + private String messageEndpoint = "/mcp/message"; - private String sseEndpoint; + private String sseEndpoint = "/sse"; - private int port; + private int port = 8080; @Override protected McpSseServerInfo.Builder self() { From 737238aa0e1ea5af6070ffa1370a6510499dce92 Mon Sep 17 00:00:00 2001 From: codeboyzhou Date: Tue, 20 May 2025 17:44:27 +0800 Subject: [PATCH 23/57] fix(server): The parameter order displayed on MCP Inspector page is incorrect --- .../server/McpSyncServerPromptRegister.java | 4 ++-- .../server/McpSyncServerResourceRegister.java | 2 +- .../server/McpSyncServerToolRegister.java | 9 +++++---- .../mcp/declarative/util/ReflectionHelper.java | 13 ++++++------- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerPromptRegister.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerPromptRegister.java index 6fa09cd..00c4dfb 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerPromptRegister.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerPromptRegister.java @@ -30,7 +30,7 @@ public McpSyncServerPromptRegister(Set> promptClasses) { @Override public void registerTo(McpSyncServer server) { for (Class promptClass : promptClasses) { - Set methods = ReflectionHelper.getMethodsAnnotatedWith(promptClass, McpPrompt.class); + List methods = ReflectionHelper.getMethodsAnnotatedWith(promptClass, McpPrompt.class); for (Method method : methods) { McpServerFeatures.SyncPromptSpecification prompt = createComponentFrom(promptClass, method); server.addPrompt(prompt); @@ -61,7 +61,7 @@ public McpServerFeatures.SyncPromptSpecification createComponentFrom(Class cl } private List createPromptArguments(Method method) { - Set parameters = ReflectionHelper.getParametersAnnotatedWith(method, McpPromptParam.class); + List parameters = ReflectionHelper.getParametersAnnotatedWith(method, McpPromptParam.class); List promptArguments = new ArrayList<>(parameters.size()); for (Parameter parameter : parameters) { McpPromptParam promptParam = parameter.getAnnotation(McpPromptParam.class); diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerResourceRegister.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerResourceRegister.java index dad8176..9c62b66 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerResourceRegister.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerResourceRegister.java @@ -27,7 +27,7 @@ public McpSyncServerResourceRegister(Set> resourceClasses) { @Override public void registerTo(McpSyncServer server) { for (Class resourceClass : resourceClasses) { - Set methods = ReflectionHelper.getMethodsAnnotatedWith(resourceClass, McpResource.class); + List methods = ReflectionHelper.getMethodsAnnotatedWith(resourceClass, McpResource.class); for (Method method : methods) { McpServerFeatures.SyncResourceSpecification resource = createComponentFrom(resourceClass, method); server.addResource(resource); diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerToolRegister.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerToolRegister.java index 56c6fd9..9d1b26d 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerToolRegister.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerToolRegister.java @@ -16,6 +16,7 @@ import java.lang.reflect.Parameter; import java.util.ArrayList; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -36,7 +37,7 @@ public McpSyncServerToolRegister(Set> toolClasses) { @Override public void registerTo(McpSyncServer server) { for (Class toolClass : toolClasses) { - Set methods = ReflectionHelper.getMethodsAnnotatedWith(toolClass, McpTool.class); + List methods = ReflectionHelper.getMethodsAnnotatedWith(toolClass, McpTool.class); for (Method method : methods) { McpServerFeatures.SyncToolSpecification tool = createComponentFrom(toolClass, method); server.addTool(tool); @@ -67,11 +68,11 @@ public McpServerFeatures.SyncToolSpecification createComponentFrom(Class claz } private McpSchema.JsonSchema createJsonSchema(Method method) { - Map properties = new HashMap<>(); - Map definitions = new HashMap<>(); + Map properties = new LinkedHashMap<>(); + Map definitions = new LinkedHashMap<>(); List required = new ArrayList<>(); - Set parameters = ReflectionHelper.getParametersAnnotatedWith(method, McpToolParam.class); + List parameters = ReflectionHelper.getParametersAnnotatedWith(method, McpToolParam.class); for (Parameter parameter : parameters) { McpToolParam toolParam = parameter.getAnnotation(McpToolParam.class); final String parameterName = toolParam.name(); diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/util/ReflectionHelper.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/util/ReflectionHelper.java index 40ce6d0..549acfe 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/util/ReflectionHelper.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/util/ReflectionHelper.java @@ -7,22 +7,21 @@ import java.lang.reflect.Method; import java.lang.reflect.Parameter; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; -import java.util.Set; import java.util.function.Consumer; - -import static java.util.stream.Collectors.toSet; +import java.util.stream.Stream; public final class ReflectionHelper { - public static Set getMethodsAnnotatedWith(Class clazz, Class annotation) { + public static List getMethodsAnnotatedWith(Class clazz, Class annotation) { Method[] methods = clazz.getMethods(); - return Set.of(methods).stream().filter(m -> m.isAnnotationPresent(annotation)).collect(toSet()); + return Stream.of(methods).filter(m -> m.isAnnotationPresent(annotation)).toList(); } - public static Set getParametersAnnotatedWith(Method method, Class annotation) { + public static List getParametersAnnotatedWith(Method method, Class annotation) { Parameter[] parameters = method.getParameters(); - return Set.of(parameters).stream().filter(p -> p.isAnnotationPresent(annotation)).collect(toSet()); + return Stream.of(parameters).filter(p -> p.isAnnotationPresent(annotation)).toList(); } public static void doWithFields(Class clazz, Consumer consumer) { From aa242d2477aeda6be1faeac6cf040ca546834db5 Mon Sep 17 00:00:00 2001 From: codeboyzhou Date: Tue, 20 May 2025 18:08:52 +0800 Subject: [PATCH 24/57] test(server): Stop http server immediately in test mode for preventing port from being occupied --- .../codeboyzhou/mcp/declarative/server/McpHttpServer.java | 1 + .../github/codeboyzhou/mcp/declarative/McpServersTest.java | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpHttpServer.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpHttpServer.java index ba7849a..158534d 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpHttpServer.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpHttpServer.java @@ -77,6 +77,7 @@ public void start() { final boolean testing = Boolean.parseBoolean(System.getProperty("mcp.declarative.java.sdk.testing")); if (testing) { logger.debug("Jetty-based HTTP server is running in test mode, not waiting for HTTP server to stop"); + httpserver.stop(); return; } diff --git a/src/test/java/com/github/codeboyzhou/mcp/declarative/McpServersTest.java b/src/test/java/com/github/codeboyzhou/mcp/declarative/McpServersTest.java index 7ea954b..004c5eb 100644 --- a/src/test/java/com/github/codeboyzhou/mcp/declarative/McpServersTest.java +++ b/src/test/java/com/github/codeboyzhou/mcp/declarative/McpServersTest.java @@ -96,10 +96,10 @@ void testStartSyncSseServer() { McpSseServerInfo serverInfo = McpSseServerInfo.builder() .instructions("test-mcp-sync-sse-server-instructions") .requestTimeout(Duration.ofSeconds(10)) - .baseUrl("http://127.0.0.1:8081") + .baseUrl("http://127.0.0.1:8080") .messageEndpoint("/message") .sseEndpoint("/sse") - .port(8081) + .port(8080) .name("test-mcp-sync-sse-server") .version("1.0.0") .build(); From 8b80f91022b5c601418b4cef83ba78012ece52c8 Mon Sep 17 00:00:00 2001 From: codeboyzhou Date: Tue, 20 May 2025 22:16:29 +0800 Subject: [PATCH 25/57] polish(server): Improve type handling and parameter processing --- .../server/McpSyncServerPromptRegister.java | 2 +- .../server/McpSyncServerToolRegister.java | 6 +- .../declarative/util/ReflectionHelper.java | 65 +++++++++++++------ 3 files changed, 49 insertions(+), 24 deletions(-) diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerPromptRegister.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerPromptRegister.java index 00c4dfb..1bb5930 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerPromptRegister.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerPromptRegister.java @@ -49,7 +49,7 @@ public McpServerFeatures.SyncPromptSpecification createComponentFrom(Class cl return new McpServerFeatures.SyncPromptSpecification(prompt, (exchange, request) -> { Object result; try { - result = ReflectionHelper.invokeMethod(clazz, method, request.arguments()); + result = ReflectionHelper.invokeMethod(clazz, method, promptArguments, request.arguments()); } catch (Throwable e) { logger.error("Error invoking prompt method", e); result = e + ": " + e.getMessage(); diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerToolRegister.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerToolRegister.java index 9d1b26d..116fd7b 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerToolRegister.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerToolRegister.java @@ -80,7 +80,7 @@ private McpSchema.JsonSchema createJsonSchema(Method method) { Map property = new HashMap<>(); if (parameterType.getAnnotation(McpJsonSchemaDefinition.class) == null) { - property.put("type", parameterType.getName().toLowerCase()); + property.put("type", parameterType.getSimpleName().toLowerCase()); property.put("description", toolParam.description()); } else { final String parameterTypeSimpleName = parameterType.getSimpleName(); @@ -103,7 +103,7 @@ private Map createJsonSchemaDefinition(Class definitionClass) Map definitionJsonSchema = new HashMap<>(); definitionJsonSchema.put("type", OBJECT_TYPE_NAME); - Map properties = new HashMap<>(); + Map properties = new LinkedHashMap<>(); List required = new ArrayList<>(); ReflectionHelper.doWithFields(definitionClass, field -> { @@ -113,7 +113,7 @@ private Map createJsonSchemaDefinition(Class definitionClass) } Map fieldProperties = new HashMap<>(); - fieldProperties.put("type", field.getType().getName().toLowerCase()); + fieldProperties.put("type", field.getType().getSimpleName().toLowerCase()); fieldProperties.put("description", property.description()); final String fieldName = property.name().isBlank() ? field.getName() : property.name(); diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/util/ReflectionHelper.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/util/ReflectionHelper.java index 549acfe..d3c5370 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/util/ReflectionHelper.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/util/ReflectionHelper.java @@ -36,9 +36,10 @@ public static Object invokeMethod(Class clazz, Method method) throws Exceptio return method.invoke(object); } - public static Object invokeMethod(Class clazz, Method method, Map parameters) throws Exception { + public static Object invokeMethod(Class clazz, Method method, List arguments, Map parameters) throws Exception { Object object = clazz.getDeclaredConstructor().newInstance(); - return method.invoke(object, parameters.values().toArray()); + Map typedParameters = asTypedParameters(method, arguments, parameters); + return method.invoke(object, typedParameters.values().toArray()); } public static Object invokeMethod(Class clazz, Method method, McpSchema.JsonSchema schema, Map parameters) throws Exception { @@ -47,6 +48,36 @@ public static Object invokeMethod(Class clazz, Method method, McpSchema.JsonS return method.invoke(object, typedParameters.values().toArray()); } + private static Map asTypedParameters(Method method, List arguments, Map parameters) { + Class[] parameterTypes = method.getParameterTypes(); + Map typedParameters = new LinkedHashMap<>(parameters.size()); + + for (int i = 0, size = arguments.size(); i < size; i++) { + final String parameterName = arguments.get(i).name(); + final Object parameterValue = parameters.get(parameterName); + // Fill in a default value when the parameter is not specified + // to ensure that the parameter type is correct when calling method.invoke() + Class parameterType = parameterTypes[i]; + if (String.class == parameterType) { + typedParameters.put(parameterName, parameterValue == null ? StringHelper.EMPTY : parameterValue.toString()); + } else if (int.class == parameterType || Integer.class == parameterType) { + typedParameters.put(parameterName, parameterValue == null ? 0 : Integer.parseInt(parameterValue.toString())); + } else if (long.class == parameterType || Long.class == parameterType) { + typedParameters.put(parameterName, parameterValue == null ? 0 : Long.parseLong(parameterValue.toString())); + } else if (float.class == parameterType || Float.class == parameterType) { + typedParameters.put(parameterName, parameterValue == null ? 0.0 : Float.parseFloat(parameterValue.toString())); + } else if (double.class == parameterType || Double.class == parameterType) { + typedParameters.put(parameterName, parameterValue == null ? 0.0 : Double.parseDouble(parameterValue.toString())); + } else if (boolean.class == parameterType || Boolean.class == parameterType) { + typedParameters.put(parameterName, parameterValue != null && Boolean.parseBoolean(parameterValue.toString())); + } else { + typedParameters.put(parameterName, parameterValue); + } + } + + return typedParameters; + } + @SuppressWarnings("unchecked") private static Map asTypedParameters(McpSchema.JsonSchema schema, Map parameters) { Map properties = schema.properties(); @@ -54,20 +85,18 @@ private static Map asTypedParameters(McpSchema.JsonSchema schema properties.forEach((parameterName, parameterProperties) -> { Object parameterValue = parameters.get(parameterName); - if (parameterValue == null) { - Map map = (Map) parameterProperties; - final String jsonSchemaType = map.getOrDefault("type", StringHelper.EMPTY).toString(); - if (jsonSchemaType.isEmpty()) { - typedParameters.put(parameterName, null); - } else if (isTypeOf(String.class, jsonSchemaType)) { - typedParameters.put(parameterName, StringHelper.EMPTY); - } else if (isTypeOf(Integer.class, jsonSchemaType)) { - typedParameters.put(parameterName, 0); - } else if (isTypeOf(Number.class, jsonSchemaType)) { - typedParameters.put(parameterName, 0.0); - } else if (isTypeOf(Boolean.class, jsonSchemaType)) { - typedParameters.put(parameterName, false); - } + // Fill in a default value when the parameter is not specified + // to ensure that the parameter type is correct when calling method.invoke() + Map map = (Map) parameterProperties; + final String jsonSchemaType = map.getOrDefault("type", StringHelper.EMPTY).toString(); + if (String.class.getSimpleName().equalsIgnoreCase(jsonSchemaType)) { + typedParameters.put(parameterName, parameterValue == null ? StringHelper.EMPTY : parameterValue.toString()); + } else if (Integer.class.getSimpleName().equalsIgnoreCase(jsonSchemaType)) { + typedParameters.put(parameterName, parameterValue == null ? 0 : Integer.parseInt(parameterValue.toString())); + } else if (Number.class.getSimpleName().equalsIgnoreCase(jsonSchemaType)) { + typedParameters.put(parameterName, parameterValue == null ? 0.0 : Double.parseDouble(parameterValue.toString())); + } else if (Boolean.class.getSimpleName().equalsIgnoreCase(jsonSchemaType)) { + typedParameters.put(parameterName, parameterValue != null && Boolean.parseBoolean(parameterValue.toString())); } else { typedParameters.put(parameterName, parameterValue); } @@ -76,8 +105,4 @@ private static Map asTypedParameters(McpSchema.JsonSchema schema return typedParameters; } - private static boolean isTypeOf(Class clazz, String jsonSchemaType) { - return clazz.getName().equalsIgnoreCase(jsonSchemaType) || clazz.getSimpleName().equalsIgnoreCase(jsonSchemaType); - } - } From 859fda9a1ac53f7a05a3ca551d1374d84918438e Mon Sep 17 00:00:00 2001 From: codeboyzhou Date: Wed, 21 May 2025 00:39:47 +0800 Subject: [PATCH 26/57] docs(README): Update README.md to introduce yaml configuration example --- README.md | 29 +++++++++++++++++-- .../configuration/McpServerConfiguration.java | 4 --- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index c141072..2be3ae5 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,8 @@ Declarative [MCP Java SDK](https://github.com/modelcontextprotocol/java-sdk) Dev Just put this one line code in your `main` method: ```java +import com.github.codeboyzhou.mcp.declarative.McpServers; + // You can use this annotation to specify the base package // to scan for MCP resources, prompts, tools, but it's optional. // If not specified, it will scan the package where the main method is located. @@ -36,13 +38,34 @@ public class MyMcpServer { McpServers.run(MyMcpServer.class, args).startSyncSseServer( McpSseServerInfo.builder().name("mcp-server").version("1.0.0").port(8080).build() ); - // or start with yaml configuration file compatible with the Spring AI framework + // or start with yaml configuration file (compatible with the Spring AI framework) McpServers.run(MyMcpServer.class, args).startServer(); + // or start with a specific configuration file (compatible with the Spring AI framework) + McpServers.run(MyMcpServer.class, args).startServer("my-mcp-server.yml"); } } ``` +This is a yaml configuration file example (named `mcp-server.yml` by default, or also `mcp-server.yaml`) only if you are using `startServer()` method: + +```yaml +enabled: true +stdio: false +name: mcp-server +version: 1.0.0 +instructions: mcp-server +request-timeout: 30000 +type: SYNC +resource-change-notification: true +prompt-change-notification: true +tool-change-notification: true +sse-message-endpoint: /mcp/message +sse-endpoint: /sse +base-url: http://localhost:8080 +sse-port: 8080 +``` + No need to care about the low-level details of native MCP Java SDK and how to create the MCP resources, prompts, and tools. Just annotate them like this: ```java @@ -106,11 +129,11 @@ Now it's all set, run your MCP server, choose one MCP client you like and start Add the following Maven dependency to your project: ```xml - + io.github.codeboyzhou mcp-declarative-java-sdk - 0.3.0 + 0.4.0 ``` diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/configuration/McpServerConfiguration.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/configuration/McpServerConfiguration.java index c0bcac0..2efdbb9 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/configuration/McpServerConfiguration.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/configuration/McpServerConfiguration.java @@ -3,8 +3,6 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.github.codeboyzhou.mcp.declarative.util.StringHelper; -import java.util.Map; - public record McpServerConfiguration( @JsonProperty("enabled") boolean enabled, @JsonProperty("stdio") boolean stdio, @@ -16,7 +14,6 @@ public record McpServerConfiguration( @JsonProperty("resource-change-notification") boolean resourceChangeNotification, @JsonProperty("prompt-change-notification") boolean promptChangeNotification, @JsonProperty("tool-change-notification") boolean toolChangeNotification, - @JsonProperty("tool-response-mime-type") Map toolResponseMimeType, @JsonProperty("sse-message-endpoint") String sseMessageEndpoint, @JsonProperty("sse-endpoint") String sseEndpoint, @JsonProperty("base-url") String baseUrl, @@ -35,7 +32,6 @@ public static McpServerConfiguration defaultConfiguration() { true, true, true, - Map.of(), "/mcp/message", "/sse", StringHelper.EMPTY, From 5091d00efad39d6f7098ffb7d948bc24b9efac2b Mon Sep 17 00:00:00 2001 From: codeboyzhou Date: Wed, 21 May 2025 00:47:32 +0800 Subject: [PATCH 27/57] chore(pom): Ready to deploy version 0.4.0 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index cc6fd9b..7ae954a 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ io.github.codeboyzhou mcp-declarative-java-sdk - 0.4.0-SNAPSHOT + 0.4.0 MCP Declarative Java SDK Annotation-driven MCP (Model Context Protocol) Development with Java - No Spring Framework Required From d0ad04ca429a6497ba2d97ac6f79708cc99694a6 Mon Sep 17 00:00:00 2001 From: codeboyzhou Date: Sat, 24 May 2025 16:13:10 +0800 Subject: [PATCH 28/57] chore(pom): Bump version to 0.5.0-SNAPSHOT --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 7ae954a..3781bee 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ io.github.codeboyzhou mcp-declarative-java-sdk - 0.4.0 + 0.5.0-SNAPSHOT MCP Declarative Java SDK Annotation-driven MCP (Model Context Protocol) Development with Java - No Spring Framework Required From fc0aa78ae38fb147087650006cbb94cc63ce3fb5 Mon Sep 17 00:00:00 2001 From: codeboyzhou Date: Sat, 24 May 2025 17:17:30 +0800 Subject: [PATCH 29/57] refactor(util): Replace type-specific parameter conversion with a general TypeConverter --- .../declarative/util/ReflectionHelper.java | 31 +----- .../mcp/declarative/util/TypeConverter.java | 95 +++++++++++++++++++ 2 files changed, 99 insertions(+), 27 deletions(-) create mode 100644 src/main/java/com/github/codeboyzhou/mcp/declarative/util/TypeConverter.java diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/util/ReflectionHelper.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/util/ReflectionHelper.java index d3c5370..086fddd 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/util/ReflectionHelper.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/util/ReflectionHelper.java @@ -57,22 +57,8 @@ private static Map asTypedParameters(Method method, List parameterType = parameterTypes[i]; - if (String.class == parameterType) { - typedParameters.put(parameterName, parameterValue == null ? StringHelper.EMPTY : parameterValue.toString()); - } else if (int.class == parameterType || Integer.class == parameterType) { - typedParameters.put(parameterName, parameterValue == null ? 0 : Integer.parseInt(parameterValue.toString())); - } else if (long.class == parameterType || Long.class == parameterType) { - typedParameters.put(parameterName, parameterValue == null ? 0 : Long.parseLong(parameterValue.toString())); - } else if (float.class == parameterType || Float.class == parameterType) { - typedParameters.put(parameterName, parameterValue == null ? 0.0 : Float.parseFloat(parameterValue.toString())); - } else if (double.class == parameterType || Double.class == parameterType) { - typedParameters.put(parameterName, parameterValue == null ? 0.0 : Double.parseDouble(parameterValue.toString())); - } else if (boolean.class == parameterType || Boolean.class == parameterType) { - typedParameters.put(parameterName, parameterValue != null && Boolean.parseBoolean(parameterValue.toString())); - } else { - typedParameters.put(parameterName, parameterValue); - } + Object typedParameterValue = TypeConverter.convert(parameterValue, parameterTypes[i]); + typedParameters.put(parameterName, typedParameterValue); } return typedParameters; @@ -89,17 +75,8 @@ private static Map asTypedParameters(McpSchema.JsonSchema schema // to ensure that the parameter type is correct when calling method.invoke() Map map = (Map) parameterProperties; final String jsonSchemaType = map.getOrDefault("type", StringHelper.EMPTY).toString(); - if (String.class.getSimpleName().equalsIgnoreCase(jsonSchemaType)) { - typedParameters.put(parameterName, parameterValue == null ? StringHelper.EMPTY : parameterValue.toString()); - } else if (Integer.class.getSimpleName().equalsIgnoreCase(jsonSchemaType)) { - typedParameters.put(parameterName, parameterValue == null ? 0 : Integer.parseInt(parameterValue.toString())); - } else if (Number.class.getSimpleName().equalsIgnoreCase(jsonSchemaType)) { - typedParameters.put(parameterName, parameterValue == null ? 0.0 : Double.parseDouble(parameterValue.toString())); - } else if (Boolean.class.getSimpleName().equalsIgnoreCase(jsonSchemaType)) { - typedParameters.put(parameterName, parameterValue != null && Boolean.parseBoolean(parameterValue.toString())); - } else { - typedParameters.put(parameterName, parameterValue); - } + Object typedParameterValue = TypeConverter.convert(parameterValue, jsonSchemaType); + typedParameters.put(parameterName, typedParameterValue); }); return typedParameters; diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/util/TypeConverter.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/util/TypeConverter.java new file mode 100644 index 0000000..f282e16 --- /dev/null +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/util/TypeConverter.java @@ -0,0 +1,95 @@ +package com.github.codeboyzhou.mcp.declarative.util; + +public final class TypeConverter { + + public static Object convert(Object value, Class targetType) { + if (value == null) { + return getDefaultValue(targetType); + } + + final String valueAsString = value.toString(); + + if (targetType == String.class) { + return valueAsString; + } + if (targetType == int.class || targetType == Integer.class) { + return Integer.parseInt(valueAsString); + } + if (targetType == long.class || targetType == Long.class) { + return Long.parseLong(valueAsString); + } + if (targetType == float.class || targetType == Float.class) { + return Float.parseFloat(valueAsString); + } + if (targetType == double.class || targetType == Double.class) { + return Double.parseDouble(valueAsString); + } + if (targetType == boolean.class || targetType == Boolean.class) { + return Boolean.parseBoolean(valueAsString); + } + + return valueAsString; + } + + public static Object convert(Object value, String jsonSchemaType) { + if (value == null) { + return getDefaultValue(jsonSchemaType); + } + + final String valueAsString = value.toString(); + + if (String.class.getSimpleName().equalsIgnoreCase(jsonSchemaType)) { + return valueAsString; + } + if (Integer.class.getSimpleName().equalsIgnoreCase(jsonSchemaType)) { + return Integer.parseInt(valueAsString); + } + if (Number.class.getSimpleName().equalsIgnoreCase(jsonSchemaType)) { + return Double.parseDouble(valueAsString); + } + if (Boolean.class.getSimpleName().equalsIgnoreCase(jsonSchemaType)) { + return Boolean.parseBoolean(valueAsString); + } + + return valueAsString; + } + + private static Object getDefaultValue(Class type) { + if (type == String.class) { + return StringHelper.EMPTY; + } + if (type == int.class || type == Integer.class) { + return 0; + } + if (type == long.class || type == Long.class) { + return 0L; + } + if (type == float.class || type == Float.class) { + return 0.0f; + } + if (type == double.class || type == Double.class) { + return 0.0; + } + if (type == boolean.class || type == Boolean.class) { + return false; + } + return null; + } + + private static Object getDefaultValue(String jsonSchemaType) { + if (String.class.getSimpleName().equalsIgnoreCase(jsonSchemaType)) { + return StringHelper.EMPTY; + } + if (Integer.class.getSimpleName().equalsIgnoreCase(jsonSchemaType)) { + return 0; + } + if (Number.class.getSimpleName().equalsIgnoreCase(jsonSchemaType)) { + return 0.0; + } + if (Boolean.class.getSimpleName().equalsIgnoreCase(jsonSchemaType)) { + return false; + } + return null; + } + +} From 570cdcf288932e2d4e6bfbf9e2237c6154d7dd57 Mon Sep 17 00:00:00 2001 From: codeboyzhou Date: Sun, 25 May 2025 21:23:12 +0800 Subject: [PATCH 30/57] refactor(configuration): Enhance MCP server configuration --- .../mcp/declarative/McpServers.java | 44 ++++--------- .../configuration/McpServerCapabilities.java | 10 +++ .../McpServerChangeNotification.java | 10 +++ .../configuration/McpServerConfiguration.java | 34 ++-------- .../configuration/McpServerSSE.java | 11 ++++ .../YamlConfigurationLoader.java | 8 ++- .../server/ConfigurableMcpServerFactory.java | 14 +++++ .../ConfigurableMcpSyncServerFactory.java | 62 +++++++++++++++++++ .../mcp/declarative/server/McpServerInfo.java | 10 +++ .../declarative/server/McpSseServerInfo.java | 18 ++++++ .../mcp/declarative/util/JsonHelper.java | 2 +- src/main/resources/mcp-server-default.yml | 19 ++++++ src/test/resources/mcp-server-async.yml | 18 +++++- src/test/resources/mcp-server-not-enabled.yml | 18 +++++- src/test/resources/mcp-server-sse-mode.yml | 18 +++++- 15 files changed, 225 insertions(+), 71 deletions(-) create mode 100644 src/main/java/com/github/codeboyzhou/mcp/declarative/configuration/McpServerCapabilities.java create mode 100644 src/main/java/com/github/codeboyzhou/mcp/declarative/configuration/McpServerChangeNotification.java create mode 100644 src/main/java/com/github/codeboyzhou/mcp/declarative/configuration/McpServerSSE.java create mode 100644 src/main/java/com/github/codeboyzhou/mcp/declarative/server/ConfigurableMcpServerFactory.java create mode 100644 src/main/java/com/github/codeboyzhou/mcp/declarative/server/ConfigurableMcpSyncServerFactory.java create mode 100644 src/main/resources/mcp-server-default.yml diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/McpServers.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/McpServers.java index 4231b4a..17d8647 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/McpServers.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/McpServers.java @@ -1,19 +1,19 @@ package com.github.codeboyzhou.mcp.declarative; -import com.fasterxml.jackson.databind.ObjectMapper; import com.github.codeboyzhou.mcp.declarative.annotation.McpComponentScan; import com.github.codeboyzhou.mcp.declarative.configuration.McpServerConfiguration; import com.github.codeboyzhou.mcp.declarative.configuration.YamlConfigurationLoader; -import com.github.codeboyzhou.mcp.declarative.enums.ServerType; import com.github.codeboyzhou.mcp.declarative.exception.McpServerException; import com.github.codeboyzhou.mcp.declarative.listener.DefaultMcpSyncHttpServerStatusListener; import com.github.codeboyzhou.mcp.declarative.listener.McpHttpServerStatusListener; +import com.github.codeboyzhou.mcp.declarative.server.ConfigurableMcpSyncServerFactory; import com.github.codeboyzhou.mcp.declarative.server.McpHttpServer; import com.github.codeboyzhou.mcp.declarative.server.McpServerComponentRegisters; import com.github.codeboyzhou.mcp.declarative.server.McpServerFactory; import com.github.codeboyzhou.mcp.declarative.server.McpServerInfo; import com.github.codeboyzhou.mcp.declarative.server.McpSseServerInfo; import com.github.codeboyzhou.mcp.declarative.server.McpSyncServerFactory; +import com.github.codeboyzhou.mcp.declarative.util.JsonHelper; import io.modelcontextprotocol.server.McpSyncServer; import io.modelcontextprotocol.server.transport.HttpServletSseServerTransportProvider; import io.modelcontextprotocol.server.transport.StdioServerTransportProvider; @@ -32,8 +32,6 @@ public class McpServers { private static final McpServers INSTANCE = new McpServers(); - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); - private static Reflections reflections; public static McpServers run(Class applicationMainClass, String[] args) { @@ -71,7 +69,7 @@ public void startSyncStdioServer(McpServerInfo serverInfo) { public void startSyncSseServer(McpSseServerInfo serverInfo, McpHttpServerStatusListener listener) { McpServerFactory factory = new McpSyncServerFactory(); HttpServletSseServerTransportProvider transportProvider = new HttpServletSseServerTransportProvider( - OBJECT_MAPPER, serverInfo.baseUrl(), serverInfo.messageEndpoint(), serverInfo.sseEndpoint() + JsonHelper.MAPPER, serverInfo.baseUrl(), serverInfo.messageEndpoint(), serverInfo.sseEndpoint() ); McpSyncServer server = factory.create(serverInfo, transportProvider); McpServerComponentRegisters.registerAllTo(server, reflections); @@ -89,11 +87,7 @@ public void startServer(String configFileName) { McpServerConfiguration configuration; try { configuration = configurationLoader.load(configFileName); - if (configuration.enabled()) { - startServerWith(configuration); - } else { - logger.info("MCP server is disabled."); - } + doStartServer(configuration); } catch (IOException e) { throw new McpServerException("Error loading configuration file: " + e.getMessage(), e); } @@ -102,32 +96,20 @@ public void startServer(String configFileName) { public void startServer() { YamlConfigurationLoader configurationLoader = new YamlConfigurationLoader(); McpServerConfiguration configuration = configurationLoader.loadConfiguration(); - startServerWith(configuration); + doStartServer(configuration); } - private void startServerWith(McpServerConfiguration configuration) { - if (ServerType.SYNC.name().equalsIgnoreCase(configuration.type())) { + private void doStartServer(McpServerConfiguration configuration) { + if (configuration.enabled()) { + McpSyncServer server = new ConfigurableMcpSyncServerFactory(configuration).create(); + McpServerComponentRegisters.registerAllTo(server, reflections); if (configuration.stdio()) { - McpServerInfo serverInfo = McpServerInfo.builder() - .name(configuration.name()) - .version(configuration.version()) - .instructions(configuration.instructions()) - .requestTimeout(Duration.ofSeconds(configuration.requestTimeout())) - .build(); - startSyncStdioServer(serverInfo); + startSyncStdioServer(McpServerInfo.from(configuration)); } else { - McpSseServerInfo serverInfo = McpSseServerInfo.builder() - .name(configuration.name()) - .version(configuration.version()) - .instructions(configuration.instructions()) - .requestTimeout(Duration.ofSeconds(configuration.requestTimeout())) - .baseUrl(configuration.baseUrl()) - .messageEndpoint(configuration.sseMessageEndpoint()) - .sseEndpoint(configuration.sseEndpoint()) - .port(configuration.ssePort()) - .build(); - startSyncSseServer(serverInfo); + startSyncSseServer(McpSseServerInfo.from(configuration)); } + } else { + logger.info("MCP server is disabled."); } } diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/configuration/McpServerCapabilities.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/configuration/McpServerCapabilities.java new file mode 100644 index 0000000..51f486d --- /dev/null +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/configuration/McpServerCapabilities.java @@ -0,0 +1,10 @@ +package com.github.codeboyzhou.mcp.declarative.configuration; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record McpServerCapabilities( + @JsonProperty("resource") boolean resource, + @JsonProperty("prompt") boolean prompt, + @JsonProperty("tool") boolean tool +) { +} diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/configuration/McpServerChangeNotification.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/configuration/McpServerChangeNotification.java new file mode 100644 index 0000000..c676f41 --- /dev/null +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/configuration/McpServerChangeNotification.java @@ -0,0 +1,10 @@ +package com.github.codeboyzhou.mcp.declarative.configuration; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record McpServerChangeNotification( + @JsonProperty("resource") boolean resource, + @JsonProperty("prompt") boolean prompt, + @JsonProperty("tool") boolean tool +) { +} diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/configuration/McpServerConfiguration.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/configuration/McpServerConfiguration.java index 2efdbb9..75e95d4 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/configuration/McpServerConfiguration.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/configuration/McpServerConfiguration.java @@ -1,42 +1,18 @@ package com.github.codeboyzhou.mcp.declarative.configuration; import com.fasterxml.jackson.annotation.JsonProperty; -import com.github.codeboyzhou.mcp.declarative.util.StringHelper; +import com.github.codeboyzhou.mcp.declarative.enums.ServerType; public record McpServerConfiguration( @JsonProperty("enabled") boolean enabled, @JsonProperty("stdio") boolean stdio, @JsonProperty("name") String name, @JsonProperty("version") String version, + @JsonProperty("type") ServerType type, @JsonProperty("instructions") String instructions, @JsonProperty("request-timeout") long requestTimeout, - @JsonProperty("type") String type, - @JsonProperty("resource-change-notification") boolean resourceChangeNotification, - @JsonProperty("prompt-change-notification") boolean promptChangeNotification, - @JsonProperty("tool-change-notification") boolean toolChangeNotification, - @JsonProperty("sse-message-endpoint") String sseMessageEndpoint, - @JsonProperty("sse-endpoint") String sseEndpoint, - @JsonProperty("base-url") String baseUrl, - @JsonProperty("sse-port") int ssePort + @JsonProperty("capabilities") McpServerCapabilities capabilities, + @JsonProperty("change-notification") McpServerChangeNotification changeNotification, + @JsonProperty("sse") McpServerSSE sse ) { - - public static McpServerConfiguration defaultConfiguration() { - return new McpServerConfiguration( - true, - false, - "mcp-server", - "1.0.0", - "mcp-server", - 10000, - "SYNC", - true, - true, - true, - "/mcp/message", - "/sse", - StringHelper.EMPTY, - 8080 - ); - } - } diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/configuration/McpServerSSE.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/configuration/McpServerSSE.java new file mode 100644 index 0000000..942eadc --- /dev/null +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/configuration/McpServerSSE.java @@ -0,0 +1,11 @@ +package com.github.codeboyzhou.mcp.declarative.configuration; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record McpServerSSE( + @JsonProperty("message-endpoint") String messageEndpoint, + @JsonProperty("endpoint") String endpoint, + @JsonProperty("base-url") String baseUrl, + @JsonProperty("port") int port +) { +} diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/configuration/YamlConfigurationLoader.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/configuration/YamlConfigurationLoader.java index 5dcfec5..ee343ce 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/configuration/YamlConfigurationLoader.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/configuration/YamlConfigurationLoader.java @@ -27,7 +27,12 @@ public McpServerConfiguration loadConfiguration() { return load("mcp-server.yaml"); } catch (IOException ex) { logger.warn("The mcp-server.yml and mcp-server.yaml were not found, will use default configuration"); - return McpServerConfiguration.defaultConfiguration(); + try { + return load("mcp-server-default.yml"); + } catch (IOException ignored) { + // should never happen + return null; + } } } } @@ -41,6 +46,7 @@ public McpServerConfiguration load(String configFileName) throws IOException { InputStreamReader inputStreamReader = new InputStreamReader(inputStream); BufferedReader bufferedReader = new BufferedReader(inputStreamReader); final String content = bufferedReader.lines().collect(joining(System.lineSeparator())); + logger.debug("Loaded configuration from {}:\n{}", configFileName, content); return mapper.readValue(content, McpServerConfiguration.class); } } diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/ConfigurableMcpServerFactory.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/ConfigurableMcpServerFactory.java new file mode 100644 index 0000000..a248aa2 --- /dev/null +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/ConfigurableMcpServerFactory.java @@ -0,0 +1,14 @@ +package com.github.codeboyzhou.mcp.declarative.server; + +import io.modelcontextprotocol.spec.McpSchema; +import io.modelcontextprotocol.spec.McpServerTransportProvider; + +public interface ConfigurableMcpServerFactory { + + T create(); + + McpServerTransportProvider transportProvider(); + + McpSchema.ServerCapabilities configureServerCapabilities(); + +} diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/ConfigurableMcpSyncServerFactory.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/ConfigurableMcpSyncServerFactory.java new file mode 100644 index 0000000..8e78f4a --- /dev/null +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/ConfigurableMcpSyncServerFactory.java @@ -0,0 +1,62 @@ +package com.github.codeboyzhou.mcp.declarative.server; + +import com.github.codeboyzhou.mcp.declarative.configuration.McpServerCapabilities; +import com.github.codeboyzhou.mcp.declarative.configuration.McpServerChangeNotification; +import com.github.codeboyzhou.mcp.declarative.configuration.McpServerConfiguration; +import com.github.codeboyzhou.mcp.declarative.configuration.McpServerSSE; +import com.github.codeboyzhou.mcp.declarative.util.JsonHelper; +import io.modelcontextprotocol.server.McpServer; +import io.modelcontextprotocol.server.McpSyncServer; +import io.modelcontextprotocol.server.transport.HttpServletSseServerTransportProvider; +import io.modelcontextprotocol.server.transport.StdioServerTransportProvider; +import io.modelcontextprotocol.spec.McpSchema; +import io.modelcontextprotocol.spec.McpServerTransportProvider; + +import java.time.Duration; + +public class ConfigurableMcpSyncServerFactory implements ConfigurableMcpServerFactory { + + private final McpServerConfiguration configuration; + + public ConfigurableMcpSyncServerFactory(McpServerConfiguration configuration) { + this.configuration = configuration; + } + + @Override + public McpSyncServer create() { + return McpServer.sync(transportProvider()) + .instructions(configuration.instructions()) + .capabilities(configureServerCapabilities()) + .serverInfo(configuration.name(), configuration.version()) + .requestTimeout(Duration.ofMillis(configuration.requestTimeout())) + .build(); + } + + @Override + public McpServerTransportProvider transportProvider() { + if (configuration.stdio()) { + return new StdioServerTransportProvider(); + } else { + McpServerSSE sse = configuration.sse(); + return new HttpServletSseServerTransportProvider(JsonHelper.MAPPER, sse.baseUrl(), sse.messageEndpoint(), sse.endpoint()); + } + } + + @Override + public McpSchema.ServerCapabilities configureServerCapabilities() { + McpSchema.ServerCapabilities.Builder capabilities = McpSchema.ServerCapabilities.builder(); + McpServerCapabilities capabilitiesConfig = configuration.capabilities(); + McpServerChangeNotification serverChangeNotification = configuration.changeNotification(); + if (capabilitiesConfig.resource()) { + capabilities.resources(true, serverChangeNotification.resource()); + } + if (capabilitiesConfig.prompt()) { + capabilities.prompts(serverChangeNotification.prompt()); + } + if (capabilitiesConfig.tool()) { + capabilities.tools(serverChangeNotification.tool()); + } + return capabilities.build(); + } + +} diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpServerInfo.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpServerInfo.java index 0e6b13c..130ee37 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpServerInfo.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpServerInfo.java @@ -1,5 +1,6 @@ package com.github.codeboyzhou.mcp.declarative.server; +import com.github.codeboyzhou.mcp.declarative.configuration.McpServerConfiguration; import com.github.codeboyzhou.mcp.declarative.util.StringHelper; import java.time.Duration; @@ -25,6 +26,15 @@ public static Builder builder() { return new Builder<>(); } + public static McpServerInfo from(McpServerConfiguration configuration) { + Builder builder = builder(); + builder.name = configuration.name(); + builder.version = configuration.version(); + builder.instructions = configuration.instructions(); + builder.requestTimeout = Duration.ofSeconds(configuration.requestTimeout()); + return builder.build(); + } + public String name() { return name; } diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSseServerInfo.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSseServerInfo.java index 4b9c150..0769c09 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSseServerInfo.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSseServerInfo.java @@ -1,7 +1,11 @@ package com.github.codeboyzhou.mcp.declarative.server; +import com.github.codeboyzhou.mcp.declarative.configuration.McpServerConfiguration; +import com.github.codeboyzhou.mcp.declarative.configuration.McpServerSSE; import com.github.codeboyzhou.mcp.declarative.util.StringHelper; +import java.time.Duration; + public class McpSseServerInfo extends McpServerInfo { private final String baseUrl; @@ -24,6 +28,20 @@ public static McpSseServerInfo.Builder builder() { return new McpSseServerInfo.Builder(); } + public static McpSseServerInfo from(McpServerConfiguration configuration) { + McpSseServerInfo.Builder builder = McpSseServerInfo.builder(); + builder.name(configuration.name()); + builder.version(configuration.version()); + builder.instructions(configuration.instructions()); + builder.requestTimeout(Duration.ofMillis(configuration.requestTimeout())); + McpServerSSE sse = configuration.sse(); + builder.baseUrl(sse.baseUrl()); + builder.messageEndpoint(sse.messageEndpoint()); + builder.sseEndpoint(sse.endpoint()); + builder.port(sse.port()); + return builder.build(); + } + public String baseUrl() { return baseUrl; } diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/util/JsonHelper.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/util/JsonHelper.java index 29258a4..95bb7fc 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/util/JsonHelper.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/util/JsonHelper.java @@ -6,7 +6,7 @@ public final class JsonHelper { - private static final ObjectMapper MAPPER = new ObjectMapper(); + public static final ObjectMapper MAPPER = new ObjectMapper(); public static String toJson(Object object) { try { diff --git a/src/main/resources/mcp-server-default.yml b/src/main/resources/mcp-server-default.yml new file mode 100644 index 0000000..944c82d --- /dev/null +++ b/src/main/resources/mcp-server-default.yml @@ -0,0 +1,19 @@ +enabled: true +stdio: false +name: mcp-server +version: 1.0.0 +type: SYNC +request-timeout: 20000 +capabilities: + resource: true + prompt: true + tool: true +change-notification: + resource: true + prompt: true + tool: true +sse: + message-endpoint: /mcp/message + endpoint: /sse + base-url: http://localhost:8080 + port: 8080 diff --git a/src/test/resources/mcp-server-async.yml b/src/test/resources/mcp-server-async.yml index ae432b3..5adc884 100644 --- a/src/test/resources/mcp-server-async.yml +++ b/src/test/resources/mcp-server-async.yml @@ -1,7 +1,19 @@ enabled: true stdio: true -name: mcp-stdio-server +name: mcp-server version: 1.0.0 -instructions: A simple server that uses stdio to communicate with the client -request-timeout: 30000 type: ASYNC +request-timeout: 20000 +capabilities: + resource: true + prompt: true + tool: true +change-notification: + resource: true + prompt: true + tool: true +sse: + message-endpoint: /mcp/message + endpoint: /sse + base-url: http://localhost:8080 + port: 8080 diff --git a/src/test/resources/mcp-server-not-enabled.yml b/src/test/resources/mcp-server-not-enabled.yml index 9e6acf0..57e81a3 100644 --- a/src/test/resources/mcp-server-not-enabled.yml +++ b/src/test/resources/mcp-server-not-enabled.yml @@ -1,7 +1,19 @@ enabled: false stdio: true -name: mcp-stdio-server +name: mcp-server version: 1.0.0 -instructions: A simple server that uses stdio to communicate with the client -request-timeout: 30000 type: SYNC +request-timeout: 20000 +capabilities: + resource: true + prompt: true + tool: true +change-notification: + resource: true + prompt: true + tool: true +sse: + message-endpoint: /mcp/message + endpoint: /sse + base-url: http://localhost:8080 + port: 8080 diff --git a/src/test/resources/mcp-server-sse-mode.yml b/src/test/resources/mcp-server-sse-mode.yml index 4491168..944c82d 100644 --- a/src/test/resources/mcp-server-sse-mode.yml +++ b/src/test/resources/mcp-server-sse-mode.yml @@ -1,7 +1,19 @@ enabled: true stdio: false -name: mcp-stdio-server +name: mcp-server version: 1.0.0 -instructions: A simple server that uses stdio to communicate with the client -request-timeout: 30000 type: SYNC +request-timeout: 20000 +capabilities: + resource: true + prompt: true + tool: true +change-notification: + resource: true + prompt: true + tool: true +sse: + message-endpoint: /mcp/message + endpoint: /sse + base-url: http://localhost:8080 + port: 8080 From 459e1768c313835119009daf52df06d1c812d4de Mon Sep 17 00:00:00 2001 From: codeboyzhou Date: Sun, 25 May 2025 21:24:51 +0800 Subject: [PATCH 31/57] refactor(server): Remove deprecated methods in v0.4.0 --- .../github/codeboyzhou/mcp/declarative/McpServers.java | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/McpServers.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/McpServers.java index 17d8647..5039a4a 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/McpServers.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/McpServers.java @@ -24,7 +24,6 @@ import org.slf4j.LoggerFactory; import java.io.IOException; -import java.time.Duration; public class McpServers { @@ -52,13 +51,6 @@ private static String determineBasePackage(McpComponentScan scan, Class appli return applicationMainClass.getPackageName(); } - @Deprecated(since = "0.4.0") - public void startSyncStdioServer(String name, String version, String instructions) { - McpServerInfo serverInfo = McpServerInfo.builder().name(name).version(version) - .instructions(instructions).requestTimeout(Duration.ofSeconds(10)).build(); - startSyncStdioServer(serverInfo); - } - public void startSyncStdioServer(McpServerInfo serverInfo) { McpServerFactory factory = new McpSyncServerFactory(); McpServerTransportProvider transportProvider = new StdioServerTransportProvider(); From 1004616e1918bf7dd7130ba36e484307672b00f4 Mon Sep 17 00:00:00 2001 From: codeboyzhou Date: Sun, 25 May 2025 21:26:44 +0800 Subject: [PATCH 32/57] chore(minor): Increase request timeout to 20 seconds --- .../codeboyzhou/mcp/declarative/server/McpServerInfo.java | 2 +- .../github/codeboyzhou/mcp/declarative/McpServersTest.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpServerInfo.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpServerInfo.java index 130ee37..5933b04 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpServerInfo.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpServerInfo.java @@ -60,7 +60,7 @@ public static class Builder> { protected String instructions = StringHelper.EMPTY; - protected Duration requestTimeout = Duration.ofSeconds(10); + protected Duration requestTimeout = Duration.ofSeconds(20); protected T self() { return (T) this; diff --git a/src/test/java/com/github/codeboyzhou/mcp/declarative/McpServersTest.java b/src/test/java/com/github/codeboyzhou/mcp/declarative/McpServersTest.java index 004c5eb..81f86b6 100644 --- a/src/test/java/com/github/codeboyzhou/mcp/declarative/McpServersTest.java +++ b/src/test/java/com/github/codeboyzhou/mcp/declarative/McpServersTest.java @@ -81,7 +81,7 @@ void testStartSyncStdioServer() { McpServers servers = McpServers.run(TestMcpComponentScanIsNull.class, EMPTY_ARGS); McpServerInfo serverInfo = McpServerInfo.builder() .instructions("test-mcp-sync-stdio-server-instructions") - .requestTimeout(Duration.ofSeconds(10)) + .requestTimeout(Duration.ofSeconds(20)) .name("test-mcp-sync-stdio-server") .version("1.0.0") .build(); @@ -95,7 +95,7 @@ void testStartSyncSseServer() { assertDoesNotThrow(() -> { McpSseServerInfo serverInfo = McpSseServerInfo.builder() .instructions("test-mcp-sync-sse-server-instructions") - .requestTimeout(Duration.ofSeconds(10)) + .requestTimeout(Duration.ofSeconds(20)) .baseUrl("http://127.0.0.1:8080") .messageEndpoint("/message") .sseEndpoint("/sse") From b12b96e8a5c5f26ad1e5263b7d56bd5156eb0435 Mon Sep 17 00:00:00 2001 From: codeboyzhou Date: Tue, 27 May 2025 00:08:33 +0800 Subject: [PATCH 33/57] refactor(server): Introduce Guice framework to enhance dependency injection --- pom.xml | 6 +++ .../mcp/declarative/McpServers.java | 30 +++++---------- .../server/McpServerComponentRegisters.java | 24 ------------ .../McpServerComponentRegister.java | 2 +- .../register/McpServerComponentRegisters.java | 20 ++++++++++ .../McpSyncServerComponentRegister.java | 14 +++++++ .../McpSyncServerPromptRegister.java | 16 ++++---- .../McpSyncServerResourceRegister.java | 16 ++++---- .../McpSyncServerToolRegister.java | 16 ++++---- .../mcp/declarative/util/GuiceInjector.java | 38 +++++++++++++++++++ .../mcp/declarative/McpServersTest.java | 21 +++------- 11 files changed, 121 insertions(+), 82 deletions(-) delete mode 100644 src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpServerComponentRegisters.java rename src/main/java/com/github/codeboyzhou/mcp/declarative/server/{ => register}/McpServerComponentRegister.java (73%) create mode 100644 src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpServerComponentRegisters.java create mode 100644 src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpSyncServerComponentRegister.java rename src/main/java/com/github/codeboyzhou/mcp/declarative/server/{ => register}/McpSyncServerPromptRegister.java (85%) rename src/main/java/com/github/codeboyzhou/mcp/declarative/server/{ => register}/McpSyncServerResourceRegister.java (79%) rename src/main/java/com/github/codeboyzhou/mcp/declarative/server/{ => register}/McpSyncServerToolRegister.java (90%) create mode 100644 src/main/java/com/github/codeboyzhou/mcp/declarative/util/GuiceInjector.java diff --git a/pom.xml b/pom.xml index 3781bee..7e1f4cf 100644 --- a/pom.xml +++ b/pom.xml @@ -51,6 +51,7 @@ 3.2.3 4.0.0 + 6.0.0 2.18.3 12.0.18 5.10.2 @@ -84,6 +85,11 @@ jackson-dataformat-yaml ${jackson-dataformat-yaml.version} + + com.google.inject + guice + ${guice.version} + io.modelcontextprotocol.sdk mcp diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/McpServers.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/McpServers.java index 5039a4a..5f17484 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/McpServers.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/McpServers.java @@ -1,6 +1,5 @@ package com.github.codeboyzhou.mcp.declarative; -import com.github.codeboyzhou.mcp.declarative.annotation.McpComponentScan; import com.github.codeboyzhou.mcp.declarative.configuration.McpServerConfiguration; import com.github.codeboyzhou.mcp.declarative.configuration.YamlConfigurationLoader; import com.github.codeboyzhou.mcp.declarative.exception.McpServerException; @@ -8,18 +7,20 @@ import com.github.codeboyzhou.mcp.declarative.listener.McpHttpServerStatusListener; import com.github.codeboyzhou.mcp.declarative.server.ConfigurableMcpSyncServerFactory; import com.github.codeboyzhou.mcp.declarative.server.McpHttpServer; -import com.github.codeboyzhou.mcp.declarative.server.McpServerComponentRegisters; +import com.github.codeboyzhou.mcp.declarative.server.register.McpServerComponentRegisters; import com.github.codeboyzhou.mcp.declarative.server.McpServerFactory; import com.github.codeboyzhou.mcp.declarative.server.McpServerInfo; import com.github.codeboyzhou.mcp.declarative.server.McpSseServerInfo; import com.github.codeboyzhou.mcp.declarative.server.McpSyncServerFactory; +import com.github.codeboyzhou.mcp.declarative.util.GuiceInjector; import com.github.codeboyzhou.mcp.declarative.util.JsonHelper; +import com.google.inject.Guice; +import com.google.inject.Injector; import io.modelcontextprotocol.server.McpSyncServer; import io.modelcontextprotocol.server.transport.HttpServletSseServerTransportProvider; import io.modelcontextprotocol.server.transport.StdioServerTransportProvider; import io.modelcontextprotocol.spec.McpServerTransportProvider; import io.modelcontextprotocol.util.Assert; -import org.reflections.Reflections; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -31,31 +32,18 @@ public class McpServers { private static final McpServers INSTANCE = new McpServers(); - private static Reflections reflections; + private static Injector injector; public static McpServers run(Class applicationMainClass, String[] args) { - McpComponentScan scan = applicationMainClass.getAnnotation(McpComponentScan.class); - reflections = new Reflections(determineBasePackage(scan, applicationMainClass)); + injector = Guice.createInjector(new GuiceInjector(applicationMainClass)); return INSTANCE; } - private static String determineBasePackage(McpComponentScan scan, Class applicationMainClass) { - if (scan != null) { - if (!scan.basePackage().trim().isBlank()) { - return scan.basePackage(); - } - if (scan.basePackageClass() != Object.class) { - return scan.basePackageClass().getPackageName(); - } - } - return applicationMainClass.getPackageName(); - } - public void startSyncStdioServer(McpServerInfo serverInfo) { McpServerFactory factory = new McpSyncServerFactory(); McpServerTransportProvider transportProvider = new StdioServerTransportProvider(); McpSyncServer server = factory.create(serverInfo, transportProvider); - McpServerComponentRegisters.registerAllTo(server, reflections); + new McpServerComponentRegisters(injector).registerAllTo(server); } public void startSyncSseServer(McpSseServerInfo serverInfo, McpHttpServerStatusListener listener) { @@ -64,7 +52,7 @@ public void startSyncSseServer(McpSseServerInfo serverInfo, McpHttpServerStatusL JsonHelper.MAPPER, serverInfo.baseUrl(), serverInfo.messageEndpoint(), serverInfo.sseEndpoint() ); McpSyncServer server = factory.create(serverInfo, transportProvider); - McpServerComponentRegisters.registerAllTo(server, reflections); + new McpServerComponentRegisters(injector).registerAllTo(server); McpHttpServer httpServer = new McpHttpServer<>(); httpServer.with(transportProvider).with(serverInfo).with(listener).attach(server).start(); } @@ -94,7 +82,7 @@ public void startServer() { private void doStartServer(McpServerConfiguration configuration) { if (configuration.enabled()) { McpSyncServer server = new ConfigurableMcpSyncServerFactory(configuration).create(); - McpServerComponentRegisters.registerAllTo(server, reflections); + new McpServerComponentRegisters(injector).registerAllTo(server); if (configuration.stdio()) { startSyncStdioServer(McpServerInfo.from(configuration)); } else { diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpServerComponentRegisters.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpServerComponentRegisters.java deleted file mode 100644 index 940021a..0000000 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpServerComponentRegisters.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.github.codeboyzhou.mcp.declarative.server; - -import com.github.codeboyzhou.mcp.declarative.annotation.McpPrompts; -import com.github.codeboyzhou.mcp.declarative.annotation.McpResources; -import com.github.codeboyzhou.mcp.declarative.annotation.McpTools; -import io.modelcontextprotocol.server.McpSyncServer; -import org.reflections.Reflections; - -import java.util.Set; - -public final class McpServerComponentRegisters { - - public static void registerAllTo(McpSyncServer server, Reflections reflections) { - Set> resourceClasses = reflections.getTypesAnnotatedWith(McpResources.class); - new McpSyncServerResourceRegister(resourceClasses).registerTo(server); - - Set> promptClasses = reflections.getTypesAnnotatedWith(McpPrompts.class); - new McpSyncServerPromptRegister(promptClasses).registerTo(server); - - Set> toolClasses = reflections.getTypesAnnotatedWith(McpTools.class); - new McpSyncServerToolRegister(toolClasses).registerTo(server); - } - -} diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpServerComponentRegister.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpServerComponentRegister.java similarity index 73% rename from src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpServerComponentRegister.java rename to src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpServerComponentRegister.java index e290d1d..47a6fb1 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpServerComponentRegister.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpServerComponentRegister.java @@ -1,4 +1,4 @@ -package com.github.codeboyzhou.mcp.declarative.server; +package com.github.codeboyzhou.mcp.declarative.server.register; import java.lang.reflect.Method; diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpServerComponentRegisters.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpServerComponentRegisters.java new file mode 100644 index 0000000..414d418 --- /dev/null +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpServerComponentRegisters.java @@ -0,0 +1,20 @@ +package com.github.codeboyzhou.mcp.declarative.server.register; + +import com.google.inject.Injector; +import io.modelcontextprotocol.server.McpSyncServer; + +public class McpServerComponentRegisters { + + private final Injector injector; + + public McpServerComponentRegisters(Injector injector) { + this.injector = injector; + } + + public void registerAllTo(McpSyncServer server) { + new McpSyncServerResourceRegister(injector).registerTo(server); + new McpSyncServerPromptRegister(injector).registerTo(server); + new McpSyncServerToolRegister(injector).registerTo(server); + } + +} diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpSyncServerComponentRegister.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpSyncServerComponentRegister.java new file mode 100644 index 0000000..f6ca2da --- /dev/null +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpSyncServerComponentRegister.java @@ -0,0 +1,14 @@ +package com.github.codeboyzhou.mcp.declarative.server.register; + +import com.google.inject.Injector; +import io.modelcontextprotocol.server.McpSyncServer; + +public abstract class McpSyncServerComponentRegister implements McpServerComponentRegister { + + protected final Injector injector; + + protected McpSyncServerComponentRegister(Injector injector) { + this.injector = injector; + } + +} diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerPromptRegister.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpSyncServerPromptRegister.java similarity index 85% rename from src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerPromptRegister.java rename to src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpSyncServerPromptRegister.java index 1bb5930..b5d841e 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerPromptRegister.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpSyncServerPromptRegister.java @@ -1,12 +1,15 @@ -package com.github.codeboyzhou.mcp.declarative.server; +package com.github.codeboyzhou.mcp.declarative.server.register; import com.github.codeboyzhou.mcp.declarative.annotation.McpPrompt; import com.github.codeboyzhou.mcp.declarative.annotation.McpPromptParam; +import com.github.codeboyzhou.mcp.declarative.annotation.McpPrompts; import com.github.codeboyzhou.mcp.declarative.util.JsonHelper; import com.github.codeboyzhou.mcp.declarative.util.ReflectionHelper; +import com.google.inject.Injector; import io.modelcontextprotocol.server.McpServerFeatures; import io.modelcontextprotocol.server.McpSyncServer; import io.modelcontextprotocol.spec.McpSchema; +import org.reflections.Reflections; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -16,19 +19,18 @@ import java.util.List; import java.util.Set; -public class McpSyncServerPromptRegister - implements McpServerComponentRegister { +public class McpSyncServerPromptRegister extends McpSyncServerComponentRegister { private static final Logger logger = LoggerFactory.getLogger(McpSyncServerPromptRegister.class); - private final Set> promptClasses; - - public McpSyncServerPromptRegister(Set> promptClasses) { - this.promptClasses = promptClasses; + protected McpSyncServerPromptRegister(Injector injector) { + super(injector); } @Override public void registerTo(McpSyncServer server) { + Reflections reflections = injector.getInstance(Reflections.class); + Set> promptClasses = reflections.getTypesAnnotatedWith(McpPrompts.class); for (Class promptClass : promptClasses) { List methods = ReflectionHelper.getMethodsAnnotatedWith(promptClass, McpPrompt.class); for (Method method : methods) { diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerResourceRegister.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpSyncServerResourceRegister.java similarity index 79% rename from src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerResourceRegister.java rename to src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpSyncServerResourceRegister.java index 9c62b66..df55ef3 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerResourceRegister.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpSyncServerResourceRegister.java @@ -1,11 +1,14 @@ -package com.github.codeboyzhou.mcp.declarative.server; +package com.github.codeboyzhou.mcp.declarative.server.register; import com.github.codeboyzhou.mcp.declarative.annotation.McpResource; +import com.github.codeboyzhou.mcp.declarative.annotation.McpResources; import com.github.codeboyzhou.mcp.declarative.util.JsonHelper; import com.github.codeboyzhou.mcp.declarative.util.ReflectionHelper; +import com.google.inject.Injector; import io.modelcontextprotocol.server.McpServerFeatures; import io.modelcontextprotocol.server.McpSyncServer; import io.modelcontextprotocol.spec.McpSchema; +import org.reflections.Reflections; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -13,19 +16,18 @@ import java.util.List; import java.util.Set; -public class McpSyncServerResourceRegister - implements McpServerComponentRegister { +public class McpSyncServerResourceRegister extends McpSyncServerComponentRegister { private static final Logger logger = LoggerFactory.getLogger(McpSyncServerResourceRegister.class); - private final Set> resourceClasses; - - public McpSyncServerResourceRegister(Set> resourceClasses) { - this.resourceClasses = resourceClasses; + protected McpSyncServerResourceRegister(Injector injector) { + super(injector); } @Override public void registerTo(McpSyncServer server) { + Reflections reflections = injector.getInstance(Reflections.class); + Set> resourceClasses = reflections.getTypesAnnotatedWith(McpResources.class); for (Class resourceClass : resourceClasses) { List methods = ReflectionHelper.getMethodsAnnotatedWith(resourceClass, McpResource.class); for (Method method : methods) { diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerToolRegister.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpSyncServerToolRegister.java similarity index 90% rename from src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerToolRegister.java rename to src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpSyncServerToolRegister.java index 116fd7b..e22f7c2 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerToolRegister.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpSyncServerToolRegister.java @@ -1,14 +1,17 @@ -package com.github.codeboyzhou.mcp.declarative.server; +package com.github.codeboyzhou.mcp.declarative.server.register; import com.github.codeboyzhou.mcp.declarative.annotation.McpJsonSchemaDefinition; import com.github.codeboyzhou.mcp.declarative.annotation.McpJsonSchemaDefinitionProperty; import com.github.codeboyzhou.mcp.declarative.annotation.McpTool; import com.github.codeboyzhou.mcp.declarative.annotation.McpToolParam; +import com.github.codeboyzhou.mcp.declarative.annotation.McpTools; import com.github.codeboyzhou.mcp.declarative.util.JsonHelper; import com.github.codeboyzhou.mcp.declarative.util.ReflectionHelper; +import com.google.inject.Injector; import io.modelcontextprotocol.server.McpServerFeatures; import io.modelcontextprotocol.server.McpSyncServer; import io.modelcontextprotocol.spec.McpSchema; +import org.reflections.Reflections; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -21,21 +24,20 @@ import java.util.Map; import java.util.Set; -public class McpSyncServerToolRegister - implements McpServerComponentRegister { +public class McpSyncServerToolRegister extends McpSyncServerComponentRegister { private static final Logger logger = LoggerFactory.getLogger(McpSyncServerToolRegister.class); private static final String OBJECT_TYPE_NAME = Object.class.getSimpleName().toLowerCase(); - private final Set> toolClasses; - - public McpSyncServerToolRegister(Set> toolClasses) { - this.toolClasses = toolClasses; + protected McpSyncServerToolRegister(Injector injector) { + super(injector); } @Override public void registerTo(McpSyncServer server) { + Reflections reflections = injector.getInstance(Reflections.class); + Set> toolClasses = reflections.getTypesAnnotatedWith(McpTools.class); for (Class toolClass : toolClasses) { List methods = ReflectionHelper.getMethodsAnnotatedWith(toolClass, McpTool.class); for (Method method : methods) { diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/util/GuiceInjector.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/util/GuiceInjector.java new file mode 100644 index 0000000..176eeba --- /dev/null +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/util/GuiceInjector.java @@ -0,0 +1,38 @@ +package com.github.codeboyzhou.mcp.declarative.util; + +import com.github.codeboyzhou.mcp.declarative.annotation.McpComponentScan; +import com.google.inject.AbstractModule; +import com.google.inject.Provides; +import com.google.inject.Singleton; +import org.reflections.Reflections; + +public final class GuiceInjector extends AbstractModule { + + private final Class applicationMainClass; + + public GuiceInjector(Class applicationMainClass) { + this.applicationMainClass = applicationMainClass; + } + + @Provides + @Singleton + @SuppressWarnings("unused") + public Reflections provideReflections() { + McpComponentScan scan = applicationMainClass.getAnnotation(McpComponentScan.class); + final String basePackage = determineBasePackage(scan, applicationMainClass); + return new Reflections(basePackage); + } + + private String determineBasePackage(McpComponentScan scan, Class applicationMainClass) { + if (scan != null) { + if (!scan.basePackage().trim().isBlank()) { + return scan.basePackage(); + } + if (scan.basePackageClass() != Object.class) { + return scan.basePackageClass().getPackageName(); + } + } + return applicationMainClass.getPackageName(); + } + +} diff --git a/src/test/java/com/github/codeboyzhou/mcp/declarative/McpServersTest.java b/src/test/java/com/github/codeboyzhou/mcp/declarative/McpServersTest.java index 81f86b6..89f4892 100644 --- a/src/test/java/com/github/codeboyzhou/mcp/declarative/McpServersTest.java +++ b/src/test/java/com/github/codeboyzhou/mcp/declarative/McpServersTest.java @@ -13,6 +13,9 @@ import com.github.codeboyzhou.mcp.declarative.server.TestMcpPrompts; import com.github.codeboyzhou.mcp.declarative.server.TestMcpResources; import com.github.codeboyzhou.mcp.declarative.server.TestMcpTools; +import com.github.codeboyzhou.mcp.declarative.util.GuiceInjector; +import com.google.inject.Guice; +import com.google.inject.Injector; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -21,7 +24,6 @@ import org.reflections.Reflections; import org.reflections.scanners.Scanners; -import java.lang.reflect.Field; import java.time.Duration; import java.util.Map; import java.util.Set; @@ -35,16 +37,15 @@ class McpServersTest { static final String[] EMPTY_ARGS = new String[]{}; - Reflections reflections; - @BeforeEach void setUp() { System.setProperty("mcp.declarative.java.sdk.testing", "true"); } @AfterEach - void tearDown() throws NoSuchFieldException, IllegalAccessException { - reflections = getReflectionsField(); + void tearDown() { + Injector injector = Guice.createInjector(new GuiceInjector(TestMcpComponentScanIsNull.class)); + Reflections reflections = injector.getInstance(Reflections.class); assertNotNull(reflections); Map> scannedClasses = reflections.getStore().get(Scanners.TypesAnnotated.name()); @@ -60,8 +61,6 @@ void tearDown() throws NoSuchFieldException, IllegalAccessException { Set scannedToolClass = scannedClasses.get(McpTools.class.getName()); assertEquals(1, scannedToolClass.size()); assertEquals(scannedToolClass.iterator().next(), TestMcpTools.class.getName()); - - reflections = null; } @ParameterizedTest @@ -137,12 +136,4 @@ void testStartServerWithInvalidConfigFileName() { assertEquals("Error loading configuration file: mcp-server-not-exist.yml", e.getMessage()); } - private Reflections getReflectionsField() throws NoSuchFieldException, IllegalAccessException { - Field reflectionsField = McpServers.class.getDeclaredField("reflections"); - reflectionsField.setAccessible(true); - Reflections reflections = (Reflections) reflectionsField.get(null); - reflectionsField.setAccessible(false); - return reflections; - } - } From 2713264c27825ac7e0c7a5450a869061e54e136e Mon Sep 17 00:00:00 2001 From: codeboyzhou Date: Tue, 27 May 2025 00:31:14 +0800 Subject: [PATCH 34/57] refactor(server): Introduce Guice framework to reduce explicit reflections --- .../register/McpSyncServerPromptRegister.java | 25 ++++++++- .../McpSyncServerResourceRegister.java | 5 +- .../register/McpSyncServerToolRegister.java | 26 ++++++++- .../mcp/declarative/util/GuiceInjector.java | 12 ++++ .../declarative/util/ReflectionHelper.java | 55 ------------------- 5 files changed, 62 insertions(+), 61 deletions(-) diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpSyncServerPromptRegister.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpSyncServerPromptRegister.java index b5d841e..cd43791 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpSyncServerPromptRegister.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpSyncServerPromptRegister.java @@ -5,6 +5,7 @@ import com.github.codeboyzhou.mcp.declarative.annotation.McpPrompts; import com.github.codeboyzhou.mcp.declarative.util.JsonHelper; import com.github.codeboyzhou.mcp.declarative.util.ReflectionHelper; +import com.github.codeboyzhou.mcp.declarative.util.TypeConverter; import com.google.inject.Injector; import io.modelcontextprotocol.server.McpServerFeatures; import io.modelcontextprotocol.server.McpSyncServer; @@ -16,7 +17,9 @@ import java.lang.reflect.Method; import java.lang.reflect.Parameter; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Set; public class McpSyncServerPromptRegister extends McpSyncServerComponentRegister { @@ -51,8 +54,10 @@ public McpServerFeatures.SyncPromptSpecification createComponentFrom(Class cl return new McpServerFeatures.SyncPromptSpecification(prompt, (exchange, request) -> { Object result; try { - result = ReflectionHelper.invokeMethod(clazz, method, promptArguments, request.arguments()); - } catch (Throwable e) { + Object instance = injector.getInstance(clazz); + Map typedParameters = asTypedParameters(method, promptArguments, request.arguments()); + result = method.invoke(instance, typedParameters.values().toArray()); + } catch (Exception e) { logger.error("Error invoking prompt method", e); result = e + ": " + e.getMessage(); } @@ -76,4 +81,20 @@ private List createPromptArguments(Method method) { return promptArguments; } + private Map asTypedParameters(Method method, List arguments, Map parameters) { + Class[] parameterTypes = method.getParameterTypes(); + Map typedParameters = new LinkedHashMap<>(parameters.size()); + + for (int i = 0, size = arguments.size(); i < size; i++) { + final String parameterName = arguments.get(i).name(); + final Object parameterValue = parameters.get(parameterName); + // Fill in a default value when the parameter is not specified + // to ensure that the parameter type is correct when calling method.invoke() + Object typedParameterValue = TypeConverter.convert(parameterValue, parameterTypes[i]); + typedParameters.put(parameterName, typedParameterValue); + } + + return typedParameters; + } + } diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpSyncServerResourceRegister.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpSyncServerResourceRegister.java index df55ef3..8ddb98f 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpSyncServerResourceRegister.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpSyncServerResourceRegister.java @@ -49,8 +49,9 @@ public McpServerFeatures.SyncResourceSpecification createComponentFrom(Class return new McpServerFeatures.SyncResourceSpecification(resource, (exchange, request) -> { Object result; try { - result = ReflectionHelper.invokeMethod(clazz, method); - } catch (Throwable e) { + Object instance = injector.getInstance(clazz); + result = method.invoke(instance); + } catch (Exception e) { logger.error("Error invoking resource method", e); result = e + ": " + e.getMessage(); } diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpSyncServerToolRegister.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpSyncServerToolRegister.java index e22f7c2..b882fa5 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpSyncServerToolRegister.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpSyncServerToolRegister.java @@ -7,6 +7,8 @@ import com.github.codeboyzhou.mcp.declarative.annotation.McpTools; import com.github.codeboyzhou.mcp.declarative.util.JsonHelper; import com.github.codeboyzhou.mcp.declarative.util.ReflectionHelper; +import com.github.codeboyzhou.mcp.declarative.util.StringHelper; +import com.github.codeboyzhou.mcp.declarative.util.TypeConverter; import com.google.inject.Injector; import io.modelcontextprotocol.server.McpServerFeatures; import io.modelcontextprotocol.server.McpSyncServer; @@ -58,8 +60,10 @@ public McpServerFeatures.SyncToolSpecification createComponentFrom(Class claz Object result; boolean isError = false; try { - result = ReflectionHelper.invokeMethod(clazz, method, paramSchema, params); - } catch (Throwable e) { + Object instance = injector.getInstance(clazz); + Map typedParameters = asTypedParameters(paramSchema, params); + result = method.invoke(instance, typedParameters.values().toArray()); + } catch (Exception e) { logger.error("Error invoking tool method", e); result = e + ": " + e.getMessage(); isError = true; @@ -132,4 +136,22 @@ private Map createJsonSchemaDefinition(Class definitionClass) return definitionJsonSchema; } + @SuppressWarnings("unchecked") + private Map asTypedParameters(McpSchema.JsonSchema schema, Map parameters) { + Map properties = schema.properties(); + Map typedParameters = new LinkedHashMap<>(properties.size()); + + properties.forEach((parameterName, parameterProperties) -> { + Object parameterValue = parameters.get(parameterName); + // Fill in a default value when the parameter is not specified + // to ensure that the parameter type is correct when calling method.invoke() + Map map = (Map) parameterProperties; + final String jsonSchemaType = map.getOrDefault("type", StringHelper.EMPTY).toString(); + Object typedParameterValue = TypeConverter.convert(parameterValue, jsonSchemaType); + typedParameters.put(parameterName, typedParameterValue); + }); + + return typedParameters; + } + } diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/util/GuiceInjector.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/util/GuiceInjector.java index 176eeba..38e1aec 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/util/GuiceInjector.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/util/GuiceInjector.java @@ -1,8 +1,12 @@ package com.github.codeboyzhou.mcp.declarative.util; import com.github.codeboyzhou.mcp.declarative.annotation.McpComponentScan; +import com.github.codeboyzhou.mcp.declarative.annotation.McpPrompts; +import com.github.codeboyzhou.mcp.declarative.annotation.McpResources; +import com.github.codeboyzhou.mcp.declarative.annotation.McpTools; import com.google.inject.AbstractModule; import com.google.inject.Provides; +import com.google.inject.Scopes; import com.google.inject.Singleton; import org.reflections.Reflections; @@ -23,6 +27,14 @@ public Reflections provideReflections() { return new Reflections(basePackage); } + @Override + protected void configure() { + Reflections reflections = provideReflections(); + reflections.getTypesAnnotatedWith(McpResources.class).forEach(clazz -> bind(clazz).in(Scopes.SINGLETON)); + reflections.getTypesAnnotatedWith(McpPrompts.class).forEach(clazz -> bind(clazz).in(Scopes.SINGLETON)); + reflections.getTypesAnnotatedWith(McpTools.class).forEach(clazz -> bind(clazz).in(Scopes.SINGLETON)); + } + private String determineBasePackage(McpComponentScan scan, Class applicationMainClass) { if (scan != null) { if (!scan.basePackage().trim().isBlank()) { diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/util/ReflectionHelper.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/util/ReflectionHelper.java index 086fddd..e4fda24 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/util/ReflectionHelper.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/util/ReflectionHelper.java @@ -1,14 +1,10 @@ package com.github.codeboyzhou.mcp.declarative.util; -import io.modelcontextprotocol.spec.McpSchema; - import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Parameter; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; import java.util.function.Consumer; import java.util.stream.Stream; @@ -31,55 +27,4 @@ public static void doWithFields(Class clazz, Consumer consumer) { } } - public static Object invokeMethod(Class clazz, Method method) throws Exception { - Object object = clazz.getDeclaredConstructor().newInstance(); - return method.invoke(object); - } - - public static Object invokeMethod(Class clazz, Method method, List arguments, Map parameters) throws Exception { - Object object = clazz.getDeclaredConstructor().newInstance(); - Map typedParameters = asTypedParameters(method, arguments, parameters); - return method.invoke(object, typedParameters.values().toArray()); - } - - public static Object invokeMethod(Class clazz, Method method, McpSchema.JsonSchema schema, Map parameters) throws Exception { - Object object = clazz.getDeclaredConstructor().newInstance(); - Map typedParameters = asTypedParameters(schema, parameters); - return method.invoke(object, typedParameters.values().toArray()); - } - - private static Map asTypedParameters(Method method, List arguments, Map parameters) { - Class[] parameterTypes = method.getParameterTypes(); - Map typedParameters = new LinkedHashMap<>(parameters.size()); - - for (int i = 0, size = arguments.size(); i < size; i++) { - final String parameterName = arguments.get(i).name(); - final Object parameterValue = parameters.get(parameterName); - // Fill in a default value when the parameter is not specified - // to ensure that the parameter type is correct when calling method.invoke() - Object typedParameterValue = TypeConverter.convert(parameterValue, parameterTypes[i]); - typedParameters.put(parameterName, typedParameterValue); - } - - return typedParameters; - } - - @SuppressWarnings("unchecked") - private static Map asTypedParameters(McpSchema.JsonSchema schema, Map parameters) { - Map properties = schema.properties(); - Map typedParameters = new LinkedHashMap<>(properties.size()); - - properties.forEach((parameterName, parameterProperties) -> { - Object parameterValue = parameters.get(parameterName); - // Fill in a default value when the parameter is not specified - // to ensure that the parameter type is correct when calling method.invoke() - Map map = (Map) parameterProperties; - final String jsonSchemaType = map.getOrDefault("type", StringHelper.EMPTY).toString(); - Object typedParameterValue = TypeConverter.convert(parameterValue, jsonSchemaType); - typedParameters.put(parameterName, typedParameterValue); - }); - - return typedParameters; - } - } From 074046850dbcbd26a2de7cfe1280a6c20bedc820 Mon Sep 17 00:00:00 2001 From: codeboyzhou Date: Tue, 27 May 2025 01:32:21 +0800 Subject: [PATCH 35/57] refactor(server): Remove class ReflectionHelper to reduce explicit reflections --- .../register/McpSyncServerPromptRegister.java | 14 +++++---- .../McpSyncServerResourceRegister.java | 4 +-- .../register/McpSyncServerToolRegister.java | 26 ++++++++++------ .../declarative/util/ReflectionHelper.java | 30 ------------------- 4 files changed, 27 insertions(+), 47 deletions(-) delete mode 100644 src/main/java/com/github/codeboyzhou/mcp/declarative/util/ReflectionHelper.java diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpSyncServerPromptRegister.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpSyncServerPromptRegister.java index cd43791..8c95276 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpSyncServerPromptRegister.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpSyncServerPromptRegister.java @@ -4,7 +4,6 @@ import com.github.codeboyzhou.mcp.declarative.annotation.McpPromptParam; import com.github.codeboyzhou.mcp.declarative.annotation.McpPrompts; import com.github.codeboyzhou.mcp.declarative.util.JsonHelper; -import com.github.codeboyzhou.mcp.declarative.util.ReflectionHelper; import com.github.codeboyzhou.mcp.declarative.util.TypeConverter; import com.google.inject.Injector; import io.modelcontextprotocol.server.McpServerFeatures; @@ -21,6 +20,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Stream; public class McpSyncServerPromptRegister extends McpSyncServerComponentRegister { @@ -35,7 +35,8 @@ public void registerTo(McpSyncServer server) { Reflections reflections = injector.getInstance(Reflections.class); Set> promptClasses = reflections.getTypesAnnotatedWith(McpPrompts.class); for (Class promptClass : promptClasses) { - List methods = ReflectionHelper.getMethodsAnnotatedWith(promptClass, McpPrompt.class); + Set promptMethods = reflections.getMethodsAnnotatedWith(McpPrompt.class); + List methods = promptMethods.stream().filter(m -> m.getDeclaringClass() == promptClass).toList(); for (Method method : methods) { McpServerFeatures.SyncPromptSpecification prompt = createComponentFrom(promptClass, method); server.addPrompt(prompt); @@ -68,10 +69,11 @@ public McpServerFeatures.SyncPromptSpecification createComponentFrom(Class cl } private List createPromptArguments(Method method) { - List parameters = ReflectionHelper.getParametersAnnotatedWith(method, McpPromptParam.class); - List promptArguments = new ArrayList<>(parameters.size()); - for (Parameter parameter : parameters) { - McpPromptParam promptParam = parameter.getAnnotation(McpPromptParam.class); + Stream parameters = Stream.of(method.getParameters()); + List params = parameters.filter(p -> p.isAnnotationPresent(McpPromptParam.class)).toList(); + List promptArguments = new ArrayList<>(params.size()); + for (Parameter param : params) { + McpPromptParam promptParam = param.getAnnotation(McpPromptParam.class); final String name = promptParam.name(); final String description = promptParam.description(); final boolean required = promptParam.required(); diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpSyncServerResourceRegister.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpSyncServerResourceRegister.java index 8ddb98f..32bafa7 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpSyncServerResourceRegister.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpSyncServerResourceRegister.java @@ -3,7 +3,6 @@ import com.github.codeboyzhou.mcp.declarative.annotation.McpResource; import com.github.codeboyzhou.mcp.declarative.annotation.McpResources; import com.github.codeboyzhou.mcp.declarative.util.JsonHelper; -import com.github.codeboyzhou.mcp.declarative.util.ReflectionHelper; import com.google.inject.Injector; import io.modelcontextprotocol.server.McpServerFeatures; import io.modelcontextprotocol.server.McpSyncServer; @@ -29,7 +28,8 @@ public void registerTo(McpSyncServer server) { Reflections reflections = injector.getInstance(Reflections.class); Set> resourceClasses = reflections.getTypesAnnotatedWith(McpResources.class); for (Class resourceClass : resourceClasses) { - List methods = ReflectionHelper.getMethodsAnnotatedWith(resourceClass, McpResource.class); + Set resourceMethods = reflections.getMethodsAnnotatedWith(McpResource.class); + List methods = resourceMethods.stream().filter(m -> m.getDeclaringClass() == resourceClass).toList(); for (Method method : methods) { McpServerFeatures.SyncResourceSpecification resource = createComponentFrom(resourceClass, method); server.addResource(resource); diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpSyncServerToolRegister.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpSyncServerToolRegister.java index b882fa5..1dacda7 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpSyncServerToolRegister.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpSyncServerToolRegister.java @@ -6,7 +6,6 @@ import com.github.codeboyzhou.mcp.declarative.annotation.McpToolParam; import com.github.codeboyzhou.mcp.declarative.annotation.McpTools; import com.github.codeboyzhou.mcp.declarative.util.JsonHelper; -import com.github.codeboyzhou.mcp.declarative.util.ReflectionHelper; import com.github.codeboyzhou.mcp.declarative.util.StringHelper; import com.github.codeboyzhou.mcp.declarative.util.TypeConverter; import com.google.inject.Injector; @@ -17,6 +16,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Parameter; import java.util.ArrayList; @@ -25,6 +25,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Stream; public class McpSyncServerToolRegister extends McpSyncServerComponentRegister { @@ -41,7 +42,8 @@ public void registerTo(McpSyncServer server) { Reflections reflections = injector.getInstance(Reflections.class); Set> toolClasses = reflections.getTypesAnnotatedWith(McpTools.class); for (Class toolClass : toolClasses) { - List methods = ReflectionHelper.getMethodsAnnotatedWith(toolClass, McpTool.class); + Set toolMethods = reflections.getMethodsAnnotatedWith(McpTool.class); + List methods = toolMethods.stream().filter(m -> m.getDeclaringClass() == toolClass).toList(); for (Method method : methods) { McpServerFeatures.SyncToolSpecification tool = createComponentFrom(toolClass, method); server.addTool(tool); @@ -78,11 +80,13 @@ private McpSchema.JsonSchema createJsonSchema(Method method) { Map definitions = new LinkedHashMap<>(); List required = new ArrayList<>(); - List parameters = ReflectionHelper.getParametersAnnotatedWith(method, McpToolParam.class); - for (Parameter parameter : parameters) { - McpToolParam toolParam = parameter.getAnnotation(McpToolParam.class); + Stream parameters = Stream.of(method.getParameters()); + List params = parameters.filter(p -> p.isAnnotationPresent(McpToolParam.class)).toList(); + + for (Parameter param : params) { + McpToolParam toolParam = param.getAnnotation(McpToolParam.class); final String parameterName = toolParam.name(); - Class parameterType = parameter.getType(); + Class parameterType = param.getType(); Map property = new HashMap<>(); if (parameterType.getAnnotation(McpJsonSchemaDefinition.class) == null) { @@ -112,10 +116,14 @@ private Map createJsonSchemaDefinition(Class definitionClass) Map properties = new LinkedHashMap<>(); List required = new ArrayList<>(); - ReflectionHelper.doWithFields(definitionClass, field -> { + Reflections reflections = injector.getInstance(Reflections.class); + Set definitionFields = reflections.getFieldsAnnotatedWith(McpJsonSchemaDefinitionProperty.class); + List fields = definitionFields.stream().filter(f -> f.getDeclaringClass() == definitionClass).toList(); + + for (Field field : fields) { McpJsonSchemaDefinitionProperty property = field.getAnnotation(McpJsonSchemaDefinitionProperty.class); if (property == null) { - return; + continue; } Map fieldProperties = new HashMap<>(); @@ -128,7 +136,7 @@ private Map createJsonSchemaDefinition(Class definitionClass) if (property.required()) { required.add(fieldName); } - }); + } definitionJsonSchema.put("properties", properties); definitionJsonSchema.put("required", required); diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/util/ReflectionHelper.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/util/ReflectionHelper.java deleted file mode 100644 index e4fda24..0000000 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/util/ReflectionHelper.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.github.codeboyzhou.mcp.declarative.util; - -import java.lang.annotation.Annotation; -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.lang.reflect.Parameter; -import java.util.List; -import java.util.function.Consumer; -import java.util.stream.Stream; - -public final class ReflectionHelper { - - public static List getMethodsAnnotatedWith(Class clazz, Class annotation) { - Method[] methods = clazz.getMethods(); - return Stream.of(methods).filter(m -> m.isAnnotationPresent(annotation)).toList(); - } - - public static List getParametersAnnotatedWith(Method method, Class annotation) { - Parameter[] parameters = method.getParameters(); - return Stream.of(parameters).filter(p -> p.isAnnotationPresent(annotation)).toList(); - } - - public static void doWithFields(Class clazz, Consumer consumer) { - Field[] declaredFields = clazz.getDeclaredFields(); - for (Field field : declaredFields) { - consumer.accept(field); - } - } - -} From e145e51f36fa5fb8f9aecdb93e668fb91afb635a Mon Sep 17 00:00:00 2001 From: codeboyzhou Date: Tue, 27 May 2025 01:47:09 +0800 Subject: [PATCH 36/57] refactor(package): Move server factory classes to new package --- .../mcp/declarative/McpServers.java | 6 ++--- .../declarative/enums/JsonSchemaDataType.java | 22 +++++++++++++++++++ .../ConfigurableMcpServerFactory.java | 2 +- .../ConfigurableMcpSyncServerFactory.java | 2 +- .../{ => factory}/McpServerFactory.java | 3 ++- .../{ => factory}/McpSyncServerFactory.java | 3 ++- .../register/McpSyncServerToolRegister.java | 7 +++--- .../mcp/declarative/util/TypeConverter.java | 10 +++++---- 8 files changed, 40 insertions(+), 15 deletions(-) create mode 100644 src/main/java/com/github/codeboyzhou/mcp/declarative/enums/JsonSchemaDataType.java rename src/main/java/com/github/codeboyzhou/mcp/declarative/server/{ => factory}/ConfigurableMcpServerFactory.java (82%) rename src/main/java/com/github/codeboyzhou/mcp/declarative/server/{ => factory}/ConfigurableMcpSyncServerFactory.java (97%) rename src/main/java/com/github/codeboyzhou/mcp/declarative/server/{ => factory}/McpServerFactory.java (78%) rename src/main/java/com/github/codeboyzhou/mcp/declarative/server/{ => factory}/McpSyncServerFactory.java (83%) diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/McpServers.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/McpServers.java index 5f17484..bd42621 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/McpServers.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/McpServers.java @@ -5,13 +5,13 @@ import com.github.codeboyzhou.mcp.declarative.exception.McpServerException; import com.github.codeboyzhou.mcp.declarative.listener.DefaultMcpSyncHttpServerStatusListener; import com.github.codeboyzhou.mcp.declarative.listener.McpHttpServerStatusListener; -import com.github.codeboyzhou.mcp.declarative.server.ConfigurableMcpSyncServerFactory; +import com.github.codeboyzhou.mcp.declarative.server.factory.ConfigurableMcpSyncServerFactory; import com.github.codeboyzhou.mcp.declarative.server.McpHttpServer; import com.github.codeboyzhou.mcp.declarative.server.register.McpServerComponentRegisters; -import com.github.codeboyzhou.mcp.declarative.server.McpServerFactory; +import com.github.codeboyzhou.mcp.declarative.server.factory.McpServerFactory; import com.github.codeboyzhou.mcp.declarative.server.McpServerInfo; import com.github.codeboyzhou.mcp.declarative.server.McpSseServerInfo; -import com.github.codeboyzhou.mcp.declarative.server.McpSyncServerFactory; +import com.github.codeboyzhou.mcp.declarative.server.factory.McpSyncServerFactory; import com.github.codeboyzhou.mcp.declarative.util.GuiceInjector; import com.github.codeboyzhou.mcp.declarative.util.JsonHelper; import com.google.inject.Guice; diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/enums/JsonSchemaDataType.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/enums/JsonSchemaDataType.java new file mode 100644 index 0000000..f852e20 --- /dev/null +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/enums/JsonSchemaDataType.java @@ -0,0 +1,22 @@ +package com.github.codeboyzhou.mcp.declarative.enums; + +public enum JsonSchemaDataType { + + STRING("string"), + NUMBER("number"), + INTEGER("integer"), + BOOLEAN("boolean"), + OBJECT("object"), + ; + + private final String type; + + JsonSchemaDataType(String type) { + this.type = type; + } + + public String getType() { + return type; + } + +} diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/ConfigurableMcpServerFactory.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/ConfigurableMcpServerFactory.java similarity index 82% rename from src/main/java/com/github/codeboyzhou/mcp/declarative/server/ConfigurableMcpServerFactory.java rename to src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/ConfigurableMcpServerFactory.java index a248aa2..f171820 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/ConfigurableMcpServerFactory.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/ConfigurableMcpServerFactory.java @@ -1,4 +1,4 @@ -package com.github.codeboyzhou.mcp.declarative.server; +package com.github.codeboyzhou.mcp.declarative.server.factory; import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpServerTransportProvider; diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/ConfigurableMcpSyncServerFactory.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/ConfigurableMcpSyncServerFactory.java similarity index 97% rename from src/main/java/com/github/codeboyzhou/mcp/declarative/server/ConfigurableMcpSyncServerFactory.java rename to src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/ConfigurableMcpSyncServerFactory.java index 8e78f4a..a6f3d59 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/ConfigurableMcpSyncServerFactory.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/ConfigurableMcpSyncServerFactory.java @@ -1,4 +1,4 @@ -package com.github.codeboyzhou.mcp.declarative.server; +package com.github.codeboyzhou.mcp.declarative.server.factory; import com.github.codeboyzhou.mcp.declarative.configuration.McpServerCapabilities; import com.github.codeboyzhou.mcp.declarative.configuration.McpServerChangeNotification; diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpServerFactory.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerFactory.java similarity index 78% rename from src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpServerFactory.java rename to src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerFactory.java index b0567ea..63046d9 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpServerFactory.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerFactory.java @@ -1,5 +1,6 @@ -package com.github.codeboyzhou.mcp.declarative.server; +package com.github.codeboyzhou.mcp.declarative.server.factory; +import com.github.codeboyzhou.mcp.declarative.server.McpServerInfo; import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpServerTransportProvider; diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerFactory.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpSyncServerFactory.java similarity index 83% rename from src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerFactory.java rename to src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpSyncServerFactory.java index 646bbef..e8915b8 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerFactory.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpSyncServerFactory.java @@ -1,5 +1,6 @@ -package com.github.codeboyzhou.mcp.declarative.server; +package com.github.codeboyzhou.mcp.declarative.server.factory; +import com.github.codeboyzhou.mcp.declarative.server.McpServerInfo; import io.modelcontextprotocol.server.McpServer; import io.modelcontextprotocol.server.McpSyncServer; import io.modelcontextprotocol.spec.McpServerTransportProvider; diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpSyncServerToolRegister.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpSyncServerToolRegister.java index 1dacda7..13a1a1a 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpSyncServerToolRegister.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpSyncServerToolRegister.java @@ -5,6 +5,7 @@ import com.github.codeboyzhou.mcp.declarative.annotation.McpTool; import com.github.codeboyzhou.mcp.declarative.annotation.McpToolParam; import com.github.codeboyzhou.mcp.declarative.annotation.McpTools; +import com.github.codeboyzhou.mcp.declarative.enums.JsonSchemaDataType; import com.github.codeboyzhou.mcp.declarative.util.JsonHelper; import com.github.codeboyzhou.mcp.declarative.util.StringHelper; import com.github.codeboyzhou.mcp.declarative.util.TypeConverter; @@ -31,8 +32,6 @@ public class McpSyncServerToolRegister extends McpSyncServerComponentRegister createJsonSchemaDefinition(Class definitionClass) { Map definitionJsonSchema = new HashMap<>(); - definitionJsonSchema.put("type", OBJECT_TYPE_NAME); + definitionJsonSchema.put("type", JsonSchemaDataType.OBJECT.getType()); Map properties = new LinkedHashMap<>(); List required = new ArrayList<>(); diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/util/TypeConverter.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/util/TypeConverter.java index f282e16..75c880f 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/util/TypeConverter.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/util/TypeConverter.java @@ -1,5 +1,7 @@ package com.github.codeboyzhou.mcp.declarative.util; +import com.github.codeboyzhou.mcp.declarative.enums.JsonSchemaDataType; + public final class TypeConverter { public static Object convert(Object value, Class targetType) { @@ -38,16 +40,16 @@ public static Object convert(Object value, String jsonSchemaType) { final String valueAsString = value.toString(); - if (String.class.getSimpleName().equalsIgnoreCase(jsonSchemaType)) { + if (JsonSchemaDataType.STRING.getType().equals(jsonSchemaType)) { return valueAsString; } - if (Integer.class.getSimpleName().equalsIgnoreCase(jsonSchemaType)) { + if (JsonSchemaDataType.INTEGER.getType().equals(jsonSchemaType)) { return Integer.parseInt(valueAsString); } - if (Number.class.getSimpleName().equalsIgnoreCase(jsonSchemaType)) { + if (JsonSchemaDataType.NUMBER.getType().equals(jsonSchemaType)) { return Double.parseDouble(valueAsString); } - if (Boolean.class.getSimpleName().equalsIgnoreCase(jsonSchemaType)) { + if (JsonSchemaDataType.BOOLEAN.getType().equals(jsonSchemaType)) { return Boolean.parseBoolean(valueAsString); } From 941ec2ce70a064353e8f1cd8989f02864155ff79 Mon Sep 17 00:00:00 2001 From: codeboyzhou Date: Tue, 27 May 2025 15:08:23 +0800 Subject: [PATCH 37/57] feat(configuration): Add support for hot-reload on configuration file changed --- README.md | 2 +- .../mcp/declarative/McpServers.java | 24 +--- .../YAMLConfigurationLoader.java | 112 ++++++++++++++++++ .../YamlConfigurationLoader.java | 54 --------- .../mcp/declarative/McpServersTest.java | 11 -- 5 files changed, 119 insertions(+), 84 deletions(-) create mode 100644 src/main/java/com/github/codeboyzhou/mcp/declarative/configuration/YAMLConfigurationLoader.java delete mode 100644 src/main/java/com/github/codeboyzhou/mcp/declarative/configuration/YamlConfigurationLoader.java diff --git a/README.md b/README.md index 2be3ae5..5832c5f 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ public class MyMcpServer { } ``` -This is a yaml configuration file example (named `mcp-server.yml` by default, or also `mcp-server.yaml`) only if you are using `startServer()` method: +This is a yaml configuration file example (named `mcp-server.yml` by default) only if you are using `startServer()` method: ```yaml enabled: true diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/McpServers.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/McpServers.java index bd42621..20bb7f5 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/McpServers.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/McpServers.java @@ -1,17 +1,16 @@ package com.github.codeboyzhou.mcp.declarative; import com.github.codeboyzhou.mcp.declarative.configuration.McpServerConfiguration; -import com.github.codeboyzhou.mcp.declarative.configuration.YamlConfigurationLoader; -import com.github.codeboyzhou.mcp.declarative.exception.McpServerException; +import com.github.codeboyzhou.mcp.declarative.configuration.YAMLConfigurationLoader; import com.github.codeboyzhou.mcp.declarative.listener.DefaultMcpSyncHttpServerStatusListener; import com.github.codeboyzhou.mcp.declarative.listener.McpHttpServerStatusListener; -import com.github.codeboyzhou.mcp.declarative.server.factory.ConfigurableMcpSyncServerFactory; import com.github.codeboyzhou.mcp.declarative.server.McpHttpServer; -import com.github.codeboyzhou.mcp.declarative.server.register.McpServerComponentRegisters; -import com.github.codeboyzhou.mcp.declarative.server.factory.McpServerFactory; import com.github.codeboyzhou.mcp.declarative.server.McpServerInfo; import com.github.codeboyzhou.mcp.declarative.server.McpSseServerInfo; +import com.github.codeboyzhou.mcp.declarative.server.factory.ConfigurableMcpSyncServerFactory; +import com.github.codeboyzhou.mcp.declarative.server.factory.McpServerFactory; import com.github.codeboyzhou.mcp.declarative.server.factory.McpSyncServerFactory; +import com.github.codeboyzhou.mcp.declarative.server.register.McpServerComponentRegisters; import com.github.codeboyzhou.mcp.declarative.util.GuiceInjector; import com.github.codeboyzhou.mcp.declarative.util.JsonHelper; import com.google.inject.Guice; @@ -24,8 +23,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.IOException; - public class McpServers { private static final Logger logger = LoggerFactory.getLogger(McpServers.class); @@ -63,20 +60,11 @@ public void startSyncSseServer(McpSseServerInfo serverInfo) { public void startServer(String configFileName) { Assert.notNull(configFileName, "configFileName must not be null"); - YamlConfigurationLoader configurationLoader = new YamlConfigurationLoader(); - McpServerConfiguration configuration; - try { - configuration = configurationLoader.load(configFileName); - doStartServer(configuration); - } catch (IOException e) { - throw new McpServerException("Error loading configuration file: " + e.getMessage(), e); - } + doStartServer(new YAMLConfigurationLoader(configFileName).getConfig()); } public void startServer() { - YamlConfigurationLoader configurationLoader = new YamlConfigurationLoader(); - McpServerConfiguration configuration = configurationLoader.loadConfiguration(); - doStartServer(configuration); + doStartServer(new YAMLConfigurationLoader().getConfig()); } private void doStartServer(McpServerConfiguration configuration) { diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/configuration/YAMLConfigurationLoader.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/configuration/YAMLConfigurationLoader.java new file mode 100644 index 0000000..57702ea --- /dev/null +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/configuration/YAMLConfigurationLoader.java @@ -0,0 +1,112 @@ +package com.github.codeboyzhou.mcp.declarative.configuration; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.FileSystems; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardWatchEventKinds; +import java.nio.file.WatchEvent; +import java.nio.file.WatchKey; +import java.nio.file.WatchService; +import java.util.List; + +public class YAMLConfigurationLoader { + + private static final Logger logger = LoggerFactory.getLogger(YAMLConfigurationLoader.class); + + private static final ObjectMapper YAML_MAPPER = new ObjectMapper(new YAMLFactory()); + + private static final String DEFAULT_CONFIG_FILE_NAME = "mcp-server-default.yml"; + + private static final String CONFIG_FILE_NAME = "mcp-server.yml"; + + private static final String WATCH_THREAD_NAME = "McpServerConfigFileWatcher"; + + private final String configFileName; + + private WatchService watchService; + + private McpServerConfiguration config; + + public YAMLConfigurationLoader(String configFileName) { + this.configFileName = configFileName; + initializeWatchService(); + loadConfig(); + } + + public YAMLConfigurationLoader() { + this(CONFIG_FILE_NAME); + } + + public McpServerConfiguration getConfig() { + return config; + } + + private Path getConfigFilePath(String fileName) { + try { + ClassLoader classLoader = YAMLConfigurationLoader.class.getClassLoader(); + URL configFileUrl = classLoader.getResource(fileName); + if (configFileUrl == null) { + configFileUrl = classLoader.getResource(DEFAULT_CONFIG_FILE_NAME); + } + assert configFileUrl != null; + return Paths.get(configFileUrl.toURI()); + } catch (URISyntaxException e) { + // should never happen + return null; + } + } + + private void initializeWatchService() { + try { + Path configFilePath = getConfigFilePath(configFileName); + assert configFilePath != null; + Path parentPath = configFilePath.getParent(); + watchService = FileSystems.getDefault().newWatchService(); + parentPath.register(watchService, StandardWatchEventKinds.ENTRY_MODIFY); + + Thread watchThread = new Thread(this::watchConfigFile, WATCH_THREAD_NAME); + watchThread.setDaemon(true); + watchThread.start(); + } catch (IOException e) { + logger.error("Failed to initialize configuration file watch service", e); + } + } + + private void watchConfigFile() { + try { + while (true) { + WatchKey watchKey = watchService.take(); + List> watchEvents = watchKey.pollEvents(); + for (WatchEvent event : watchEvents) { + if (event.context().toString().equals(configFileName)) { + loadConfig(); + } + } + watchKey.reset(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + logger.error("Configuration file watch service interrupted", e); + } + } + + private void loadConfig() { + try { + Path configFilePath = getConfigFilePath(configFileName); + assert configFilePath != null; + config = YAML_MAPPER.readValue(configFilePath.toFile(), McpServerConfiguration.class); + logger.info("Configuration loaded successfully"); + } catch (IOException e) { + logger.error("Failed to reload configuration", e); + } + } + +} diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/configuration/YamlConfigurationLoader.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/configuration/YamlConfigurationLoader.java deleted file mode 100644 index ee343ce..0000000 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/configuration/YamlConfigurationLoader.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.github.codeboyzhou.mcp.declarative.configuration; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.nio.file.NoSuchFileException; - -import static java.util.stream.Collectors.joining; - -public class YamlConfigurationLoader { - - private static final Logger logger = LoggerFactory.getLogger(YamlConfigurationLoader.class); - - private final ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); - - public McpServerConfiguration loadConfiguration() { - try { - return load("mcp-server.yml"); - } catch (IOException e) { - try { - return load("mcp-server.yaml"); - } catch (IOException ex) { - logger.warn("The mcp-server.yml and mcp-server.yaml were not found, will use default configuration"); - try { - return load("mcp-server-default.yml"); - } catch (IOException ignored) { - // should never happen - return null; - } - } - } - } - - public McpServerConfiguration load(String configFileName) throws IOException { - ClassLoader classLoader = YamlConfigurationLoader.class.getClassLoader(); - try (InputStream inputStream = classLoader.getResourceAsStream(configFileName)) { - if (inputStream == null) { - throw new NoSuchFileException(configFileName); - } - InputStreamReader inputStreamReader = new InputStreamReader(inputStream); - BufferedReader bufferedReader = new BufferedReader(inputStreamReader); - final String content = bufferedReader.lines().collect(joining(System.lineSeparator())); - logger.debug("Loaded configuration from {}:\n{}", configFileName, content); - return mapper.readValue(content, McpServerConfiguration.class); - } - } - -} diff --git a/src/test/java/com/github/codeboyzhou/mcp/declarative/McpServersTest.java b/src/test/java/com/github/codeboyzhou/mcp/declarative/McpServersTest.java index 89f4892..51028ee 100644 --- a/src/test/java/com/github/codeboyzhou/mcp/declarative/McpServersTest.java +++ b/src/test/java/com/github/codeboyzhou/mcp/declarative/McpServersTest.java @@ -3,7 +3,6 @@ import com.github.codeboyzhou.mcp.declarative.annotation.McpPrompts; import com.github.codeboyzhou.mcp.declarative.annotation.McpResources; import com.github.codeboyzhou.mcp.declarative.annotation.McpTools; -import com.github.codeboyzhou.mcp.declarative.exception.McpServerException; import com.github.codeboyzhou.mcp.declarative.server.McpServerInfo; import com.github.codeboyzhou.mcp.declarative.server.McpSseServerInfo; import com.github.codeboyzhou.mcp.declarative.server.TestMcpComponentScanBasePackageClass; @@ -31,7 +30,6 @@ import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; class McpServersTest { @@ -127,13 +125,4 @@ void testStartServerWithConfigFileName(String configFileName) { }); } - @Test - void testStartServerWithInvalidConfigFileName() { - McpServerException e = assertThrows(McpServerException.class, () -> { - McpServers servers = McpServers.run(TestMcpComponentScanIsNull.class, EMPTY_ARGS); - servers.startServer("mcp-server-not-exist.yml"); - }); - assertEquals("Error loading configuration file: mcp-server-not-exist.yml", e.getMessage()); - } - } From cf073eb52eb2f3b58ed57ff3c49097872b632028 Mon Sep 17 00:00:00 2001 From: codeboyzhou Date: Fri, 30 May 2025 12:55:43 +0800 Subject: [PATCH 38/57] perf(injector): Optimize Reflections to scan specific elements --- .../github/codeboyzhou/mcp/declarative/util/GuiceInjector.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/util/GuiceInjector.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/util/GuiceInjector.java index 38e1aec..4560eae 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/util/GuiceInjector.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/util/GuiceInjector.java @@ -9,6 +9,7 @@ import com.google.inject.Scopes; import com.google.inject.Singleton; import org.reflections.Reflections; +import org.reflections.scanners.Scanners; public final class GuiceInjector extends AbstractModule { @@ -24,7 +25,7 @@ public GuiceInjector(Class applicationMainClass) { public Reflections provideReflections() { McpComponentScan scan = applicationMainClass.getAnnotation(McpComponentScan.class); final String basePackage = determineBasePackage(scan, applicationMainClass); - return new Reflections(basePackage); + return new Reflections(basePackage, Scanners.TypesAnnotated, Scanners.MethodsAnnotated, Scanners.FieldsAnnotated); } @Override From b2fd5371de5f62aba595da8167390a667b18277a Mon Sep 17 00:00:00 2001 From: codeboyzhou Date: Fri, 30 May 2025 13:37:48 +0800 Subject: [PATCH 39/57] fix(register): Implement ComponentBufferQueue to serialize component registration, preventing Sinks.EmitResult.FAIL_NON_SERIALIZED during message transmission in the low-level MCP SDK --- pom.xml | 7 +++ .../server/register/ComponentBufferQueue.java | 53 +++++++++++++++++++ .../register/McpSyncServerPromptRegister.java | 4 +- .../McpSyncServerResourceRegister.java | 4 +- .../register/McpSyncServerToolRegister.java | 4 +- .../declarative/util/NamedThreadFactory.java | 25 +++++++++ 6 files changed, 94 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/ComponentBufferQueue.java create mode 100644 src/main/java/com/github/codeboyzhou/mcp/declarative/util/NamedThreadFactory.java diff --git a/pom.xml b/pom.xml index 7e1f4cf..64eee3d 100644 --- a/pom.xml +++ b/pom.xml @@ -51,6 +51,7 @@ 3.2.3 4.0.0 + 24.0.0 6.0.0 2.18.3 12.0.18 @@ -99,6 +100,12 @@ jetty-ee10-servlet ${jetty.version} + + org.jetbrains + annotations + ${annotations.version} + compile + org.junit.jupiter junit-jupiter diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/ComponentBufferQueue.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/ComponentBufferQueue.java new file mode 100644 index 0000000..09f1f08 --- /dev/null +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/ComponentBufferQueue.java @@ -0,0 +1,53 @@ +package com.github.codeboyzhou.mcp.declarative.server.register; + +import com.github.codeboyzhou.mcp.declarative.util.NamedThreadFactory; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; + +public class ComponentBufferQueue { + + private static final int DEFAULT_DELAYED_CONSUMPTION_MILLIS = 10; + + private final BlockingQueue queue = new LinkedBlockingQueue<>(); + + private final long delayMillis; + + public ComponentBufferQueue(long delayMillis) { + if (delayMillis <= 0) { + throw new IllegalArgumentException("delayMillis must be greater than 0"); + } + this.delayMillis = delayMillis; + } + + public ComponentBufferQueue() { + this(DEFAULT_DELAYED_CONSUMPTION_MILLIS); + } + + public void submit(R component) { + try { + queue.put(component); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + public void consume(T server, BiConsumer consumer) { + NamedThreadFactory threadFactory = new NamedThreadFactory(getClass().getSimpleName()); + Executors.newSingleThreadExecutor(threadFactory).execute(() -> { + try { + while (!Thread.interrupted()) { + R component = queue.take(); + consumer.accept(server, component); + TimeUnit.MILLISECONDS.sleep(delayMillis); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + } + +} diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpSyncServerPromptRegister.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpSyncServerPromptRegister.java index 8c95276..e4b4cbb 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpSyncServerPromptRegister.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpSyncServerPromptRegister.java @@ -34,14 +34,16 @@ protected McpSyncServerPromptRegister(Injector injector) { public void registerTo(McpSyncServer server) { Reflections reflections = injector.getInstance(Reflections.class); Set> promptClasses = reflections.getTypesAnnotatedWith(McpPrompts.class); + ComponentBufferQueue queue = new ComponentBufferQueue<>(); for (Class promptClass : promptClasses) { Set promptMethods = reflections.getMethodsAnnotatedWith(McpPrompt.class); List methods = promptMethods.stream().filter(m -> m.getDeclaringClass() == promptClass).toList(); for (Method method : methods) { McpServerFeatures.SyncPromptSpecification prompt = createComponentFrom(promptClass, method); - server.addPrompt(prompt); + queue.submit(prompt); } } + queue.consume(server, McpSyncServer::addPrompt); } @Override diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpSyncServerResourceRegister.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpSyncServerResourceRegister.java index 32bafa7..dd14aa7 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpSyncServerResourceRegister.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpSyncServerResourceRegister.java @@ -27,14 +27,16 @@ protected McpSyncServerResourceRegister(Injector injector) { public void registerTo(McpSyncServer server) { Reflections reflections = injector.getInstance(Reflections.class); Set> resourceClasses = reflections.getTypesAnnotatedWith(McpResources.class); + ComponentBufferQueue queue = new ComponentBufferQueue<>(); for (Class resourceClass : resourceClasses) { Set resourceMethods = reflections.getMethodsAnnotatedWith(McpResource.class); List methods = resourceMethods.stream().filter(m -> m.getDeclaringClass() == resourceClass).toList(); for (Method method : methods) { McpServerFeatures.SyncResourceSpecification resource = createComponentFrom(resourceClass, method); - server.addResource(resource); + queue.submit(resource); } } + queue.consume(server, McpSyncServer::addResource); } @Override diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpSyncServerToolRegister.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpSyncServerToolRegister.java index 13a1a1a..9468fe6 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpSyncServerToolRegister.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpSyncServerToolRegister.java @@ -40,14 +40,16 @@ protected McpSyncServerToolRegister(Injector injector) { public void registerTo(McpSyncServer server) { Reflections reflections = injector.getInstance(Reflections.class); Set> toolClasses = reflections.getTypesAnnotatedWith(McpTools.class); + ComponentBufferQueue queue = new ComponentBufferQueue<>(); for (Class toolClass : toolClasses) { Set toolMethods = reflections.getMethodsAnnotatedWith(McpTool.class); List methods = toolMethods.stream().filter(m -> m.getDeclaringClass() == toolClass).toList(); for (Method method : methods) { McpServerFeatures.SyncToolSpecification tool = createComponentFrom(toolClass, method); - server.addTool(tool); + queue.submit(tool); } } + queue.consume(server, McpSyncServer::addTool); } @Override diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/util/NamedThreadFactory.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/util/NamedThreadFactory.java new file mode 100644 index 0000000..9dccf8d --- /dev/null +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/util/NamedThreadFactory.java @@ -0,0 +1,25 @@ +package com.github.codeboyzhou.mcp.declarative.util; + +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; + +public final class NamedThreadFactory implements ThreadFactory { + + private static final AtomicInteger poolNumber = new AtomicInteger(1); + + private final AtomicInteger threadNumber = new AtomicInteger(1); + + private final String namePrefix; + + public NamedThreadFactory(String namePrefix) { + this.namePrefix = namePrefix + "-" + poolNumber.getAndIncrement() + "-thread-"; + } + + @Override + public Thread newThread(@NotNull Runnable runnable) { + return new Thread(runnable, namePrefix + threadNumber.getAndIncrement()); + } + +} From c90fcaeb17a53d43f807c14a23b2451957acbdc3 Mon Sep 17 00:00:00 2001 From: codeboyzhou Date: Wed, 4 Jun 2025 00:53:33 +0800 Subject: [PATCH 40/57] refactor(server): Implement abstract server factory and simplify server creation --- .../mcp/declarative/McpServers.java | 28 ++++------- .../mcp/declarative/server/McpHttpServer.java | 46 ++++--------------- .../factory/AbstractMcpServerFactory.java | 17 +++++++ .../factory/McpHttpSseServerFactory.java | 34 ++++++++++++++ .../server/factory/McpServerFactory.java | 14 ++---- .../server/factory/McpStdioServerFactory.java | 26 +++++++++++ .../server/factory/McpSyncServerFactory.java | 20 -------- 7 files changed, 98 insertions(+), 87 deletions(-) create mode 100644 src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/AbstractMcpServerFactory.java create mode 100644 src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpHttpSseServerFactory.java create mode 100644 src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpStdioServerFactory.java delete mode 100644 src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpSyncServerFactory.java diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/McpServers.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/McpServers.java index 20bb7f5..c0d2e1f 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/McpServers.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/McpServers.java @@ -4,21 +4,17 @@ import com.github.codeboyzhou.mcp.declarative.configuration.YAMLConfigurationLoader; import com.github.codeboyzhou.mcp.declarative.listener.DefaultMcpSyncHttpServerStatusListener; import com.github.codeboyzhou.mcp.declarative.listener.McpHttpServerStatusListener; -import com.github.codeboyzhou.mcp.declarative.server.McpHttpServer; import com.github.codeboyzhou.mcp.declarative.server.McpServerInfo; import com.github.codeboyzhou.mcp.declarative.server.McpSseServerInfo; import com.github.codeboyzhou.mcp.declarative.server.factory.ConfigurableMcpSyncServerFactory; -import com.github.codeboyzhou.mcp.declarative.server.factory.McpServerFactory; -import com.github.codeboyzhou.mcp.declarative.server.factory.McpSyncServerFactory; +import com.github.codeboyzhou.mcp.declarative.server.factory.McpHttpSseServerFactory; +import com.github.codeboyzhou.mcp.declarative.server.factory.McpStdioServerFactory; import com.github.codeboyzhou.mcp.declarative.server.register.McpServerComponentRegisters; import com.github.codeboyzhou.mcp.declarative.util.GuiceInjector; -import com.github.codeboyzhou.mcp.declarative.util.JsonHelper; import com.google.inject.Guice; import com.google.inject.Injector; +import io.modelcontextprotocol.server.McpAsyncServer; import io.modelcontextprotocol.server.McpSyncServer; -import io.modelcontextprotocol.server.transport.HttpServletSseServerTransportProvider; -import io.modelcontextprotocol.server.transport.StdioServerTransportProvider; -import io.modelcontextprotocol.spec.McpServerTransportProvider; import io.modelcontextprotocol.util.Assert; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -37,21 +33,15 @@ public static McpServers run(Class applicationMainClass, String[] args) { } public void startSyncStdioServer(McpServerInfo serverInfo) { - McpServerFactory factory = new McpSyncServerFactory(); - McpServerTransportProvider transportProvider = new StdioServerTransportProvider(); - McpSyncServer server = factory.create(serverInfo, transportProvider); - new McpServerComponentRegisters(injector).registerAllTo(server); + McpStdioServerFactory factory = new McpStdioServerFactory(); + McpAsyncServer server = factory.create(serverInfo); + new McpServerComponentRegisters(injector).registerAllTo(new McpSyncServer(server)); } public void startSyncSseServer(McpSseServerInfo serverInfo, McpHttpServerStatusListener listener) { - McpServerFactory factory = new McpSyncServerFactory(); - HttpServletSseServerTransportProvider transportProvider = new HttpServletSseServerTransportProvider( - JsonHelper.MAPPER, serverInfo.baseUrl(), serverInfo.messageEndpoint(), serverInfo.sseEndpoint() - ); - McpSyncServer server = factory.create(serverInfo, transportProvider); - new McpServerComponentRegisters(injector).registerAllTo(server); - McpHttpServer httpServer = new McpHttpServer<>(); - httpServer.with(transportProvider).with(serverInfo).with(listener).attach(server).start(); + McpHttpSseServerFactory factory = new McpHttpSseServerFactory(); + McpAsyncServer server = factory.create(serverInfo); + new McpServerComponentRegisters(injector).registerAllTo(new McpSyncServer(server)); } public void startSyncSseServer(McpSseServerInfo serverInfo) { diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpHttpServer.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpHttpServer.java index 158534d..22b69f7 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpHttpServer.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpHttpServer.java @@ -1,8 +1,6 @@ package com.github.codeboyzhou.mcp.declarative.server; -import com.github.codeboyzhou.mcp.declarative.listener.McpHttpServerStatusListener; import io.modelcontextprotocol.server.transport.HttpServletSseServerTransportProvider; -import io.modelcontextprotocol.util.Assert; import org.eclipse.jetty.ee10.servlet.ServletContextHandler; import org.eclipse.jetty.ee10.servlet.ServletHolder; import org.eclipse.jetty.server.Server; @@ -11,7 +9,7 @@ import java.time.Duration; -public class McpHttpServer { +public class McpHttpServer { private static final Logger logger = LoggerFactory.getLogger(McpHttpServer.class); @@ -19,36 +17,13 @@ public class McpHttpServer { private static final String DEFAULT_SERVLET_PATH = "/*"; - private HttpServletSseServerTransportProvider transportProvider; + private final HttpServletSseServerTransportProvider transportProvider; - private McpSseServerInfo serverInfo; + private final int port; - private McpHttpServerStatusListener statusListener; - - private T mcpServer; - - public McpHttpServer with(HttpServletSseServerTransportProvider transportProvider) { - Assert.notNull(transportProvider, "transportProvider cannot be null"); + public McpHttpServer(HttpServletSseServerTransportProvider transportProvider, int port) { this.transportProvider = transportProvider; - return this; - } - - public McpHttpServer with(McpSseServerInfo serverInfo) { - Assert.notNull(serverInfo, "serverInfo cannot be null"); - this.serverInfo = serverInfo; - return this; - } - - public McpHttpServer with(McpHttpServerStatusListener statusListener) { - Assert.notNull(statusListener, "statusListener cannot be null"); - this.statusListener = statusListener; - return this; - } - - public McpHttpServer attach(T mcpServer) { - Assert.notNull(mcpServer, "mcpServer cannot be null"); - this.mcpServer = mcpServer; - return this; + this.port = port; } public void start() { @@ -58,17 +33,14 @@ public void start() { ServletHolder servletHolder = new ServletHolder(transportProvider); handler.addServlet(servletHolder, DEFAULT_SERVLET_PATH); - Server httpserver = new Server(serverInfo.port()); + Server httpserver = new Server(port); httpserver.setHandler(handler); httpserver.setStopAtShutdown(true); httpserver.setStopTimeout(Duration.ofSeconds(5).getSeconds()); try { httpserver.start(); - logger.info("Jetty-based HTTP server started on http://127.0.0.1:{}", serverInfo.port()); - - // Notify the listener that the server has started - statusListener.onStarted(mcpServer); + logger.info("Jetty-based HTTP server started on http://127.0.0.1:{}", port); // Add a shutdown hook to stop the HTTP server and MCP server gracefully addShutdownHook(httpserver); @@ -84,8 +56,7 @@ public void start() { // Wait for the HTTP server to stop httpserver.join(); } catch (Exception e) { - logger.error("Error starting HTTP server on http://127.0.0.1:{}", serverInfo.port(), e); - statusListener.onError(mcpServer, e); + logger.error("Error starting HTTP server on http://127.0.0.1:{}", port, e); } } @@ -94,7 +65,6 @@ private void addShutdownHook(Server httpserver) { try { logger.info("Shutting down HTTP server and MCP server"); httpserver.stop(); - statusListener.onStopped(mcpServer); } catch (Exception e) { logger.error("Error stopping HTTP server and MCP server", e); } diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/AbstractMcpServerFactory.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/AbstractMcpServerFactory.java new file mode 100644 index 0000000..78fa26c --- /dev/null +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/AbstractMcpServerFactory.java @@ -0,0 +1,17 @@ +package com.github.codeboyzhou.mcp.declarative.server.factory; + +import com.github.codeboyzhou.mcp.declarative.server.McpServerInfo; +import io.modelcontextprotocol.spec.McpSchema; +import io.modelcontextprotocol.spec.McpServerTransportProvider; + +public abstract class AbstractMcpServerFactory implements McpServerFactory { + + protected McpSchema.ServerCapabilities serverCapabilities() { + return McpSchema.ServerCapabilities.builder() + .resources(true, true) + .prompts(true) + .tools(true) + .build(); + } + +} diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpHttpSseServerFactory.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpHttpSseServerFactory.java new file mode 100644 index 0000000..ab6bf75 --- /dev/null +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpHttpSseServerFactory.java @@ -0,0 +1,34 @@ +package com.github.codeboyzhou.mcp.declarative.server.factory; + +import com.github.codeboyzhou.mcp.declarative.server.McpHttpServer; +import com.github.codeboyzhou.mcp.declarative.server.McpSseServerInfo; +import com.github.codeboyzhou.mcp.declarative.util.JsonHelper; +import io.modelcontextprotocol.server.McpAsyncServer; +import io.modelcontextprotocol.server.McpServer; +import io.modelcontextprotocol.server.transport.HttpServletSseServerTransportProvider; + +public class McpHttpSseServerFactory extends AbstractMcpServerFactory { + + @Override + public HttpServletSseServerTransportProvider transportProvider(McpSseServerInfo serverInfo) { + final String baseUrl = serverInfo.baseUrl(); + final String messageEndpoint = serverInfo.messageEndpoint(); + final String sseEndpoint = serverInfo.sseEndpoint(); + return new HttpServletSseServerTransportProvider(JsonHelper.MAPPER, baseUrl, messageEndpoint, sseEndpoint); + } + + @Override + public McpAsyncServer create(McpSseServerInfo serverInfo) { + HttpServletSseServerTransportProvider transportProvider = transportProvider(serverInfo); + McpAsyncServer server = McpServer.async(transportProvider) + .serverInfo(serverInfo.name(), serverInfo.version()) + .capabilities(serverCapabilities()) + .instructions(serverInfo.instructions()) + .requestTimeout(serverInfo.requestTimeout()) + .build(); + McpHttpServer httpServer = new McpHttpServer(transportProvider, serverInfo.port()); + httpServer.start(); + return server; + } + +} diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerFactory.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerFactory.java index 63046d9..6d881d8 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerFactory.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerFactory.java @@ -1,19 +1,13 @@ package com.github.codeboyzhou.mcp.declarative.server.factory; import com.github.codeboyzhou.mcp.declarative.server.McpServerInfo; -import io.modelcontextprotocol.spec.McpSchema; +import io.modelcontextprotocol.server.McpAsyncServer; import io.modelcontextprotocol.spec.McpServerTransportProvider; -public interface McpServerFactory { +public interface McpServerFactory { - T create(McpServerInfo serverInfo, McpServerTransportProvider transportProvider); + T transportProvider(S serverInfo); - default McpSchema.ServerCapabilities configureServerCapabilities() { - return McpSchema.ServerCapabilities.builder() - .resources(true, true) - .prompts(true) - .tools(true) - .build(); - } + McpAsyncServer create(S serverInfo); } diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpStdioServerFactory.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpStdioServerFactory.java new file mode 100644 index 0000000..ff4722d --- /dev/null +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpStdioServerFactory.java @@ -0,0 +1,26 @@ +package com.github.codeboyzhou.mcp.declarative.server.factory; + +import com.github.codeboyzhou.mcp.declarative.server.McpServerInfo; +import io.modelcontextprotocol.server.McpAsyncServer; +import io.modelcontextprotocol.server.McpServer; +import io.modelcontextprotocol.server.transport.StdioServerTransportProvider; +import io.modelcontextprotocol.spec.McpServerTransportProvider; + +public class McpStdioServerFactory extends AbstractMcpServerFactory { + + @Override + public McpServerTransportProvider transportProvider(McpServerInfo serverInfo) { + return new StdioServerTransportProvider(); + } + + @Override + public McpAsyncServer create(McpServerInfo serverInfo) { + return McpServer.async(transportProvider(serverInfo)) + .serverInfo(serverInfo.name(), serverInfo.version()) + .capabilities(serverCapabilities()) + .instructions(serverInfo.instructions()) + .requestTimeout(serverInfo.requestTimeout()) + .build(); + } + +} diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpSyncServerFactory.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpSyncServerFactory.java deleted file mode 100644 index e8915b8..0000000 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpSyncServerFactory.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.github.codeboyzhou.mcp.declarative.server.factory; - -import com.github.codeboyzhou.mcp.declarative.server.McpServerInfo; -import io.modelcontextprotocol.server.McpServer; -import io.modelcontextprotocol.server.McpSyncServer; -import io.modelcontextprotocol.spec.McpServerTransportProvider; - -public class McpSyncServerFactory implements McpServerFactory { - - @Override - public McpSyncServer create(McpServerInfo serverInfo, McpServerTransportProvider transportProvider) { - return McpServer.sync(transportProvider) - .instructions(serverInfo.instructions()) - .capabilities(configureServerCapabilities()) - .serverInfo(serverInfo.name(), serverInfo.version()) - .requestTimeout(serverInfo.requestTimeout()) - .build(); - } - -} From 2cee774360f7a4c550a40ec4b630318ce06ddda2 Mon Sep 17 00:00:00 2001 From: codeboyzhou Date: Wed, 4 Jun 2025 08:14:00 +0800 Subject: [PATCH 41/57] refactor(server): Enhance server component registration and introduce new factories for improved server management --- .../mcp/declarative/McpServers.java | 44 +++++++++++------- .../BufferQueue.java} | 28 +++++++----- .../GuiceInjectorModule.java} | 29 ++++++++---- .../{util => common}/NamedThreadFactory.java | 2 +- ...AbstractConfigurableMcpServerFactory.java} | 32 ++++--------- .../AbstractMcpServerComponentFactory.java | 13 ++++++ .../factory/AbstractMcpServerFactory.java | 3 +- .../ConfigurableMcpHttpSseServerFactory.java | 23 ++++++++++ .../factory/ConfigurableMcpServerFactory.java | 9 ++-- .../ConfigurableMcpStdioServerFactory.java | 18 ++++++++ .../factory/McpServerComponentFactory.java | 13 ++++++ .../server/factory/McpServerFactory.java | 3 ++ .../McpServerPromptFactory.java} | 45 ++++++++++--------- .../McpServerResourceFactory.java} | 45 ++++++++++--------- .../McpServerToolFactory.java} | 45 ++++++++++--------- .../register/McpServerComponentRegister.java | 10 ----- .../register/McpServerComponentRegisters.java | 20 --------- .../McpSyncServerComponentRegister.java | 14 ------ .../mcp/declarative/McpServersTest.java | 4 +- 19 files changed, 227 insertions(+), 173 deletions(-) rename src/main/java/com/github/codeboyzhou/mcp/declarative/{server/register/ComponentBufferQueue.java => common/BufferQueue.java} (57%) rename src/main/java/com/github/codeboyzhou/mcp/declarative/{util/GuiceInjector.java => common/GuiceInjectorModule.java} (56%) rename src/main/java/com/github/codeboyzhou/mcp/declarative/{util => common}/NamedThreadFactory.java (92%) rename src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/{ConfigurableMcpSyncServerFactory.java => AbstractConfigurableMcpServerFactory.java} (55%) create mode 100644 src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/AbstractMcpServerComponentFactory.java create mode 100644 src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/ConfigurableMcpHttpSseServerFactory.java create mode 100644 src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/ConfigurableMcpStdioServerFactory.java create mode 100644 src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerComponentFactory.java rename src/main/java/com/github/codeboyzhou/mcp/declarative/server/{register/McpSyncServerPromptRegister.java => factory/McpServerPromptFactory.java} (88%) rename src/main/java/com/github/codeboyzhou/mcp/declarative/server/{register/McpSyncServerResourceRegister.java => factory/McpServerResourceFactory.java} (78%) rename src/main/java/com/github/codeboyzhou/mcp/declarative/server/{register/McpSyncServerToolRegister.java => factory/McpServerToolFactory.java} (92%) delete mode 100644 src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpServerComponentRegister.java delete mode 100644 src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpServerComponentRegisters.java delete mode 100644 src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpSyncServerComponentRegister.java diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/McpServers.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/McpServers.java index c0d2e1f..2f6de38 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/McpServers.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/McpServers.java @@ -1,20 +1,25 @@ package com.github.codeboyzhou.mcp.declarative; +import com.github.codeboyzhou.mcp.declarative.common.GuiceInjectorModule; import com.github.codeboyzhou.mcp.declarative.configuration.McpServerConfiguration; import com.github.codeboyzhou.mcp.declarative.configuration.YAMLConfigurationLoader; import com.github.codeboyzhou.mcp.declarative.listener.DefaultMcpSyncHttpServerStatusListener; import com.github.codeboyzhou.mcp.declarative.listener.McpHttpServerStatusListener; import com.github.codeboyzhou.mcp.declarative.server.McpServerInfo; import com.github.codeboyzhou.mcp.declarative.server.McpSseServerInfo; -import com.github.codeboyzhou.mcp.declarative.server.factory.ConfigurableMcpSyncServerFactory; +import com.github.codeboyzhou.mcp.declarative.server.factory.ConfigurableMcpHttpSseServerFactory; +import com.github.codeboyzhou.mcp.declarative.server.factory.ConfigurableMcpServerFactory; +import com.github.codeboyzhou.mcp.declarative.server.factory.ConfigurableMcpStdioServerFactory; import com.github.codeboyzhou.mcp.declarative.server.factory.McpHttpSseServerFactory; +import com.github.codeboyzhou.mcp.declarative.server.factory.McpServerPromptFactory; +import com.github.codeboyzhou.mcp.declarative.server.factory.McpServerResourceFactory; +import com.github.codeboyzhou.mcp.declarative.server.factory.McpServerToolFactory; import com.github.codeboyzhou.mcp.declarative.server.factory.McpStdioServerFactory; -import com.github.codeboyzhou.mcp.declarative.server.register.McpServerComponentRegisters; -import com.github.codeboyzhou.mcp.declarative.util.GuiceInjector; import com.google.inject.Guice; import com.google.inject.Injector; import io.modelcontextprotocol.server.McpAsyncServer; import io.modelcontextprotocol.server.McpSyncServer; +import io.modelcontextprotocol.spec.McpServerTransportProvider; import io.modelcontextprotocol.util.Assert; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -28,20 +33,20 @@ public class McpServers { private static Injector injector; public static McpServers run(Class applicationMainClass, String[] args) { - injector = Guice.createInjector(new GuiceInjector(applicationMainClass)); + injector = Guice.createInjector(new GuiceInjectorModule(applicationMainClass)); return INSTANCE; } public void startSyncStdioServer(McpServerInfo serverInfo) { McpStdioServerFactory factory = new McpStdioServerFactory(); McpAsyncServer server = factory.create(serverInfo); - new McpServerComponentRegisters(injector).registerAllTo(new McpSyncServer(server)); + registerComponents(server); } public void startSyncSseServer(McpSseServerInfo serverInfo, McpHttpServerStatusListener listener) { McpHttpSseServerFactory factory = new McpHttpSseServerFactory(); McpAsyncServer server = factory.create(serverInfo); - new McpServerComponentRegisters(injector).registerAllTo(new McpSyncServer(server)); + registerComponents(server); } public void startSyncSseServer(McpSseServerInfo serverInfo) { @@ -58,17 +63,26 @@ public void startServer() { } private void doStartServer(McpServerConfiguration configuration) { - if (configuration.enabled()) { - McpSyncServer server = new ConfigurableMcpSyncServerFactory(configuration).create(); - new McpServerComponentRegisters(injector).registerAllTo(server); - if (configuration.stdio()) { - startSyncStdioServer(McpServerInfo.from(configuration)); - } else { - startSyncSseServer(McpSseServerInfo.from(configuration)); - } + if (!configuration.enabled()) { + logger.warn("MCP server is disabled, please check your configuration file."); + return; + } + + ConfigurableMcpServerFactory factory; + if (configuration.stdio()) { + factory = new ConfigurableMcpStdioServerFactory(configuration); } else { - logger.info("MCP server is disabled."); + factory = new ConfigurableMcpHttpSseServerFactory(configuration); } + McpAsyncServer server = factory.create(); + registerComponents(server); + } + + private void registerComponents(McpAsyncServer server) { + McpSyncServer syncServer = new McpSyncServer(server); + injector.getInstance(McpServerResourceFactory.class).registerTo(syncServer); + injector.getInstance(McpServerPromptFactory.class).registerTo(syncServer); + injector.getInstance(McpServerToolFactory.class).registerTo(syncServer); } } diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/ComponentBufferQueue.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/common/BufferQueue.java similarity index 57% rename from src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/ComponentBufferQueue.java rename to src/main/java/com/github/codeboyzhou/mcp/declarative/common/BufferQueue.java index 09f1f08..ce33b40 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/ComponentBufferQueue.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/common/BufferQueue.java @@ -1,47 +1,53 @@ -package com.github.codeboyzhou.mcp.declarative.server.register; +package com.github.codeboyzhou.mcp.declarative.common; -import com.github.codeboyzhou.mcp.declarative.util.NamedThreadFactory; +import com.github.codeboyzhou.mcp.declarative.util.JsonHelper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.concurrent.BlockingQueue; import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; -import java.util.function.BiConsumer; +import java.util.function.Consumer; -public class ComponentBufferQueue { +public class BufferQueue { + + private static final Logger logger = LoggerFactory.getLogger(BufferQueue.class); private static final int DEFAULT_DELAYED_CONSUMPTION_MILLIS = 10; - private final BlockingQueue queue = new LinkedBlockingQueue<>(); + private final BlockingQueue queue = new LinkedBlockingQueue<>(); private final long delayMillis; - public ComponentBufferQueue(long delayMillis) { + public BufferQueue(long delayMillis) { if (delayMillis <= 0) { throw new IllegalArgumentException("delayMillis must be greater than 0"); } this.delayMillis = delayMillis; } - public ComponentBufferQueue() { + public BufferQueue() { this(DEFAULT_DELAYED_CONSUMPTION_MILLIS); } - public void submit(R component) { + public void submit(T component) { try { queue.put(component); + logger.debug("Component submitted to queue: {}", JsonHelper.toJson(component)); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } - public void consume(T server, BiConsumer consumer) { + public void consume(Consumer consumer) { NamedThreadFactory threadFactory = new NamedThreadFactory(getClass().getSimpleName()); Executors.newSingleThreadExecutor(threadFactory).execute(() -> { try { while (!Thread.interrupted()) { - R component = queue.take(); - consumer.accept(server, component); + T component = queue.take(); + consumer.accept(component); + logger.debug("Component consumed from queue: {}", JsonHelper.toJson(component)); TimeUnit.MILLISECONDS.sleep(delayMillis); } } catch (InterruptedException e) { diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/util/GuiceInjector.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/common/GuiceInjectorModule.java similarity index 56% rename from src/main/java/com/github/codeboyzhou/mcp/declarative/util/GuiceInjector.java rename to src/main/java/com/github/codeboyzhou/mcp/declarative/common/GuiceInjectorModule.java index 4560eae..68c8c56 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/util/GuiceInjector.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/common/GuiceInjectorModule.java @@ -1,21 +1,27 @@ -package com.github.codeboyzhou.mcp.declarative.util; +package com.github.codeboyzhou.mcp.declarative.common; import com.github.codeboyzhou.mcp.declarative.annotation.McpComponentScan; import com.github.codeboyzhou.mcp.declarative.annotation.McpPrompts; import com.github.codeboyzhou.mcp.declarative.annotation.McpResources; import com.github.codeboyzhou.mcp.declarative.annotation.McpTools; +import com.github.codeboyzhou.mcp.declarative.server.factory.McpServerPromptFactory; +import com.github.codeboyzhou.mcp.declarative.server.factory.McpServerResourceFactory; +import com.github.codeboyzhou.mcp.declarative.server.factory.McpServerToolFactory; import com.google.inject.AbstractModule; import com.google.inject.Provides; -import com.google.inject.Scopes; import com.google.inject.Singleton; import org.reflections.Reflections; -import org.reflections.scanners.Scanners; -public final class GuiceInjector extends AbstractModule { +import static com.google.inject.Scopes.SINGLETON; +import static org.reflections.scanners.Scanners.FieldsAnnotated; +import static org.reflections.scanners.Scanners.MethodsAnnotated; +import static org.reflections.scanners.Scanners.TypesAnnotated; + +public final class GuiceInjectorModule extends AbstractModule { private final Class applicationMainClass; - public GuiceInjector(Class applicationMainClass) { + public GuiceInjectorModule(Class applicationMainClass) { this.applicationMainClass = applicationMainClass; } @@ -25,15 +31,20 @@ public GuiceInjector(Class applicationMainClass) { public Reflections provideReflections() { McpComponentScan scan = applicationMainClass.getAnnotation(McpComponentScan.class); final String basePackage = determineBasePackage(scan, applicationMainClass); - return new Reflections(basePackage, Scanners.TypesAnnotated, Scanners.MethodsAnnotated, Scanners.FieldsAnnotated); + return new Reflections(basePackage, TypesAnnotated, MethodsAnnotated, FieldsAnnotated); } @Override protected void configure() { + // Bind classes annotated by McpResources, McpPrompts, McpTools Reflections reflections = provideReflections(); - reflections.getTypesAnnotatedWith(McpResources.class).forEach(clazz -> bind(clazz).in(Scopes.SINGLETON)); - reflections.getTypesAnnotatedWith(McpPrompts.class).forEach(clazz -> bind(clazz).in(Scopes.SINGLETON)); - reflections.getTypesAnnotatedWith(McpTools.class).forEach(clazz -> bind(clazz).in(Scopes.SINGLETON)); + reflections.getTypesAnnotatedWith(McpResources.class).forEach(clazz -> bind(clazz).in(SINGLETON)); + reflections.getTypesAnnotatedWith(McpPrompts.class).forEach(clazz -> bind(clazz).in(SINGLETON)); + reflections.getTypesAnnotatedWith(McpTools.class).forEach(clazz -> bind(clazz).in(SINGLETON)); + // Bind all implementations of McpServerComponentFactory + bind(McpServerResourceFactory.class).in(SINGLETON); + bind(McpServerPromptFactory.class).in(SINGLETON); + bind(McpServerToolFactory.class).in(SINGLETON); } private String determineBasePackage(McpComponentScan scan, Class applicationMainClass) { diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/util/NamedThreadFactory.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/common/NamedThreadFactory.java similarity index 92% rename from src/main/java/com/github/codeboyzhou/mcp/declarative/util/NamedThreadFactory.java rename to src/main/java/com/github/codeboyzhou/mcp/declarative/common/NamedThreadFactory.java index 9dccf8d..fdef89a 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/util/NamedThreadFactory.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/common/NamedThreadFactory.java @@ -1,4 +1,4 @@ -package com.github.codeboyzhou.mcp.declarative.util; +package com.github.codeboyzhou.mcp.declarative.common; import org.jetbrains.annotations.NotNull; diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/ConfigurableMcpSyncServerFactory.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/AbstractConfigurableMcpServerFactory.java similarity index 55% rename from src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/ConfigurableMcpSyncServerFactory.java rename to src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/AbstractConfigurableMcpServerFactory.java index a6f3d59..cda934a 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/ConfigurableMcpSyncServerFactory.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/AbstractConfigurableMcpServerFactory.java @@ -3,47 +3,33 @@ import com.github.codeboyzhou.mcp.declarative.configuration.McpServerCapabilities; import com.github.codeboyzhou.mcp.declarative.configuration.McpServerChangeNotification; import com.github.codeboyzhou.mcp.declarative.configuration.McpServerConfiguration; -import com.github.codeboyzhou.mcp.declarative.configuration.McpServerSSE; -import com.github.codeboyzhou.mcp.declarative.util.JsonHelper; +import io.modelcontextprotocol.server.McpAsyncServer; import io.modelcontextprotocol.server.McpServer; -import io.modelcontextprotocol.server.McpSyncServer; -import io.modelcontextprotocol.server.transport.HttpServletSseServerTransportProvider; -import io.modelcontextprotocol.server.transport.StdioServerTransportProvider; import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpServerTransportProvider; import java.time.Duration; -public class ConfigurableMcpSyncServerFactory implements ConfigurableMcpServerFactory { +public abstract class AbstractConfigurableMcpServerFactory implements ConfigurableMcpServerFactory { - private final McpServerConfiguration configuration; + protected final McpServerConfiguration configuration; - public ConfigurableMcpSyncServerFactory(McpServerConfiguration configuration) { + protected AbstractConfigurableMcpServerFactory(McpServerConfiguration configuration) { this.configuration = configuration; } @Override - public McpSyncServer create() { - return McpServer.sync(transportProvider()) - .instructions(configuration.instructions()) - .capabilities(configureServerCapabilities()) + public McpAsyncServer create() { + return McpServer.async(transportProvider()) .serverInfo(configuration.name(), configuration.version()) + .capabilities(serverCapabilities()) + .instructions(configuration.instructions()) .requestTimeout(Duration.ofMillis(configuration.requestTimeout())) .build(); } @Override - public McpServerTransportProvider transportProvider() { - if (configuration.stdio()) { - return new StdioServerTransportProvider(); - } else { - McpServerSSE sse = configuration.sse(); - return new HttpServletSseServerTransportProvider(JsonHelper.MAPPER, sse.baseUrl(), sse.messageEndpoint(), sse.endpoint()); - } - } - - @Override - public McpSchema.ServerCapabilities configureServerCapabilities() { + public McpSchema.ServerCapabilities serverCapabilities() { McpSchema.ServerCapabilities.Builder capabilities = McpSchema.ServerCapabilities.builder(); McpServerCapabilities capabilitiesConfig = configuration.capabilities(); McpServerChangeNotification serverChangeNotification = configuration.changeNotification(); diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/AbstractMcpServerComponentFactory.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/AbstractMcpServerComponentFactory.java new file mode 100644 index 0000000..c2ab511 --- /dev/null +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/AbstractMcpServerComponentFactory.java @@ -0,0 +1,13 @@ +package com.github.codeboyzhou.mcp.declarative.server.factory; + +import com.google.inject.Injector; + +public abstract class AbstractMcpServerComponentFactory implements McpServerComponentFactory { + + protected final Injector injector; + + protected AbstractMcpServerComponentFactory(Injector injector) { + this.injector = injector; + } + +} diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/AbstractMcpServerFactory.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/AbstractMcpServerFactory.java index 78fa26c..520dc5e 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/AbstractMcpServerFactory.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/AbstractMcpServerFactory.java @@ -6,7 +6,8 @@ public abstract class AbstractMcpServerFactory implements McpServerFactory { - protected McpSchema.ServerCapabilities serverCapabilities() { + @Override + public McpSchema.ServerCapabilities serverCapabilities() { return McpSchema.ServerCapabilities.builder() .resources(true, true) .prompts(true) diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/ConfigurableMcpHttpSseServerFactory.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/ConfigurableMcpHttpSseServerFactory.java new file mode 100644 index 0000000..41604e5 --- /dev/null +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/ConfigurableMcpHttpSseServerFactory.java @@ -0,0 +1,23 @@ +package com.github.codeboyzhou.mcp.declarative.server.factory; + +import com.github.codeboyzhou.mcp.declarative.configuration.McpServerConfiguration; +import com.github.codeboyzhou.mcp.declarative.configuration.McpServerSSE; +import com.github.codeboyzhou.mcp.declarative.util.JsonHelper; +import io.modelcontextprotocol.server.transport.HttpServletSseServerTransportProvider; + +public class ConfigurableMcpHttpSseServerFactory extends AbstractConfigurableMcpServerFactory { + + public ConfigurableMcpHttpSseServerFactory(McpServerConfiguration configuration) { + super(configuration); + } + + @Override + public HttpServletSseServerTransportProvider transportProvider() { + McpServerSSE sse = configuration.sse(); + final String baseUrl = sse.baseUrl(); + final String messageEndpoint = sse.messageEndpoint(); + final String sseEndpoint = sse.endpoint(); + return new HttpServletSseServerTransportProvider(JsonHelper.MAPPER, baseUrl, messageEndpoint, sseEndpoint); + } + +} diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/ConfigurableMcpServerFactory.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/ConfigurableMcpServerFactory.java index f171820..07ed42e 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/ConfigurableMcpServerFactory.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/ConfigurableMcpServerFactory.java @@ -1,14 +1,15 @@ package com.github.codeboyzhou.mcp.declarative.server.factory; +import io.modelcontextprotocol.server.McpAsyncServer; import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpServerTransportProvider; -public interface ConfigurableMcpServerFactory { +public interface ConfigurableMcpServerFactory { - T create(); + T transportProvider(); - McpServerTransportProvider transportProvider(); + McpAsyncServer create(); - McpSchema.ServerCapabilities configureServerCapabilities(); + McpSchema.ServerCapabilities serverCapabilities(); } diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/ConfigurableMcpStdioServerFactory.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/ConfigurableMcpStdioServerFactory.java new file mode 100644 index 0000000..31304c5 --- /dev/null +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/ConfigurableMcpStdioServerFactory.java @@ -0,0 +1,18 @@ +package com.github.codeboyzhou.mcp.declarative.server.factory; + +import com.github.codeboyzhou.mcp.declarative.configuration.McpServerConfiguration; +import io.modelcontextprotocol.server.transport.StdioServerTransportProvider; +import io.modelcontextprotocol.spec.McpServerTransportProvider; + +public class ConfigurableMcpStdioServerFactory extends AbstractConfigurableMcpServerFactory { + + public ConfigurableMcpStdioServerFactory(McpServerConfiguration configuration) { + super(configuration); + } + + @Override + public McpServerTransportProvider transportProvider() { + return new StdioServerTransportProvider(); + } + +} diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerComponentFactory.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerComponentFactory.java new file mode 100644 index 0000000..7e6b5ca --- /dev/null +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerComponentFactory.java @@ -0,0 +1,13 @@ +package com.github.codeboyzhou.mcp.declarative.server.factory; + +import io.modelcontextprotocol.server.McpSyncServer; + +import java.lang.reflect.Method; + +public interface McpServerComponentFactory { + + T create(Class clazz, Method method); + + void registerTo(McpSyncServer server); + +} diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerFactory.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerFactory.java index 6d881d8..8080f3c 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerFactory.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerFactory.java @@ -2,6 +2,7 @@ import com.github.codeboyzhou.mcp.declarative.server.McpServerInfo; import io.modelcontextprotocol.server.McpAsyncServer; +import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpServerTransportProvider; public interface McpServerFactory { @@ -10,4 +11,6 @@ public interface McpServerFactory { +public class McpServerPromptFactory extends AbstractMcpServerComponentFactory { - private static final Logger logger = LoggerFactory.getLogger(McpSyncServerPromptRegister.class); + private static final Logger logger = LoggerFactory.getLogger(McpServerPromptFactory.class); - protected McpSyncServerPromptRegister(Injector injector) { + @Inject + protected McpServerPromptFactory(Injector injector) { super(injector); } @Override - public void registerTo(McpSyncServer server) { - Reflections reflections = injector.getInstance(Reflections.class); - Set> promptClasses = reflections.getTypesAnnotatedWith(McpPrompts.class); - ComponentBufferQueue queue = new ComponentBufferQueue<>(); - for (Class promptClass : promptClasses) { - Set promptMethods = reflections.getMethodsAnnotatedWith(McpPrompt.class); - List methods = promptMethods.stream().filter(m -> m.getDeclaringClass() == promptClass).toList(); - for (Method method : methods) { - McpServerFeatures.SyncPromptSpecification prompt = createComponentFrom(promptClass, method); - queue.submit(prompt); - } - } - queue.consume(server, McpSyncServer::addPrompt); - } - - @Override - public McpServerFeatures.SyncPromptSpecification createComponentFrom(Class clazz, Method method) { + public McpServerFeatures.SyncPromptSpecification create(Class clazz, Method method) { McpPrompt promptMethod = method.getAnnotation(McpPrompt.class); final String name = promptMethod.name().isBlank() ? method.getName() : promptMethod.name(); final String description = promptMethod.description(); @@ -70,6 +57,22 @@ public McpServerFeatures.SyncPromptSpecification createComponentFrom(Class cl }); } + @Override + public void registerTo(McpSyncServer server) { + Reflections reflections = injector.getInstance(Reflections.class); + Set> promptClasses = reflections.getTypesAnnotatedWith(McpPrompts.class); + BufferQueue queue = new BufferQueue<>(); + for (Class promptClass : promptClasses) { + Set promptMethods = reflections.getMethodsAnnotatedWith(McpPrompt.class); + List methods = promptMethods.stream().filter(m -> m.getDeclaringClass() == promptClass).toList(); + for (Method method : methods) { + McpServerFeatures.SyncPromptSpecification prompt = create(promptClass, method); + queue.submit(prompt); + } + } + queue.consume(server::addPrompt); + } + private List createPromptArguments(Method method) { Stream parameters = Stream.of(method.getParameters()); List params = parameters.filter(p -> p.isAnnotationPresent(McpPromptParam.class)).toList(); diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpSyncServerResourceRegister.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerResourceFactory.java similarity index 78% rename from src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpSyncServerResourceRegister.java rename to src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerResourceFactory.java index dd14aa7..87ec18b 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpSyncServerResourceRegister.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerResourceFactory.java @@ -1,8 +1,10 @@ -package com.github.codeboyzhou.mcp.declarative.server.register; +package com.github.codeboyzhou.mcp.declarative.server.factory; import com.github.codeboyzhou.mcp.declarative.annotation.McpResource; import com.github.codeboyzhou.mcp.declarative.annotation.McpResources; +import com.github.codeboyzhou.mcp.declarative.common.BufferQueue; import com.github.codeboyzhou.mcp.declarative.util.JsonHelper; +import com.google.inject.Inject; import com.google.inject.Injector; import io.modelcontextprotocol.server.McpServerFeatures; import io.modelcontextprotocol.server.McpSyncServer; @@ -15,32 +17,17 @@ import java.util.List; import java.util.Set; -public class McpSyncServerResourceRegister extends McpSyncServerComponentRegister { +public class McpServerResourceFactory extends AbstractMcpServerComponentFactory { - private static final Logger logger = LoggerFactory.getLogger(McpSyncServerResourceRegister.class); + private static final Logger logger = LoggerFactory.getLogger(McpServerResourceFactory.class); - protected McpSyncServerResourceRegister(Injector injector) { + @Inject + protected McpServerResourceFactory(Injector injector) { super(injector); } @Override - public void registerTo(McpSyncServer server) { - Reflections reflections = injector.getInstance(Reflections.class); - Set> resourceClasses = reflections.getTypesAnnotatedWith(McpResources.class); - ComponentBufferQueue queue = new ComponentBufferQueue<>(); - for (Class resourceClass : resourceClasses) { - Set resourceMethods = reflections.getMethodsAnnotatedWith(McpResource.class); - List methods = resourceMethods.stream().filter(m -> m.getDeclaringClass() == resourceClass).toList(); - for (Method method : methods) { - McpServerFeatures.SyncResourceSpecification resource = createComponentFrom(resourceClass, method); - queue.submit(resource); - } - } - queue.consume(server, McpSyncServer::addResource); - } - - @Override - public McpServerFeatures.SyncResourceSpecification createComponentFrom(Class clazz, Method method) { + public McpServerFeatures.SyncResourceSpecification create(Class clazz, Method method) { McpResource res = method.getAnnotation(McpResource.class); final String name = res.name().isBlank() ? method.getName() : res.name(); McpSchema.Resource resource = new McpSchema.Resource( @@ -64,4 +51,20 @@ public McpServerFeatures.SyncResourceSpecification createComponentFrom(Class }); } + @Override + public void registerTo(McpSyncServer server) { + Reflections reflections = injector.getInstance(Reflections.class); + Set> resourceClasses = reflections.getTypesAnnotatedWith(McpResources.class); + BufferQueue queue = new BufferQueue<>(); + for (Class resourceClass : resourceClasses) { + Set resourceMethods = reflections.getMethodsAnnotatedWith(McpResource.class); + List methods = resourceMethods.stream().filter(m -> m.getDeclaringClass() == resourceClass).toList(); + for (Method method : methods) { + McpServerFeatures.SyncResourceSpecification resource = create(resourceClass, method); + queue.submit(resource); + } + } + queue.consume(server::addResource); + } + } diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpSyncServerToolRegister.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerToolFactory.java similarity index 92% rename from src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpSyncServerToolRegister.java rename to src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerToolFactory.java index 9468fe6..5a7a704 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpSyncServerToolRegister.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerToolFactory.java @@ -1,14 +1,16 @@ -package com.github.codeboyzhou.mcp.declarative.server.register; +package com.github.codeboyzhou.mcp.declarative.server.factory; import com.github.codeboyzhou.mcp.declarative.annotation.McpJsonSchemaDefinition; import com.github.codeboyzhou.mcp.declarative.annotation.McpJsonSchemaDefinitionProperty; import com.github.codeboyzhou.mcp.declarative.annotation.McpTool; import com.github.codeboyzhou.mcp.declarative.annotation.McpToolParam; import com.github.codeboyzhou.mcp.declarative.annotation.McpTools; +import com.github.codeboyzhou.mcp.declarative.common.BufferQueue; import com.github.codeboyzhou.mcp.declarative.enums.JsonSchemaDataType; import com.github.codeboyzhou.mcp.declarative.util.JsonHelper; import com.github.codeboyzhou.mcp.declarative.util.StringHelper; import com.github.codeboyzhou.mcp.declarative.util.TypeConverter; +import com.google.inject.Inject; import com.google.inject.Injector; import io.modelcontextprotocol.server.McpServerFeatures; import io.modelcontextprotocol.server.McpSyncServer; @@ -28,32 +30,17 @@ import java.util.Set; import java.util.stream.Stream; -public class McpSyncServerToolRegister extends McpSyncServerComponentRegister { +public class McpServerToolFactory extends AbstractMcpServerComponentFactory { - private static final Logger logger = LoggerFactory.getLogger(McpSyncServerToolRegister.class); + private static final Logger logger = LoggerFactory.getLogger(McpServerToolFactory.class); - protected McpSyncServerToolRegister(Injector injector) { + @Inject + protected McpServerToolFactory(Injector injector) { super(injector); } @Override - public void registerTo(McpSyncServer server) { - Reflections reflections = injector.getInstance(Reflections.class); - Set> toolClasses = reflections.getTypesAnnotatedWith(McpTools.class); - ComponentBufferQueue queue = new ComponentBufferQueue<>(); - for (Class toolClass : toolClasses) { - Set toolMethods = reflections.getMethodsAnnotatedWith(McpTool.class); - List methods = toolMethods.stream().filter(m -> m.getDeclaringClass() == toolClass).toList(); - for (Method method : methods) { - McpServerFeatures.SyncToolSpecification tool = createComponentFrom(toolClass, method); - queue.submit(tool); - } - } - queue.consume(server, McpSyncServer::addTool); - } - - @Override - public McpServerFeatures.SyncToolSpecification createComponentFrom(Class clazz, Method method) { + public McpServerFeatures.SyncToolSpecification create(Class clazz, Method method) { McpTool toolMethod = method.getAnnotation(McpTool.class); McpSchema.JsonSchema paramSchema = createJsonSchema(method); final String name = toolMethod.name().isBlank() ? method.getName() : toolMethod.name(); @@ -76,6 +63,22 @@ public McpServerFeatures.SyncToolSpecification createComponentFrom(Class claz }); } + @Override + public void registerTo(McpSyncServer server) { + Reflections reflections = injector.getInstance(Reflections.class); + Set> toolClasses = reflections.getTypesAnnotatedWith(McpTools.class); + BufferQueue queue = new BufferQueue<>(); + for (Class toolClass : toolClasses) { + Set toolMethods = reflections.getMethodsAnnotatedWith(McpTool.class); + List methods = toolMethods.stream().filter(m -> m.getDeclaringClass() == toolClass).toList(); + for (Method method : methods) { + McpServerFeatures.SyncToolSpecification tool = create(toolClass, method); + queue.submit(tool); + } + } + queue.consume(server::addTool); + } + private McpSchema.JsonSchema createJsonSchema(Method method) { Map properties = new LinkedHashMap<>(); Map definitions = new LinkedHashMap<>(); diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpServerComponentRegister.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpServerComponentRegister.java deleted file mode 100644 index 47a6fb1..0000000 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpServerComponentRegister.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.github.codeboyzhou.mcp.declarative.server.register; - -import java.lang.reflect.Method; - -public interface McpServerComponentRegister { - - void registerTo(T server); - - R createComponentFrom(Class clazz, Method method); -} diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpServerComponentRegisters.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpServerComponentRegisters.java deleted file mode 100644 index 414d418..0000000 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpServerComponentRegisters.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.github.codeboyzhou.mcp.declarative.server.register; - -import com.google.inject.Injector; -import io.modelcontextprotocol.server.McpSyncServer; - -public class McpServerComponentRegisters { - - private final Injector injector; - - public McpServerComponentRegisters(Injector injector) { - this.injector = injector; - } - - public void registerAllTo(McpSyncServer server) { - new McpSyncServerResourceRegister(injector).registerTo(server); - new McpSyncServerPromptRegister(injector).registerTo(server); - new McpSyncServerToolRegister(injector).registerTo(server); - } - -} diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpSyncServerComponentRegister.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpSyncServerComponentRegister.java deleted file mode 100644 index f6ca2da..0000000 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/register/McpSyncServerComponentRegister.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.github.codeboyzhou.mcp.declarative.server.register; - -import com.google.inject.Injector; -import io.modelcontextprotocol.server.McpSyncServer; - -public abstract class McpSyncServerComponentRegister implements McpServerComponentRegister { - - protected final Injector injector; - - protected McpSyncServerComponentRegister(Injector injector) { - this.injector = injector; - } - -} diff --git a/src/test/java/com/github/codeboyzhou/mcp/declarative/McpServersTest.java b/src/test/java/com/github/codeboyzhou/mcp/declarative/McpServersTest.java index 51028ee..3929092 100644 --- a/src/test/java/com/github/codeboyzhou/mcp/declarative/McpServersTest.java +++ b/src/test/java/com/github/codeboyzhou/mcp/declarative/McpServersTest.java @@ -12,7 +12,7 @@ import com.github.codeboyzhou.mcp.declarative.server.TestMcpPrompts; import com.github.codeboyzhou.mcp.declarative.server.TestMcpResources; import com.github.codeboyzhou.mcp.declarative.server.TestMcpTools; -import com.github.codeboyzhou.mcp.declarative.util.GuiceInjector; +import com.github.codeboyzhou.mcp.declarative.common.GuiceInjectorModule; import com.google.inject.Guice; import com.google.inject.Injector; import org.junit.jupiter.api.AfterEach; @@ -42,7 +42,7 @@ void setUp() { @AfterEach void tearDown() { - Injector injector = Guice.createInjector(new GuiceInjector(TestMcpComponentScanIsNull.class)); + Injector injector = Guice.createInjector(new GuiceInjectorModule(TestMcpComponentScanIsNull.class)); Reflections reflections = injector.getInstance(Reflections.class); assertNotNull(reflections); From 5081611518268e7bb762694fe8dd07a41405fea6 Mon Sep 17 00:00:00 2001 From: codeboyzhou Date: Wed, 4 Jun 2025 08:24:17 +0800 Subject: [PATCH 42/57] refactor(server): Transition server factories to asynchronous processing and update component registration to use McpAsyncServer --- .../mcp/declarative/McpServers.java | 7 ++- .../factory/McpServerComponentFactory.java | 4 +- .../factory/McpServerPromptFactory.java | 43 +++++++++--------- .../factory/McpServerResourceFactory.java | 43 +++++++++--------- .../server/factory/McpServerToolFactory.java | 45 ++++++++++--------- 5 files changed, 75 insertions(+), 67 deletions(-) diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/McpServers.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/McpServers.java index 2f6de38..0f6135f 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/McpServers.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/McpServers.java @@ -79,10 +79,9 @@ private void doStartServer(McpServerConfiguration configuration) { } private void registerComponents(McpAsyncServer server) { - McpSyncServer syncServer = new McpSyncServer(server); - injector.getInstance(McpServerResourceFactory.class).registerTo(syncServer); - injector.getInstance(McpServerPromptFactory.class).registerTo(syncServer); - injector.getInstance(McpServerToolFactory.class).registerTo(syncServer); + injector.getInstance(McpServerResourceFactory.class).registerTo(server); + injector.getInstance(McpServerPromptFactory.class).registerTo(server); + injector.getInstance(McpServerToolFactory.class).registerTo(server); } } diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerComponentFactory.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerComponentFactory.java index 7e6b5ca..553a17e 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerComponentFactory.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerComponentFactory.java @@ -1,6 +1,6 @@ package com.github.codeboyzhou.mcp.declarative.server.factory; -import io.modelcontextprotocol.server.McpSyncServer; +import io.modelcontextprotocol.server.McpAsyncServer; import java.lang.reflect.Method; @@ -8,6 +8,6 @@ public interface McpServerComponentFactory { T create(Class clazz, Method method); - void registerTo(McpSyncServer server); + void registerTo(McpAsyncServer server); } diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerPromptFactory.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerPromptFactory.java index 38c06c5..7036716 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerPromptFactory.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerPromptFactory.java @@ -8,12 +8,13 @@ import com.github.codeboyzhou.mcp.declarative.util.TypeConverter; import com.google.inject.Inject; import com.google.inject.Injector; +import io.modelcontextprotocol.server.McpAsyncServer; import io.modelcontextprotocol.server.McpServerFeatures; -import io.modelcontextprotocol.server.McpSyncServer; import io.modelcontextprotocol.spec.McpSchema; import org.reflections.Reflections; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import reactor.core.publisher.Mono; import java.lang.reflect.Method; import java.lang.reflect.Parameter; @@ -24,7 +25,7 @@ import java.util.Set; import java.util.stream.Stream; -public class McpServerPromptFactory extends AbstractMcpServerComponentFactory { +public class McpServerPromptFactory extends AbstractMcpServerComponentFactory { private static final Logger logger = LoggerFactory.getLogger(McpServerPromptFactory.class); @@ -34,39 +35,41 @@ protected McpServerPromptFactory(Injector injector) { } @Override - public McpServerFeatures.SyncPromptSpecification create(Class clazz, Method method) { + public McpServerFeatures.AsyncPromptSpecification create(Class clazz, Method method) { McpPrompt promptMethod = method.getAnnotation(McpPrompt.class); final String name = promptMethod.name().isBlank() ? method.getName() : promptMethod.name(); final String description = promptMethod.description(); List promptArguments = createPromptArguments(method); McpSchema.Prompt prompt = new McpSchema.Prompt(name, description, promptArguments); logger.debug("Registering prompt: {}", JsonHelper.toJson(prompt)); - return new McpServerFeatures.SyncPromptSpecification(prompt, (exchange, request) -> { - Object result; - try { - Object instance = injector.getInstance(clazz); - Map typedParameters = asTypedParameters(method, promptArguments, request.arguments()); - result = method.invoke(instance, typedParameters.values().toArray()); - } catch (Exception e) { - logger.error("Error invoking prompt method", e); - result = e + ": " + e.getMessage(); - } - McpSchema.Content content = new McpSchema.TextContent(result.toString()); - McpSchema.PromptMessage message = new McpSchema.PromptMessage(McpSchema.Role.USER, content); - return new McpSchema.GetPromptResult(description, List.of(message)); - }); + return new McpServerFeatures.AsyncPromptSpecification(prompt, (exchange, request) -> + Mono.fromSupplier(() -> { + Object result; + try { + Object instance = injector.getInstance(clazz); + Map typedParameters = asTypedParameters(method, promptArguments, request.arguments()); + result = method.invoke(instance, typedParameters.values().toArray()); + } catch (Exception e) { + logger.error("Error invoking prompt method", e); + result = e + ": " + e.getMessage(); + } + McpSchema.Content content = new McpSchema.TextContent(result.toString()); + McpSchema.PromptMessage message = new McpSchema.PromptMessage(McpSchema.Role.USER, content); + return new McpSchema.GetPromptResult(description, List.of(message)); + }) + ); } @Override - public void registerTo(McpSyncServer server) { + public void registerTo(McpAsyncServer server) { Reflections reflections = injector.getInstance(Reflections.class); Set> promptClasses = reflections.getTypesAnnotatedWith(McpPrompts.class); - BufferQueue queue = new BufferQueue<>(); + BufferQueue queue = new BufferQueue<>(); for (Class promptClass : promptClasses) { Set promptMethods = reflections.getMethodsAnnotatedWith(McpPrompt.class); List methods = promptMethods.stream().filter(m -> m.getDeclaringClass() == promptClass).toList(); for (Method method : methods) { - McpServerFeatures.SyncPromptSpecification prompt = create(promptClass, method); + McpServerFeatures.AsyncPromptSpecification prompt = create(promptClass, method); queue.submit(prompt); } } diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerResourceFactory.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerResourceFactory.java index 87ec18b..cb0148f 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerResourceFactory.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerResourceFactory.java @@ -6,18 +6,19 @@ import com.github.codeboyzhou.mcp.declarative.util.JsonHelper; import com.google.inject.Inject; import com.google.inject.Injector; +import io.modelcontextprotocol.server.McpAsyncServer; import io.modelcontextprotocol.server.McpServerFeatures; -import io.modelcontextprotocol.server.McpSyncServer; import io.modelcontextprotocol.spec.McpSchema; import org.reflections.Reflections; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import reactor.core.publisher.Mono; import java.lang.reflect.Method; import java.util.List; import java.util.Set; -public class McpServerResourceFactory extends AbstractMcpServerComponentFactory { +public class McpServerResourceFactory extends AbstractMcpServerComponentFactory { private static final Logger logger = LoggerFactory.getLogger(McpServerResourceFactory.class); @@ -27,7 +28,7 @@ protected McpServerResourceFactory(Injector injector) { } @Override - public McpServerFeatures.SyncResourceSpecification create(Class clazz, Method method) { + public McpServerFeatures.AsyncResourceSpecification create(Class clazz, Method method) { McpResource res = method.getAnnotation(McpResource.class); final String name = res.name().isBlank() ? method.getName() : res.name(); McpSchema.Resource resource = new McpSchema.Resource( @@ -35,32 +36,34 @@ public McpServerFeatures.SyncResourceSpecification create(Class clazz, Method new McpSchema.Annotations(List.of(res.roles()), res.priority()) ); logger.debug("Registering resource: {}", JsonHelper.toJson(resource)); - return new McpServerFeatures.SyncResourceSpecification(resource, (exchange, request) -> { - Object result; - try { - Object instance = injector.getInstance(clazz); - result = method.invoke(instance); - } catch (Exception e) { - logger.error("Error invoking resource method", e); - result = e + ": " + e.getMessage(); - } - McpSchema.ResourceContents contents = new McpSchema.TextResourceContents( - resource.uri(), resource.mimeType(), result.toString() - ); - return new McpSchema.ReadResourceResult(List.of(contents)); - }); + return new McpServerFeatures.AsyncResourceSpecification(resource, (exchange, request) -> + Mono.fromSupplier(() -> { + Object result; + try { + Object instance = injector.getInstance(clazz); + result = method.invoke(instance); + } catch (Exception e) { + logger.error("Error invoking resource method", e); + result = e + ": " + e.getMessage(); + } + McpSchema.ResourceContents contents = new McpSchema.TextResourceContents( + resource.uri(), resource.mimeType(), result.toString() + ); + return new McpSchema.ReadResourceResult(List.of(contents)); + }) + ); } @Override - public void registerTo(McpSyncServer server) { + public void registerTo(McpAsyncServer server) { Reflections reflections = injector.getInstance(Reflections.class); Set> resourceClasses = reflections.getTypesAnnotatedWith(McpResources.class); - BufferQueue queue = new BufferQueue<>(); + BufferQueue queue = new BufferQueue<>(); for (Class resourceClass : resourceClasses) { Set resourceMethods = reflections.getMethodsAnnotatedWith(McpResource.class); List methods = resourceMethods.stream().filter(m -> m.getDeclaringClass() == resourceClass).toList(); for (Method method : methods) { - McpServerFeatures.SyncResourceSpecification resource = create(resourceClass, method); + McpServerFeatures.AsyncResourceSpecification resource = create(resourceClass, method); queue.submit(resource); } } diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerToolFactory.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerToolFactory.java index 5a7a704..9d68341 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerToolFactory.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerToolFactory.java @@ -12,12 +12,13 @@ import com.github.codeboyzhou.mcp.declarative.util.TypeConverter; import com.google.inject.Inject; import com.google.inject.Injector; +import io.modelcontextprotocol.server.McpAsyncServer; import io.modelcontextprotocol.server.McpServerFeatures; -import io.modelcontextprotocol.server.McpSyncServer; import io.modelcontextprotocol.spec.McpSchema; import org.reflections.Reflections; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import reactor.core.publisher.Mono; import java.lang.reflect.Field; import java.lang.reflect.Method; @@ -30,7 +31,7 @@ import java.util.Set; import java.util.stream.Stream; -public class McpServerToolFactory extends AbstractMcpServerComponentFactory { +public class McpServerToolFactory extends AbstractMcpServerComponentFactory { private static final Logger logger = LoggerFactory.getLogger(McpServerToolFactory.class); @@ -40,39 +41,41 @@ protected McpServerToolFactory(Injector injector) { } @Override - public McpServerFeatures.SyncToolSpecification create(Class clazz, Method method) { + public McpServerFeatures.AsyncToolSpecification create(Class clazz, Method method) { McpTool toolMethod = method.getAnnotation(McpTool.class); McpSchema.JsonSchema paramSchema = createJsonSchema(method); final String name = toolMethod.name().isBlank() ? method.getName() : toolMethod.name(); McpSchema.Tool tool = new McpSchema.Tool(name, toolMethod.description(), paramSchema); logger.debug("Registering tool: {}", JsonHelper.toJson(tool)); - return new McpServerFeatures.SyncToolSpecification(tool, (exchange, params) -> { - Object result; - boolean isError = false; - try { - Object instance = injector.getInstance(clazz); - Map typedParameters = asTypedParameters(paramSchema, params); - result = method.invoke(instance, typedParameters.values().toArray()); - } catch (Exception e) { - logger.error("Error invoking tool method", e); - result = e + ": " + e.getMessage(); - isError = true; - } - McpSchema.Content content = new McpSchema.TextContent(result.toString()); - return new McpSchema.CallToolResult(List.of(content), isError); - }); + return new McpServerFeatures.AsyncToolSpecification(tool, (exchange, params) -> + Mono.fromSupplier(() -> { + Object result; + boolean isError = false; + try { + Object instance = injector.getInstance(clazz); + Map typedParameters = asTypedParameters(paramSchema, params); + result = method.invoke(instance, typedParameters.values().toArray()); + } catch (Exception e) { + logger.error("Error invoking tool method", e); + result = e + ": " + e.getMessage(); + isError = true; + } + McpSchema.Content content = new McpSchema.TextContent(result.toString()); + return new McpSchema.CallToolResult(List.of(content), isError); + }) + ); } @Override - public void registerTo(McpSyncServer server) { + public void registerTo(McpAsyncServer server) { Reflections reflections = injector.getInstance(Reflections.class); Set> toolClasses = reflections.getTypesAnnotatedWith(McpTools.class); - BufferQueue queue = new BufferQueue<>(); + BufferQueue queue = new BufferQueue<>(); for (Class toolClass : toolClasses) { Set toolMethods = reflections.getMethodsAnnotatedWith(McpTool.class); List methods = toolMethods.stream().filter(m -> m.getDeclaringClass() == toolClass).toList(); for (Method method : methods) { - McpServerFeatures.SyncToolSpecification tool = create(toolClass, method); + McpServerFeatures.AsyncToolSpecification tool = create(toolClass, method); queue.submit(tool); } } From 2d6593264cfab42cd18bca361bcdb79f68f285a2 Mon Sep 17 00:00:00 2001 From: codeboyzhou Date: Wed, 4 Jun 2025 08:36:06 +0800 Subject: [PATCH 43/57] refactor(server): Deprecate synchronous server start methods and introduce new asynchronous counterparts for improved server management --- .../codeboyzhou/mcp/declarative/McpServers.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/McpServers.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/McpServers.java index 0f6135f..9ed3f47 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/McpServers.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/McpServers.java @@ -37,22 +37,37 @@ public static McpServers run(Class applicationMainClass, String[] args) { return INSTANCE; } + @Deprecated(since = "0.5.0", forRemoval = true) public void startSyncStdioServer(McpServerInfo serverInfo) { McpStdioServerFactory factory = new McpStdioServerFactory(); McpAsyncServer server = factory.create(serverInfo); registerComponents(server); } + @Deprecated(since = "0.5.0", forRemoval = true) public void startSyncSseServer(McpSseServerInfo serverInfo, McpHttpServerStatusListener listener) { McpHttpSseServerFactory factory = new McpHttpSseServerFactory(); McpAsyncServer server = factory.create(serverInfo); registerComponents(server); } + @Deprecated(since = "0.5.0", forRemoval = true) public void startSyncSseServer(McpSseServerInfo serverInfo) { startSyncSseServer(serverInfo, new DefaultMcpSyncHttpServerStatusListener()); } + public void startStdioServer(McpServerInfo serverInfo) { + McpStdioServerFactory factory = new McpStdioServerFactory(); + McpAsyncServer server = factory.create(serverInfo); + registerComponents(server); + } + + public void startSseServer(McpSseServerInfo serverInfo) { + McpHttpSseServerFactory factory = new McpHttpSseServerFactory(); + McpAsyncServer server = factory.create(serverInfo); + registerComponents(server); + } + public void startServer(String configFileName) { Assert.notNull(configFileName, "configFileName must not be null"); doStartServer(new YAMLConfigurationLoader(configFileName).getConfig()); From d8570c3e95637704e0ed414721ce921d31b581e3 Mon Sep 17 00:00:00 2001 From: codeboyzhou Date: Mon, 9 Jun 2025 00:51:35 +0800 Subject: [PATCH 44/57] feat(i18n): Add i18n support for MCP server component description - Introduce ResourceBundle for handling localized descriptions - Add descriptionI18nKey field to relevant annotations - Update factory classes to use localized descriptions - Modify getDescription method to handle both localized and default descriptions --- .../McpJsonSchemaDefinitionProperty.java | 5 ++++- .../mcp/declarative/annotation/McpPrompt.java | 5 ++++- .../declarative/annotation/McpPromptParam.java | 7 ++++++- .../mcp/declarative/annotation/McpResource.java | 5 ++++- .../mcp/declarative/annotation/McpTool.java | 5 ++++- .../mcp/declarative/annotation/McpToolParam.java | 7 ++++++- .../AbstractMcpServerComponentFactory.java | 16 ++++++++++++++++ .../server/factory/McpServerPromptFactory.java | 4 ++-- .../server/factory/McpServerResourceFactory.java | 7 +++---- .../server/factory/McpServerToolFactory.java | 7 ++++--- .../mcp_server_component_descriptions.properties | 0 11 files changed, 53 insertions(+), 15 deletions(-) create mode 100644 src/test/resources/i18n/mcp_server_component_descriptions.properties diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpJsonSchemaDefinitionProperty.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpJsonSchemaDefinitionProperty.java index f994ba6..04b80cb 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpJsonSchemaDefinitionProperty.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpJsonSchemaDefinitionProperty.java @@ -12,7 +12,10 @@ public @interface McpJsonSchemaDefinitionProperty { String name() default StringHelper.EMPTY; - String description(); + String description() default StringHelper.EMPTY; + + String descriptionI18nKey() default StringHelper.EMPTY; boolean required() default false; + } diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpPrompt.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpPrompt.java index afc21a5..30b8eda 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpPrompt.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpPrompt.java @@ -13,5 +13,8 @@ String name() default StringHelper.EMPTY; - String description(); + String description() default StringHelper.EMPTY; + + String descriptionI18nKey() default StringHelper.EMPTY; + } diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpPromptParam.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpPromptParam.java index 6ab2500..fd8c7d4 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpPromptParam.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpPromptParam.java @@ -1,5 +1,7 @@ package com.github.codeboyzhou.mcp.declarative.annotation; +import com.github.codeboyzhou.mcp.declarative.util.StringHelper; + import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -11,7 +13,10 @@ String name(); - String description(); + String description() default StringHelper.EMPTY; + + String descriptionI18nKey() default StringHelper.EMPTY; boolean required() default false; + } diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpResource.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpResource.java index 0b3caf7..99a745c 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpResource.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpResource.java @@ -15,11 +15,14 @@ String name() default StringHelper.EMPTY; - String description(); + String description() default StringHelper.EMPTY; + + String descriptionI18nKey() default StringHelper.EMPTY; String mimeType() default "text/plain"; McpSchema.Role[] roles() default {McpSchema.Role.ASSISTANT, McpSchema.Role.USER}; double priority() default 1.0; + } diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpTool.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpTool.java index 307a551..5289e05 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpTool.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpTool.java @@ -13,5 +13,8 @@ String name() default StringHelper.EMPTY; - String description(); + String description() default StringHelper.EMPTY; + + String descriptionI18nKey() default StringHelper.EMPTY; + } diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpToolParam.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpToolParam.java index a78d001..ddc178e 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpToolParam.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpToolParam.java @@ -1,5 +1,7 @@ package com.github.codeboyzhou.mcp.declarative.annotation; +import com.github.codeboyzhou.mcp.declarative.util.StringHelper; + import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -10,7 +12,10 @@ public @interface McpToolParam { String name(); - String description(); + String description() default StringHelper.EMPTY; + + String descriptionI18nKey() default StringHelper.EMPTY; boolean required() default false; + } diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/AbstractMcpServerComponentFactory.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/AbstractMcpServerComponentFactory.java index c2ab511..d625b03 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/AbstractMcpServerComponentFactory.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/AbstractMcpServerComponentFactory.java @@ -2,12 +2,28 @@ import com.google.inject.Injector; +import java.util.Locale; +import java.util.ResourceBundle; + public abstract class AbstractMcpServerComponentFactory implements McpServerComponentFactory { protected final Injector injector; + private final ResourceBundle bundle; + protected AbstractMcpServerComponentFactory(Injector injector) { this.injector = injector; + this.bundle = ResourceBundle.getBundle("i18n/mcp_server_component_descriptions", Locale.getDefault()); + } + + protected String getDescription(String descriptionI18nKey, String description) { + if (!descriptionI18nKey.isBlank() && bundle.containsKey(descriptionI18nKey)) { + bundle.getString(descriptionI18nKey); + } + if (!description.isBlank()) { + return description; + } + return "No description provided."; } } diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerPromptFactory.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerPromptFactory.java index 7036716..1585d66 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerPromptFactory.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerPromptFactory.java @@ -38,7 +38,7 @@ protected McpServerPromptFactory(Injector injector) { public McpServerFeatures.AsyncPromptSpecification create(Class clazz, Method method) { McpPrompt promptMethod = method.getAnnotation(McpPrompt.class); final String name = promptMethod.name().isBlank() ? method.getName() : promptMethod.name(); - final String description = promptMethod.description(); + final String description = getDescription(promptMethod.descriptionI18nKey(), promptMethod.description()); List promptArguments = createPromptArguments(method); McpSchema.Prompt prompt = new McpSchema.Prompt(name, description, promptArguments); logger.debug("Registering prompt: {}", JsonHelper.toJson(prompt)); @@ -83,7 +83,7 @@ private List createPromptArguments(Method method) { for (Parameter param : params) { McpPromptParam promptParam = param.getAnnotation(McpPromptParam.class); final String name = promptParam.name(); - final String description = promptParam.description(); + final String description = getDescription(promptParam.descriptionI18nKey(), promptParam.description()); final boolean required = promptParam.required(); McpSchema.PromptArgument promptArgument = new McpSchema.PromptArgument(name, description, required); promptArguments.add(promptArgument); diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerResourceFactory.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerResourceFactory.java index cb0148f..95de89d 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerResourceFactory.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerResourceFactory.java @@ -31,10 +31,9 @@ protected McpServerResourceFactory(Injector injector) { public McpServerFeatures.AsyncResourceSpecification create(Class clazz, Method method) { McpResource res = method.getAnnotation(McpResource.class); final String name = res.name().isBlank() ? method.getName() : res.name(); - McpSchema.Resource resource = new McpSchema.Resource( - res.uri(), name, res.description(), res.mimeType(), - new McpSchema.Annotations(List.of(res.roles()), res.priority()) - ); + final String description = getDescription(res.descriptionI18nKey(), res.description()); + McpSchema.Annotations annotations = new McpSchema.Annotations(List.of(res.roles()), res.priority()); + McpSchema.Resource resource = new McpSchema.Resource(res.uri(), name, description, res.mimeType(), annotations); logger.debug("Registering resource: {}", JsonHelper.toJson(resource)); return new McpServerFeatures.AsyncResourceSpecification(resource, (exchange, request) -> Mono.fromSupplier(() -> { diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerToolFactory.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerToolFactory.java index 9d68341..ba27f59 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerToolFactory.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerToolFactory.java @@ -45,7 +45,8 @@ public McpServerFeatures.AsyncToolSpecification create(Class clazz, Method me McpTool toolMethod = method.getAnnotation(McpTool.class); McpSchema.JsonSchema paramSchema = createJsonSchema(method); final String name = toolMethod.name().isBlank() ? method.getName() : toolMethod.name(); - McpSchema.Tool tool = new McpSchema.Tool(name, toolMethod.description(), paramSchema); + final String description = getDescription(toolMethod.descriptionI18nKey(), toolMethod.description()); + McpSchema.Tool tool = new McpSchema.Tool(name, description, paramSchema); logger.debug("Registering tool: {}", JsonHelper.toJson(tool)); return new McpServerFeatures.AsyncToolSpecification(tool, (exchange, params) -> Mono.fromSupplier(() -> { @@ -98,7 +99,7 @@ private McpSchema.JsonSchema createJsonSchema(Method method) { if (parameterType.getAnnotation(McpJsonSchemaDefinition.class) == null) { property.put("type", parameterType.getSimpleName().toLowerCase()); - property.put("description", toolParam.description()); + property.put("description", getDescription(toolParam.descriptionI18nKey(), toolParam.description())); } else { final String parameterTypeSimpleName = parameterType.getSimpleName(); property.put("$ref", "#/definitions/" + parameterTypeSimpleName); @@ -135,7 +136,7 @@ private Map createJsonSchemaDefinition(Class definitionClass) Map fieldProperties = new HashMap<>(); fieldProperties.put("type", field.getType().getSimpleName().toLowerCase()); - fieldProperties.put("description", property.description()); + fieldProperties.put("description", getDescription(property.descriptionI18nKey(), property.description())); final String fieldName = property.name().isBlank() ? field.getName() : property.name(); properties.put(fieldName, fieldProperties); diff --git a/src/test/resources/i18n/mcp_server_component_descriptions.properties b/src/test/resources/i18n/mcp_server_component_descriptions.properties new file mode 100644 index 0000000..e69de29 From 69e28d7c095b7a9ce56fdf5e0537b1eb16b48c85 Mon Sep 17 00:00:00 2001 From: codeboyzhou Date: Mon, 9 Jun 2025 11:17:30 +0800 Subject: [PATCH 45/57] test(util): Add unit tests for utility classes --- .../mcp/declarative/util/JsonHelper.java | 6 ++ .../mcp/declarative/util/StringHelper.java | 7 +++ .../mcp/declarative/util/TypeConverter.java | 6 ++ .../mcp/declarative/util/JsonHelperTest.java | 32 ++++++++++ .../declarative/util/StringHelperTest.java | 16 +++++ .../declarative/util/TypeConverterTest.java | 61 +++++++++++++++++++ 6 files changed, 128 insertions(+) create mode 100644 src/test/java/com/github/codeboyzhou/mcp/declarative/util/JsonHelperTest.java create mode 100644 src/test/java/com/github/codeboyzhou/mcp/declarative/util/StringHelperTest.java create mode 100644 src/test/java/com/github/codeboyzhou/mcp/declarative/util/TypeConverterTest.java diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/util/JsonHelper.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/util/JsonHelper.java index 95bb7fc..5045b0e 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/util/JsonHelper.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/util/JsonHelper.java @@ -3,11 +3,17 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.github.codeboyzhou.mcp.declarative.exception.McpServerException; +import org.jetbrains.annotations.VisibleForTesting; public final class JsonHelper { public static final ObjectMapper MAPPER = new ObjectMapper(); + @VisibleForTesting + JsonHelper() { + throw new UnsupportedOperationException("Utility class should not be instantiated"); + } + public static String toJson(Object object) { try { return MAPPER.writeValueAsString(object); diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/util/StringHelper.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/util/StringHelper.java index a1915e0..337ec06 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/util/StringHelper.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/util/StringHelper.java @@ -1,7 +1,14 @@ package com.github.codeboyzhou.mcp.declarative.util; +import org.jetbrains.annotations.VisibleForTesting; + public final class StringHelper { public static final String EMPTY = ""; + @VisibleForTesting + StringHelper() { + throw new UnsupportedOperationException("Utility class should not be instantiated"); + } + } diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/util/TypeConverter.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/util/TypeConverter.java index 75c880f..816d9e3 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/util/TypeConverter.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/util/TypeConverter.java @@ -1,9 +1,15 @@ package com.github.codeboyzhou.mcp.declarative.util; import com.github.codeboyzhou.mcp.declarative.enums.JsonSchemaDataType; +import org.jetbrains.annotations.VisibleForTesting; public final class TypeConverter { + @VisibleForTesting + TypeConverter() { + throw new UnsupportedOperationException("Utility class should not be instantiated"); + } + public static Object convert(Object value, Class targetType) { if (value == null) { return getDefaultValue(targetType); diff --git a/src/test/java/com/github/codeboyzhou/mcp/declarative/util/JsonHelperTest.java b/src/test/java/com/github/codeboyzhou/mcp/declarative/util/JsonHelperTest.java new file mode 100644 index 0000000..5104215 --- /dev/null +++ b/src/test/java/com/github/codeboyzhou/mcp/declarative/util/JsonHelperTest.java @@ -0,0 +1,32 @@ +package com.github.codeboyzhou.mcp.declarative.util; + +import com.github.codeboyzhou.mcp.declarative.exception.McpServerException; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class JsonHelperTest { + + record TestClass(String name, int age) { + } + + @Test + void testNewInstance() { + UnsupportedOperationException e = assertThrows(UnsupportedOperationException.class, JsonHelper::new); + assertEquals("Utility class should not be instantiated", e.getMessage()); + } + + @Test + void testToJson() { + assertDoesNotThrow(() -> { + TestClass testObject = new TestClass("test", 18); + assertEquals("{\"name\":\"test\",\"age\":18}", JsonHelper.toJson(testObject)); + }); + + McpServerException e = assertThrows(McpServerException.class, () -> JsonHelper.toJson(this)); + assertEquals("Error converting object to JSON", e.getMessage()); + } + +} diff --git a/src/test/java/com/github/codeboyzhou/mcp/declarative/util/StringHelperTest.java b/src/test/java/com/github/codeboyzhou/mcp/declarative/util/StringHelperTest.java new file mode 100644 index 0000000..903b50e --- /dev/null +++ b/src/test/java/com/github/codeboyzhou/mcp/declarative/util/StringHelperTest.java @@ -0,0 +1,16 @@ +package com.github.codeboyzhou.mcp.declarative.util; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class StringHelperTest { + + @Test + void testNewInstance() { + UnsupportedOperationException e = assertThrows(UnsupportedOperationException.class, StringHelper::new); + assertEquals("Utility class should not be instantiated", e.getMessage()); + } + +} diff --git a/src/test/java/com/github/codeboyzhou/mcp/declarative/util/TypeConverterTest.java b/src/test/java/com/github/codeboyzhou/mcp/declarative/util/TypeConverterTest.java new file mode 100644 index 0000000..528806a --- /dev/null +++ b/src/test/java/com/github/codeboyzhou/mcp/declarative/util/TypeConverterTest.java @@ -0,0 +1,61 @@ +package com.github.codeboyzhou.mcp.declarative.util; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class TypeConverterTest { + + @Test + void testNewInstance() { + UnsupportedOperationException e = assertThrows(UnsupportedOperationException.class, TypeConverter::new); + assertEquals("Utility class should not be instantiated", e.getMessage()); + } + + @Test + void testConvertToTargetType() { + assertEquals(StringHelper.EMPTY, TypeConverter.convert(null, String.class)); + assertEquals(0, TypeConverter.convert(null, int.class)); + assertEquals(0, TypeConverter.convert(null, Integer.class)); + assertEquals(0L, TypeConverter.convert(null, long.class)); + assertEquals(0L, TypeConverter.convert(null, Long.class)); + assertEquals(0.0f, TypeConverter.convert(null, float.class)); + assertEquals(0.0f, TypeConverter.convert(null, Float.class)); + assertEquals(0.0, TypeConverter.convert(null, double.class)); + assertEquals(0.0, TypeConverter.convert(null, Double.class)); + assertEquals(false, TypeConverter.convert(null, boolean.class)); + assertEquals(false, TypeConverter.convert(null, Boolean.class)); + assertNull(TypeConverter.convert(null, TypeConverterTest.class)); + + assertEquals("123", TypeConverter.convert("123", String.class)); + assertEquals(123, TypeConverter.convert("123", int.class)); + assertEquals(123, TypeConverter.convert("123", Integer.class)); + assertEquals(123L, TypeConverter.convert("123", long.class)); + assertEquals(123L, TypeConverter.convert("123", Long.class)); + assertEquals(123.0f, TypeConverter.convert("123", float.class)); + assertEquals(123.0f, TypeConverter.convert("123", Float.class)); + assertEquals(123.0, TypeConverter.convert("123", double.class)); + assertEquals(123.0, TypeConverter.convert("123", Double.class)); + assertEquals(true, TypeConverter.convert("true", boolean.class)); + assertEquals(true, TypeConverter.convert("true", Boolean.class)); + assertEquals("123", TypeConverter.convert("123", TypeConverterTest.class)); + } + + @Test + void testConvertToJsonSchemaType() { + assertEquals(StringHelper.EMPTY, TypeConverter.convert(null, "string")); + assertEquals(0, TypeConverter.convert(null, "integer")); + assertEquals(0.0, TypeConverter.convert(null, "number")); + assertEquals(false, TypeConverter.convert(null, "boolean")); + assertNull(TypeConverter.convert(null, "object")); + + assertEquals("123", TypeConverter.convert("123", "string")); + assertEquals(123, TypeConverter.convert("123", "integer")); + assertEquals(123.0, TypeConverter.convert("123", "number")); + assertEquals(true, TypeConverter.convert("true", "boolean")); + assertEquals("123", TypeConverter.convert("123", "object")); + } + +} From d29e3a95c687f5a64630307e7d4ab93d87b10e01 Mon Sep 17 00:00:00 2001 From: codeboyzhou Date: Mon, 9 Jun 2025 12:53:45 +0800 Subject: [PATCH 46/57] test(common): Add unit tests for BufferQueue --- pom.xml | 7 +++ .../exception/McpServerException.java | 4 -- .../declarative/common/BufferQueueTest.java | 58 +++++++++++++++++++ 3 files changed, 65 insertions(+), 4 deletions(-) create mode 100644 src/test/java/com/github/codeboyzhou/mcp/declarative/common/BufferQueueTest.java diff --git a/pom.xml b/pom.xml index 64eee3d..4137b98 100644 --- a/pom.xml +++ b/pom.xml @@ -58,6 +58,7 @@ 5.10.2 1.5.18 0.10.0 + 5.18.0 0.10.2 @@ -112,6 +113,12 @@ ${junit5.version} test + + org.mockito + mockito-core + ${mockito.version} + test + org.reflections reflections diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/exception/McpServerException.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/exception/McpServerException.java index ee9b39f..d08af16 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/exception/McpServerException.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/exception/McpServerException.java @@ -2,10 +2,6 @@ public class McpServerException extends RuntimeException { - public McpServerException(String message) { - super(message); - } - public McpServerException(String message, Throwable cause) { super(message, cause); } diff --git a/src/test/java/com/github/codeboyzhou/mcp/declarative/common/BufferQueueTest.java b/src/test/java/com/github/codeboyzhou/mcp/declarative/common/BufferQueueTest.java new file mode 100644 index 0000000..5974cab --- /dev/null +++ b/src/test/java/com/github/codeboyzhou/mcp/declarative/common/BufferQueueTest.java @@ -0,0 +1,58 @@ +package com.github.codeboyzhou.mcp.declarative.common; + +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Field; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +class BufferQueueTest { + + @Test + void testNewInstance() { + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> new BufferQueue<>(0)); + assertEquals("delayMillis must be greater than 0", e.getMessage()); + } + + @Test + @SuppressWarnings("unchecked") + void testSubmitCatchException() throws Exception { + BlockingQueue queueMock = mock(LinkedBlockingQueue.class); + doThrow(InterruptedException.class).when(queueMock).put(anyString()); + + BufferQueue bufferQueue = new BufferQueue<>(); + Field queue = BufferQueue.class.getDeclaredField("queue"); + queue.setAccessible(true); + queue.set(bufferQueue, queueMock); + + bufferQueue.submit("test"); + + verify(queueMock).put("test"); + } + + @Test + @SuppressWarnings("unchecked") + void testConsumeCatchException() throws Exception { + BlockingQueue queueMock = mock(LinkedBlockingQueue.class); + doThrow(InterruptedException.class).when(queueMock).take(); + + BufferQueue bufferQueue = new BufferQueue<>(); + Field queue = BufferQueue.class.getDeclaredField("queue"); + queue.setAccessible(true); + queue.set(bufferQueue, queueMock); + + bufferQueue.consume(string -> { + // do nothing + }); + + verify(queueMock).take(); + } + +} From 95130fa2a7dc6f5ccd9344bee366c6f7e09e6762 Mon Sep 17 00:00:00 2001 From: codeboyzhou Date: Mon, 9 Jun 2025 12:58:22 +0800 Subject: [PATCH 47/57] refactor(server): Remove unused static factory methods --- .../mcp/declarative/server/McpServerInfo.java | 10 ---------- .../declarative/server/McpSseServerInfo.java | 18 ------------------ 2 files changed, 28 deletions(-) diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpServerInfo.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpServerInfo.java index 5933b04..b297c98 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpServerInfo.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpServerInfo.java @@ -1,6 +1,5 @@ package com.github.codeboyzhou.mcp.declarative.server; -import com.github.codeboyzhou.mcp.declarative.configuration.McpServerConfiguration; import com.github.codeboyzhou.mcp.declarative.util.StringHelper; import java.time.Duration; @@ -26,15 +25,6 @@ public static Builder builder() { return new Builder<>(); } - public static McpServerInfo from(McpServerConfiguration configuration) { - Builder builder = builder(); - builder.name = configuration.name(); - builder.version = configuration.version(); - builder.instructions = configuration.instructions(); - builder.requestTimeout = Duration.ofSeconds(configuration.requestTimeout()); - return builder.build(); - } - public String name() { return name; } diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSseServerInfo.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSseServerInfo.java index 0769c09..4b9c150 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSseServerInfo.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSseServerInfo.java @@ -1,11 +1,7 @@ package com.github.codeboyzhou.mcp.declarative.server; -import com.github.codeboyzhou.mcp.declarative.configuration.McpServerConfiguration; -import com.github.codeboyzhou.mcp.declarative.configuration.McpServerSSE; import com.github.codeboyzhou.mcp.declarative.util.StringHelper; -import java.time.Duration; - public class McpSseServerInfo extends McpServerInfo { private final String baseUrl; @@ -28,20 +24,6 @@ public static McpSseServerInfo.Builder builder() { return new McpSseServerInfo.Builder(); } - public static McpSseServerInfo from(McpServerConfiguration configuration) { - McpSseServerInfo.Builder builder = McpSseServerInfo.builder(); - builder.name(configuration.name()); - builder.version(configuration.version()); - builder.instructions(configuration.instructions()); - builder.requestTimeout(Duration.ofMillis(configuration.requestTimeout())); - McpServerSSE sse = configuration.sse(); - builder.baseUrl(sse.baseUrl()); - builder.messageEndpoint(sse.messageEndpoint()); - builder.sseEndpoint(sse.endpoint()); - builder.port(sse.port()); - return builder.build(); - } - public String baseUrl() { return baseUrl; } From c9e4e6dd75801aa2852063d83101609038302a6c Mon Sep 17 00:00:00 2001 From: codeboyzhou Date: Mon, 9 Jun 2025 13:11:39 +0800 Subject: [PATCH 48/57] test(server): Add unit tests for McpServers --- .../codeboyzhou/mcp/declarative/McpServersTest.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/test/java/com/github/codeboyzhou/mcp/declarative/McpServersTest.java b/src/test/java/com/github/codeboyzhou/mcp/declarative/McpServersTest.java index 3929092..13c2629 100644 --- a/src/test/java/com/github/codeboyzhou/mcp/declarative/McpServersTest.java +++ b/src/test/java/com/github/codeboyzhou/mcp/declarative/McpServersTest.java @@ -3,6 +3,7 @@ import com.github.codeboyzhou.mcp.declarative.annotation.McpPrompts; import com.github.codeboyzhou.mcp.declarative.annotation.McpResources; import com.github.codeboyzhou.mcp.declarative.annotation.McpTools; +import com.github.codeboyzhou.mcp.declarative.common.GuiceInjectorModule; import com.github.codeboyzhou.mcp.declarative.server.McpServerInfo; import com.github.codeboyzhou.mcp.declarative.server.McpSseServerInfo; import com.github.codeboyzhou.mcp.declarative.server.TestMcpComponentScanBasePackageClass; @@ -12,7 +13,6 @@ import com.github.codeboyzhou.mcp.declarative.server.TestMcpPrompts; import com.github.codeboyzhou.mcp.declarative.server.TestMcpResources; import com.github.codeboyzhou.mcp.declarative.server.TestMcpTools; -import com.github.codeboyzhou.mcp.declarative.common.GuiceInjectorModule; import com.google.inject.Guice; import com.google.inject.Injector; import org.junit.jupiter.api.AfterEach; @@ -73,7 +73,7 @@ void testRun(Class applicationMainClass) { } @Test - void testStartSyncStdioServer() { + void testStartStdioServer() { assertDoesNotThrow(() -> { McpServers servers = McpServers.run(TestMcpComponentScanIsNull.class, EMPTY_ARGS); McpServerInfo serverInfo = McpServerInfo.builder() @@ -82,12 +82,12 @@ void testStartSyncStdioServer() { .name("test-mcp-sync-stdio-server") .version("1.0.0") .build(); - servers.startSyncStdioServer(serverInfo); + servers.startStdioServer(serverInfo); }); } @Test - void testStartSyncSseServer() { + void testStartSseServer() { McpServers servers = McpServers.run(TestMcpComponentScanIsNull.class, EMPTY_ARGS); assertDoesNotThrow(() -> { McpSseServerInfo serverInfo = McpSseServerInfo.builder() @@ -100,7 +100,7 @@ void testStartSyncSseServer() { .name("test-mcp-sync-sse-server") .version("1.0.0") .build(); - servers.startSyncSseServer(serverInfo); + servers.startSseServer(serverInfo); }); } From ce240f6ca83329f83450dc971b49c6f8a7a2deab Mon Sep 17 00:00:00 2001 From: codeboyzhou Date: Tue, 10 Jun 2025 21:34:59 +0800 Subject: [PATCH 49/57] fix(server): Start McpHttpServer in a dedicated thread for avoid blocking main thread --- .../declarative/server/factory/McpHttpSseServerFactory.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpHttpSseServerFactory.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpHttpSseServerFactory.java index ab6bf75..9e5e0e3 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpHttpSseServerFactory.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpHttpSseServerFactory.java @@ -1,5 +1,6 @@ package com.github.codeboyzhou.mcp.declarative.server.factory; +import com.github.codeboyzhou.mcp.declarative.common.NamedThreadFactory; import com.github.codeboyzhou.mcp.declarative.server.McpHttpServer; import com.github.codeboyzhou.mcp.declarative.server.McpSseServerInfo; import com.github.codeboyzhou.mcp.declarative.util.JsonHelper; @@ -7,6 +8,8 @@ import io.modelcontextprotocol.server.McpServer; import io.modelcontextprotocol.server.transport.HttpServletSseServerTransportProvider; +import java.util.concurrent.Executors; + public class McpHttpSseServerFactory extends AbstractMcpServerFactory { @Override @@ -27,7 +30,8 @@ public McpAsyncServer create(McpSseServerInfo serverInfo) { .requestTimeout(serverInfo.requestTimeout()) .build(); McpHttpServer httpServer = new McpHttpServer(transportProvider, serverInfo.port()); - httpServer.start(); + NamedThreadFactory threadFactory = new NamedThreadFactory(McpHttpServer.class.getSimpleName()); + Executors.newSingleThreadExecutor(threadFactory).execute(httpServer::start); return server; } From 29d9e3c8be72905981d365ea4de1eed210e510a1 Mon Sep 17 00:00:00 2001 From: codeboyzhou Date: Tue, 10 Jun 2025 21:37:18 +0800 Subject: [PATCH 50/57] fix(server): Mono.subscribe() should be called explicitly --- .../mcp/declarative/server/factory/McpServerPromptFactory.java | 2 +- .../declarative/server/factory/McpServerResourceFactory.java | 2 +- .../mcp/declarative/server/factory/McpServerToolFactory.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerPromptFactory.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerPromptFactory.java index 1585d66..8722146 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerPromptFactory.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerPromptFactory.java @@ -73,7 +73,7 @@ public void registerTo(McpAsyncServer server) { queue.submit(prompt); } } - queue.consume(server::addPrompt); + queue.consume(prompt -> server.addPrompt(prompt).subscribe()); } private List createPromptArguments(Method method) { diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerResourceFactory.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerResourceFactory.java index 95de89d..3835552 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerResourceFactory.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerResourceFactory.java @@ -66,7 +66,7 @@ public void registerTo(McpAsyncServer server) { queue.submit(resource); } } - queue.consume(server::addResource); + queue.consume(resource -> server.addResource(resource).subscribe()); } } diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerToolFactory.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerToolFactory.java index ba27f59..a8896ba 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerToolFactory.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerToolFactory.java @@ -80,7 +80,7 @@ public void registerTo(McpAsyncServer server) { queue.submit(tool); } } - queue.consume(server::addTool); + queue.consume(tool -> server.addTool(tool).subscribe()); } private McpSchema.JsonSchema createJsonSchema(Method method) { From 7eb131677cdcabf21c0a94b1ea668d4553269f2d Mon Sep 17 00:00:00 2001 From: codeboyzhou Date: Tue, 10 Jun 2025 21:41:23 +0800 Subject: [PATCH 51/57] fix(server): Add return statement for bundle.getString(descriptionI18nKey) --- .../server/factory/AbstractMcpServerComponentFactory.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/AbstractMcpServerComponentFactory.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/AbstractMcpServerComponentFactory.java index d625b03..184db72 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/AbstractMcpServerComponentFactory.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/AbstractMcpServerComponentFactory.java @@ -18,7 +18,7 @@ protected AbstractMcpServerComponentFactory(Injector injector) { protected String getDescription(String descriptionI18nKey, String description) { if (!descriptionI18nKey.isBlank() && bundle.containsKey(descriptionI18nKey)) { - bundle.getString(descriptionI18nKey); + return bundle.getString(descriptionI18nKey); } if (!description.isBlank()) { return description; From d5f8266e96b6597fbfbfcdb14e73d41eb9dbeed6 Mon Sep 17 00:00:00 2001 From: codeboyzhou Date: Tue, 10 Jun 2025 22:29:25 +0800 Subject: [PATCH 52/57] docs(README): Update documentation for v0.5.0 release --- README.md | 52 +++++++++++++++++++++++++++++++--------------------- 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 5832c5f..663204d 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ Declarative [MCP Java SDK](https://github.com/modelcontextprotocol/java-sdk) Dev - Get rid of complex and lengthy JSON schema definitions. - Just focus on your core logic (resources/prompts/tools). - Configuration file compatible with the Spring AI framework. +- Built-in multi-languages support for MCP server (resources/prompts/tools). ## Showcase @@ -30,18 +31,15 @@ import com.github.codeboyzhou.mcp.declarative.McpServers; public class MyMcpServer { public static void main(String[] args) { + McpServers servers = McpServers.run(MyMcpServer.class, args); // Start a STDIO MCP server - McpServers.run(MyMcpServer.class, args).startSyncStdioServer( - McpServerInfo.builder().name("mcp-server").version("1.0.0").build() - ); + servers.startStdioServer(McpServerInfo.builder().name("mcp-server").version("1.0.0").build()); // or a HTTP SSE MCP server - McpServers.run(MyMcpServer.class, args).startSyncSseServer( - McpSseServerInfo.builder().name("mcp-server").version("1.0.0").port(8080).build() - ); - // or start with yaml configuration file (compatible with the Spring AI framework) - McpServers.run(MyMcpServer.class, args).startServer(); - // or start with a specific configuration file (compatible with the Spring AI framework) - McpServers.run(MyMcpServer.class, args).startServer("my-mcp-server.yml"); + servers.startSseServer(McpSseServerInfo.builder().name("mcp-server").version("1.0.0").port(8080).build()); + // or start with yaml config file (compatible with the Spring AI framework) + servers.startServer(); + // or start with a custom config file (compatible with the Spring AI framework) + servers.startServer("my-mcp-server.yml"); } } @@ -54,16 +52,21 @@ enabled: true stdio: false name: mcp-server version: 1.0.0 -instructions: mcp-server -request-timeout: 30000 -type: SYNC -resource-change-notification: true -prompt-change-notification: true -tool-change-notification: true -sse-message-endpoint: /mcp/message -sse-endpoint: /sse -base-url: http://localhost:8080 -sse-port: 8080 +type: ASYNC +request-timeout: 20000 +capabilities: + resource: true + prompt: true + tool: true +change-notification: + resource: true + prompt: true + tool: true +sse: + message-endpoint: /mcp/message + endpoint: /sse + base-url: http://localhost:8080 + port: 8080 ``` No need to care about the low-level details of native MCP Java SDK and how to create the MCP resources, prompts, and tools. Just annotate them like this: @@ -74,6 +77,8 @@ public class MyMcpResources { // This method defines a MCP resource to expose the OS env variables @McpResource(uri = "env://variables", description = "OS env variables") + // or you can use it like this to support multi-languages + @McpResource(uri = "env://variables", descriptionI18nKey = "your_i18n_key_in_properties_file") public String getSystemEnv() { // Just put your logic code here, forget about the MCP SDK details. return System.getenv().toString(); @@ -87,7 +92,10 @@ public class MyMcpResources { @McpPrompts public class MyMcpPrompts { + // This method defines a MCP prompt to read a file @McpPrompt(description = "A simple prompt to read a file") + // or you can use it like this to support multi-languages + @McpPrompt(descriptionI18nKey = "your_i18n_key_in_properties_file") public String readFile( @McpPromptParam(name = "path", description = "filepath", required = true) String path) { // Just put your logic code here, forget about the MCP SDK details. @@ -103,6 +111,8 @@ public class MyMcpTools { // This method defines a MCP tool to read a file @McpTool(description = "Read complete file contents with UTF-8 encoding") + // or you can use it like this to support multi-languages + @McpTool(descriptionI18nKey = "your_i18n_key_in_properties_file") public String readFile( @McpToolParam(name = "path", description = "filepath", required = true) String path) { // Just put your logic code here, forget about the MCP SDK details. @@ -133,7 +143,7 @@ Add the following Maven dependency to your project: io.github.codeboyzhou mcp-declarative-java-sdk - 0.4.0 + 0.5.0 ``` From 21d6744c2766a3bc2ac0dae4b19ca1f531c81a95 Mon Sep 17 00:00:00 2001 From: codeboyzhou Date: Tue, 10 Jun 2025 23:40:42 +0800 Subject: [PATCH 53/57] fix(config): java.nio.file.FileSystemNotFoundException --- .../declarative/configuration/YAMLConfigurationLoader.java | 6 ++---- .../mcp/declarative/exception/McpServerException.java | 4 ++++ .../resources/mcp-server.yml} | 4 ++-- 3 files changed, 8 insertions(+), 6 deletions(-) rename src/{main/resources/mcp-server-default.yml => test/resources/mcp-server.yml} (89%) diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/configuration/YAMLConfigurationLoader.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/configuration/YAMLConfigurationLoader.java index 57702ea..69cd442 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/configuration/YAMLConfigurationLoader.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/configuration/YAMLConfigurationLoader.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.github.codeboyzhou.mcp.declarative.exception.McpServerException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -23,8 +24,6 @@ public class YAMLConfigurationLoader { private static final ObjectMapper YAML_MAPPER = new ObjectMapper(new YAMLFactory()); - private static final String DEFAULT_CONFIG_FILE_NAME = "mcp-server-default.yml"; - private static final String CONFIG_FILE_NAME = "mcp-server.yml"; private static final String WATCH_THREAD_NAME = "McpServerConfigFileWatcher"; @@ -54,9 +53,8 @@ private Path getConfigFilePath(String fileName) { ClassLoader classLoader = YAMLConfigurationLoader.class.getClassLoader(); URL configFileUrl = classLoader.getResource(fileName); if (configFileUrl == null) { - configFileUrl = classLoader.getResource(DEFAULT_CONFIG_FILE_NAME); + throw new McpServerException("Configuration file not found: " + fileName); } - assert configFileUrl != null; return Paths.get(configFileUrl.toURI()); } catch (URISyntaxException e) { // should never happen diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/exception/McpServerException.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/exception/McpServerException.java index d08af16..ee9b39f 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/exception/McpServerException.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/exception/McpServerException.java @@ -2,6 +2,10 @@ public class McpServerException extends RuntimeException { + public McpServerException(String message) { + super(message); + } + public McpServerException(String message, Throwable cause) { super(message, cause); } diff --git a/src/main/resources/mcp-server-default.yml b/src/test/resources/mcp-server.yml similarity index 89% rename from src/main/resources/mcp-server-default.yml rename to src/test/resources/mcp-server.yml index 944c82d..e720139 100644 --- a/src/main/resources/mcp-server-default.yml +++ b/src/test/resources/mcp-server.yml @@ -1,8 +1,8 @@ enabled: true stdio: false -name: mcp-server +name: mcp-server-test version: 1.0.0 -type: SYNC +type: ASYNC request-timeout: 20000 capabilities: resource: true From 3455173d6c18af695f8a2da62ee06743ef18c335 Mon Sep 17 00:00:00 2001 From: codeboyzhou Date: Tue, 10 Jun 2025 23:49:34 +0800 Subject: [PATCH 54/57] fix(server): ConfigurableMcpHttpSseServer does not start properly --- .../AbstractConfigurableMcpServerFactory.java | 14 ------------ .../ConfigurableMcpHttpSseServerFactory.java | 22 +++++++++++++++++++ .../ConfigurableMcpStdioServerFactory.java | 14 ++++++++++++ 3 files changed, 36 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/AbstractConfigurableMcpServerFactory.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/AbstractConfigurableMcpServerFactory.java index cda934a..a463ba1 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/AbstractConfigurableMcpServerFactory.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/AbstractConfigurableMcpServerFactory.java @@ -3,13 +3,9 @@ import com.github.codeboyzhou.mcp.declarative.configuration.McpServerCapabilities; import com.github.codeboyzhou.mcp.declarative.configuration.McpServerChangeNotification; import com.github.codeboyzhou.mcp.declarative.configuration.McpServerConfiguration; -import io.modelcontextprotocol.server.McpAsyncServer; -import io.modelcontextprotocol.server.McpServer; import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpServerTransportProvider; -import java.time.Duration; - public abstract class AbstractConfigurableMcpServerFactory implements ConfigurableMcpServerFactory { protected final McpServerConfiguration configuration; @@ -18,16 +14,6 @@ protected AbstractConfigurableMcpServerFactory(McpServerConfiguration configurat this.configuration = configuration; } - @Override - public McpAsyncServer create() { - return McpServer.async(transportProvider()) - .serverInfo(configuration.name(), configuration.version()) - .capabilities(serverCapabilities()) - .instructions(configuration.instructions()) - .requestTimeout(Duration.ofMillis(configuration.requestTimeout())) - .build(); - } - @Override public McpSchema.ServerCapabilities serverCapabilities() { McpSchema.ServerCapabilities.Builder capabilities = McpSchema.ServerCapabilities.builder(); diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/ConfigurableMcpHttpSseServerFactory.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/ConfigurableMcpHttpSseServerFactory.java index 41604e5..442293a 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/ConfigurableMcpHttpSseServerFactory.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/ConfigurableMcpHttpSseServerFactory.java @@ -1,10 +1,17 @@ package com.github.codeboyzhou.mcp.declarative.server.factory; +import com.github.codeboyzhou.mcp.declarative.common.NamedThreadFactory; import com.github.codeboyzhou.mcp.declarative.configuration.McpServerConfiguration; import com.github.codeboyzhou.mcp.declarative.configuration.McpServerSSE; +import com.github.codeboyzhou.mcp.declarative.server.McpHttpServer; import com.github.codeboyzhou.mcp.declarative.util.JsonHelper; +import io.modelcontextprotocol.server.McpAsyncServer; +import io.modelcontextprotocol.server.McpServer; import io.modelcontextprotocol.server.transport.HttpServletSseServerTransportProvider; +import java.time.Duration; +import java.util.concurrent.Executors; + public class ConfigurableMcpHttpSseServerFactory extends AbstractConfigurableMcpServerFactory { public ConfigurableMcpHttpSseServerFactory(McpServerConfiguration configuration) { @@ -20,4 +27,19 @@ public HttpServletSseServerTransportProvider transportProvider() { return new HttpServletSseServerTransportProvider(JsonHelper.MAPPER, baseUrl, messageEndpoint, sseEndpoint); } + @Override + public McpAsyncServer create() { + HttpServletSseServerTransportProvider transportProvider = transportProvider(); + McpAsyncServer server = McpServer.async(transportProvider) + .serverInfo(configuration.name(), configuration.version()) + .capabilities(serverCapabilities()) + .instructions(configuration.instructions()) + .requestTimeout(Duration.ofMillis(configuration.requestTimeout())) + .build(); + McpHttpServer httpServer = new McpHttpServer(transportProvider, configuration.sse().port()); + NamedThreadFactory threadFactory = new NamedThreadFactory(McpHttpServer.class.getSimpleName()); + Executors.newSingleThreadExecutor(threadFactory).execute(httpServer::start); + return server; + } + } diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/ConfigurableMcpStdioServerFactory.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/ConfigurableMcpStdioServerFactory.java index 31304c5..ab4aa0c 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/ConfigurableMcpStdioServerFactory.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/ConfigurableMcpStdioServerFactory.java @@ -1,9 +1,13 @@ package com.github.codeboyzhou.mcp.declarative.server.factory; import com.github.codeboyzhou.mcp.declarative.configuration.McpServerConfiguration; +import io.modelcontextprotocol.server.McpAsyncServer; +import io.modelcontextprotocol.server.McpServer; import io.modelcontextprotocol.server.transport.StdioServerTransportProvider; import io.modelcontextprotocol.spec.McpServerTransportProvider; +import java.time.Duration; + public class ConfigurableMcpStdioServerFactory extends AbstractConfigurableMcpServerFactory { public ConfigurableMcpStdioServerFactory(McpServerConfiguration configuration) { @@ -15,4 +19,14 @@ public McpServerTransportProvider transportProvider() { return new StdioServerTransportProvider(); } + @Override + public McpAsyncServer create() { + return McpServer.async(transportProvider()) + .serverInfo(configuration.name(), configuration.version()) + .capabilities(serverCapabilities()) + .instructions(configuration.instructions()) + .requestTimeout(Duration.ofMillis(configuration.requestTimeout())) + .build(); + } + } From d3bdc5844bdf619053f1f7c69be051bf39ec8624 Mon Sep 17 00:00:00 2001 From: codeboyzhou Date: Wed, 11 Jun 2025 00:17:39 +0800 Subject: [PATCH 55/57] test(enhancement): Avoid port occupation --- src/test/resources/mcp-server-async.yml | 4 ++-- src/test/resources/mcp-server-not-enabled.yml | 4 ++-- src/test/resources/mcp-server-sse-mode.yml | 4 ++-- src/test/resources/mcp-server.yml | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/test/resources/mcp-server-async.yml b/src/test/resources/mcp-server-async.yml index 5adc884..e2cbac3 100644 --- a/src/test/resources/mcp-server-async.yml +++ b/src/test/resources/mcp-server-async.yml @@ -15,5 +15,5 @@ change-notification: sse: message-endpoint: /mcp/message endpoint: /sse - base-url: http://localhost:8080 - port: 8080 + base-url: http://localhost:8082 + port: 8082 diff --git a/src/test/resources/mcp-server-not-enabled.yml b/src/test/resources/mcp-server-not-enabled.yml index 57e81a3..ed324a5 100644 --- a/src/test/resources/mcp-server-not-enabled.yml +++ b/src/test/resources/mcp-server-not-enabled.yml @@ -15,5 +15,5 @@ change-notification: sse: message-endpoint: /mcp/message endpoint: /sse - base-url: http://localhost:8080 - port: 8080 + base-url: http://localhost:8083 + port: 8083 diff --git a/src/test/resources/mcp-server-sse-mode.yml b/src/test/resources/mcp-server-sse-mode.yml index 944c82d..483feee 100644 --- a/src/test/resources/mcp-server-sse-mode.yml +++ b/src/test/resources/mcp-server-sse-mode.yml @@ -15,5 +15,5 @@ change-notification: sse: message-endpoint: /mcp/message endpoint: /sse - base-url: http://localhost:8080 - port: 8080 + base-url: http://localhost:8084 + port: 8084 diff --git a/src/test/resources/mcp-server.yml b/src/test/resources/mcp-server.yml index e720139..7efe602 100644 --- a/src/test/resources/mcp-server.yml +++ b/src/test/resources/mcp-server.yml @@ -15,5 +15,5 @@ change-notification: sse: message-endpoint: /mcp/message endpoint: /sse - base-url: http://localhost:8080 - port: 8080 + base-url: http://localhost:8081 + port: 8081 From 62269ee8a50285116c2717f9e6276e3876639160 Mon Sep 17 00:00:00 2001 From: codeboyzhou Date: Wed, 11 Jun 2025 00:50:34 +0800 Subject: [PATCH 56/57] feat(i18n): Allow not to use i18n feature --- .../AbstractMcpServerComponentFactory.java | 20 +++++++++++++++++-- ...p_server_component_descriptions.properties | 0 2 files changed, 18 insertions(+), 2 deletions(-) delete mode 100644 src/test/resources/i18n/mcp_server_component_descriptions.properties diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/AbstractMcpServerComponentFactory.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/AbstractMcpServerComponentFactory.java index 184db72..9253bdd 100644 --- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/AbstractMcpServerComponentFactory.java +++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/AbstractMcpServerComponentFactory.java @@ -1,23 +1,29 @@ package com.github.codeboyzhou.mcp.declarative.server.factory; import com.google.inject.Injector; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.Locale; import java.util.ResourceBundle; public abstract class AbstractMcpServerComponentFactory implements McpServerComponentFactory { + private static final Logger logger = LoggerFactory.getLogger(AbstractMcpServerComponentFactory.class); + + private static final String RESOURCE_BUNDLE_BASE_NAME = "i18n/mcp_server_component_descriptions"; + protected final Injector injector; private final ResourceBundle bundle; protected AbstractMcpServerComponentFactory(Injector injector) { this.injector = injector; - this.bundle = ResourceBundle.getBundle("i18n/mcp_server_component_descriptions", Locale.getDefault()); + this.bundle = loadResourceBundle(); } protected String getDescription(String descriptionI18nKey, String description) { - if (!descriptionI18nKey.isBlank() && bundle.containsKey(descriptionI18nKey)) { + if (!descriptionI18nKey.isBlank() && bundle != null && bundle.containsKey(descriptionI18nKey)) { return bundle.getString(descriptionI18nKey); } if (!description.isBlank()) { @@ -26,4 +32,14 @@ protected String getDescription(String descriptionI18nKey, String description) { return "No description provided."; } + private ResourceBundle loadResourceBundle() { + Locale locale = Locale.getDefault(); + try { + return ResourceBundle.getBundle(RESOURCE_BUNDLE_BASE_NAME, locale); + } catch (Exception e) { + logger.warn("Can't find resource bundle for base name: {}, locale {}, i18n will be unsupported", RESOURCE_BUNDLE_BASE_NAME, locale); + return null; + } + } + } diff --git a/src/test/resources/i18n/mcp_server_component_descriptions.properties b/src/test/resources/i18n/mcp_server_component_descriptions.properties deleted file mode 100644 index e69de29..0000000 From 1dcff5d8ed5eae3c5a663117adf4b849c903fc81 Mon Sep 17 00:00:00 2001 From: codeboyzhou Date: Thu, 12 Jun 2025 09:01:14 +0800 Subject: [PATCH 57/57] chore(pom): Ready to deploy version 0.5.0 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 4137b98..80fcd2d 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ io.github.codeboyzhou mcp-declarative-java-sdk - 0.5.0-SNAPSHOT + 0.5.0 MCP Declarative Java SDK Annotation-driven MCP (Model Context Protocol) Development with Java - No Spring Framework Required