diff --git a/README.md b/README.md
index 9fd3d50..663204d 100644
--- a/README.md
+++ b/README.md
@@ -14,27 +14,61 @@ 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.
+- Built-in multi-languages support for MCP server (resources/prompts/tools).
## Showcase
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.
-@McpComponentScan(basePackage = "com.github.codeboyzhou.mcp.examples")
+// If not specified, it will scan the package where the main method is located.
+@McpComponentScan(basePackage = "com.github.codeboyzhou.mcp.server.examples")
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("mcp-server", "1.0.0");
+ servers.startStdioServer(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");
+ 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");
}
}
```
+This is a yaml configuration file example (named `mcp-server.yml` by default) only if you are using `startServer()` method:
+
+```yaml
+enabled: true
+stdio: false
+name: mcp-server
+version: 1.0.0
+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:
```java
@@ -42,7 +76,9 @@ 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")
+ // 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();
@@ -52,12 +88,31 @@ public class MyMcpResources {
}
```
+```java
+@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.
+ return String.format("What is the complete contents of the file: %s", path);
+ }
+
+}
+```
+
```java
@McpTools
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")
+ // 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.
@@ -84,17 +139,17 @@ 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.2.0
+ 0.5.0
```
### 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?
diff --git a/pom.xml b/pom.xml
index 7c1ba39..80fcd2d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
io.github.codeboyzhou
mcp-declarative-java-sdk
- 0.2.0
+ 0.5.0
MCP Declarative Java SDK
Annotation-driven MCP (Model Context Protocol) Development with Java - No Spring Framework Required
@@ -51,10 +51,14 @@
3.2.3
4.0.0
+ 24.0.0
+ 6.0.0
+ 2.18.3
12.0.18
5.10.2
1.5.18
- 0.9.0
+ 0.10.0
+ 5.18.0
0.10.2
@@ -78,6 +82,16 @@
${logback.version}
test
+
+ com.fasterxml.jackson.dataformat
+ jackson-dataformat-yaml
+ ${jackson-dataformat-yaml.version}
+
+
+ com.google.inject
+ guice
+ ${guice.version}
+
io.modelcontextprotocol.sdk
mcp
@@ -87,12 +101,24 @@
jetty-ee10-servlet
${jetty.version}
+
+ org.jetbrains
+ annotations
+ ${annotations.version}
+ compile
+
org.junit.jupiter
junit-jupiter
${junit5.version}
test
+
+ org.mockito
+ mockito-core
+ ${mockito.version}
+ test
+
org.reflections
reflections
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..9ed3f47 100644
--- a/src/main/java/com/github/codeboyzhou/mcp/declarative/McpServers.java
+++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/McpServers.java
@@ -1,110 +1,102 @@
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.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.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.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.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 org.reflections.Reflections;
-
-import static io.modelcontextprotocol.server.transport.HttpServletSseServerTransportProvider.DEFAULT_BASE_URL;
-import static io.modelcontextprotocol.server.transport.HttpServletSseServerTransportProvider.DEFAULT_SSE_ENDPOINT;
+import io.modelcontextprotocol.util.Assert;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
public class McpServers {
- private static final McpServers INSTANCE = new McpServers();
-
- private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+ private static final Logger logger = LoggerFactory.getLogger(McpServers.class);
- private static final String DEFAULT_MESSAGE_ENDPOINT = "/message";
-
- private static final int DEFAULT_HTTP_SERVER_PORT = 8080;
+ 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 GuiceInjectorModule(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();
+ @Deprecated(since = "0.5.0", forRemoval = true)
+ public void startSyncStdioServer(McpServerInfo serverInfo) {
+ McpStdioServerFactory factory = new McpStdioServerFactory();
+ McpAsyncServer server = factory.create(serverInfo);
+ registerComponents(server);
}
- public void startSyncStdioServer(String name, String version, String instructions) {
- 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);
+ @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.2.0")
- public void startSyncStdioServer(String name, String version) {
- startSyncStdioServer(name, version, "You are using a deprecated API with default server instructions");
+ @Deprecated(since = "0.5.0", forRemoval = true)
+ public void startSyncSseServer(McpSseServerInfo serverInfo) {
+ startSyncSseServer(serverInfo, new DefaultMcpSyncHttpServerStatusListener());
}
- public void startSyncSseServer(McpSseServerInfo serverInfo, McpHttpServerStatusListener listener) {
- McpServerFactory factory = new McpSyncServerFactory();
- HttpServletSseServerTransportProvider transportProvider = new HttpServletSseServerTransportProvider(
- OBJECT_MAPPER, serverInfo.baseUrl(), serverInfo.messageEndpoint(), serverInfo.sseEndpoint()
- );
- McpSyncServer server = factory.create(serverInfo, transportProvider);
- McpServerComponentRegisters.registerAllTo(server, reflections);
- McpHttpServer httpServer = new McpHttpServer<>();
- httpServer.with(transportProvider).with(serverInfo).with(listener).attach(server).start();
+ public void startStdioServer(McpServerInfo serverInfo) {
+ McpStdioServerFactory factory = new McpStdioServerFactory();
+ McpAsyncServer server = factory.create(serverInfo);
+ registerComponents(server);
}
- public void startSyncSseServer(McpSseServerInfo serverInfo) {
- startSyncSseServer(serverInfo, new DefaultMcpSyncHttpServerStatusListener());
+ 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());
}
- @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);
+ public void startServer() {
+ doStartServer(new YAMLConfigurationLoader().getConfig());
}
- @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);
+ private void doStartServer(McpServerConfiguration configuration) {
+ if (!configuration.enabled()) {
+ logger.warn("MCP server is disabled, please check your configuration file.");
+ return;
+ }
+
+ ConfigurableMcpServerFactory extends McpServerTransportProvider> factory;
+ if (configuration.stdio()) {
+ factory = new ConfigurableMcpStdioServerFactory(configuration);
+ } else {
+ factory = new ConfigurableMcpHttpSseServerFactory(configuration);
+ }
+ McpAsyncServer server = factory.create();
+ registerComponents(server);
}
- @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);
+ private void registerComponents(McpAsyncServer server) {
+ 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/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/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..04b80cb
--- /dev/null
+++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpJsonSchemaDefinitionProperty.java
@@ -0,0 +1,21 @@
+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() 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
new file mode 100644
index 0000000..30b8eda
--- /dev/null
+++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpPrompt.java
@@ -0,0 +1,20 @@
+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.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface McpPrompt {
+
+ String name() default StringHelper.EMPTY;
+
+ 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
new file mode 100644
index 0000000..fd8c7d4
--- /dev/null
+++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpPromptParam.java
@@ -0,0 +1,22 @@
+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.PARAMETER)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface McpPromptParam {
+
+ String name();
+
+ 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/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/annotation/McpResource.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpResource.java
index a81e5b6..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
@@ -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,13 +13,16 @@
public @interface McpResource {
String uri();
- String name() default "";
+ 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 ea52b23..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
@@ -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,10 @@
@Retention(RetentionPolicy.RUNTIME)
public @interface McpTool {
- String name() default "";
+ String name() default StringHelper.EMPTY;
+
+ String description() default StringHelper.EMPTY;
+
+ String descriptionI18nKey() default StringHelper.EMPTY;
- String description();
}
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/common/BufferQueue.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/common/BufferQueue.java
new file mode 100644
index 0000000..ce33b40
--- /dev/null
+++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/common/BufferQueue.java
@@ -0,0 +1,59 @@
+package com.github.codeboyzhou.mcp.declarative.common;
+
+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.Consumer;
+
+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 long delayMillis;
+
+ public BufferQueue(long delayMillis) {
+ if (delayMillis <= 0) {
+ throw new IllegalArgumentException("delayMillis must be greater than 0");
+ }
+ this.delayMillis = delayMillis;
+ }
+
+ public BufferQueue() {
+ this(DEFAULT_DELAYED_CONSUMPTION_MILLIS);
+ }
+
+ 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(Consumer consumer) {
+ NamedThreadFactory threadFactory = new NamedThreadFactory(getClass().getSimpleName());
+ Executors.newSingleThreadExecutor(threadFactory).execute(() -> {
+ try {
+ while (!Thread.interrupted()) {
+ T component = queue.take();
+ consumer.accept(component);
+ logger.debug("Component consumed from queue: {}", JsonHelper.toJson(component));
+ TimeUnit.MILLISECONDS.sleep(delayMillis);
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/common/GuiceInjectorModule.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/common/GuiceInjectorModule.java
new file mode 100644
index 0000000..68c8c56
--- /dev/null
+++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/common/GuiceInjectorModule.java
@@ -0,0 +1,62 @@
+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.Singleton;
+import org.reflections.Reflections;
+
+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 GuiceInjectorModule(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, 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(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) {
+ 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/main/java/com/github/codeboyzhou/mcp/declarative/common/NamedThreadFactory.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/common/NamedThreadFactory.java
new file mode 100644
index 0000000..fdef89a
--- /dev/null
+++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/common/NamedThreadFactory.java
@@ -0,0 +1,25 @@
+package com.github.codeboyzhou.mcp.declarative.common;
+
+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());
+ }
+
+}
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
new file mode 100644
index 0000000..75e95d4
--- /dev/null
+++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/configuration/McpServerConfiguration.java
@@ -0,0 +1,18 @@
+package com.github.codeboyzhou.mcp.declarative.configuration;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+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("capabilities") McpServerCapabilities capabilities,
+ @JsonProperty("change-notification") McpServerChangeNotification changeNotification,
+ @JsonProperty("sse") McpServerSSE sse
+) {
+}
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
new file mode 100644
index 0000000..69cd442
--- /dev/null
+++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/configuration/YAMLConfigurationLoader.java
@@ -0,0 +1,110 @@
+package com.github.codeboyzhou.mcp.declarative.configuration;
+
+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;
+
+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 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) {
+ throw new McpServerException("Configuration file not found: " + fileName);
+ }
+ 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/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/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);
+ }
+
+}
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..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);
@@ -77,14 +49,14 @@ 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;
}
// 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);
}
}
@@ -93,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/McpServerComponentRegister.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpServerComponentRegister.java
deleted file mode 100644
index e290d1d..0000000
--- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpServerComponentRegister.java
+++ /dev/null
@@ -1,10 +0,0 @@
-package com.github.codeboyzhou.mcp.declarative.server;
-
-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/McpServerComponentRegisters.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpServerComponentRegisters.java
deleted file mode 100644
index 6a016d1..0000000
--- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpServerComponentRegisters.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package com.github.codeboyzhou.mcp.declarative.server;
-
-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> toolClasses = reflections.getTypesAnnotatedWith(McpTools.class);
- new McpSyncServerToolRegister(toolClasses).registerTo(server);
- }
-
-}
diff --git a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpServerFactory.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpServerFactory.java
deleted file mode 100644
index b0567ea..0000000
--- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpServerFactory.java
+++ /dev/null
@@ -1,18 +0,0 @@
-package com.github.codeboyzhou.mcp.declarative.server;
-
-import io.modelcontextprotocol.spec.McpSchema;
-import io.modelcontextprotocol.spec.McpServerTransportProvider;
-
-public interface McpServerFactory {
-
- T create(McpServerInfo serverInfo, McpServerTransportProvider transportProvider);
-
- default McpSchema.ServerCapabilities configureServerCapabilities() {
- return McpSchema.ServerCapabilities.builder()
- .resources(true, true)
- .prompts(true)
- .tools(true)
- .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 6674e1c..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,5 +1,9 @@
package com.github.codeboyzhou.mcp.declarative.server;
+import com.github.codeboyzhou.mcp.declarative.util.StringHelper;
+
+import java.time.Duration;
+
public class McpServerInfo {
private final String name;
@@ -8,10 +12,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,14 +37,20 @@ public String instructions() {
return instructions;
}
+ public Duration requestTimeout() {
+ return 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 = StringHelper.EMPTY;
+
+ protected Duration requestTimeout = Duration.ofSeconds(20);
protected T self() {
return (T) this;
@@ -62,6 +75,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/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() {
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
deleted file mode 100644
index b05da27..0000000
--- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerFactory.java
+++ /dev/null
@@ -1,18 +0,0 @@
-package com.github.codeboyzhou.mcp.declarative.server;
-
-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())
- .build();
- }
-
-}
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
deleted file mode 100644
index ae715ca..0000000
--- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerResourceRegister.java
+++ /dev/null
@@ -1,60 +0,0 @@
-package com.github.codeboyzhou.mcp.declarative.server;
-
-import com.github.codeboyzhou.mcp.declarative.annotation.McpResource;
-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.util.List;
-import java.util.Set;
-
-public class McpSyncServerResourceRegister
- implements McpServerComponentRegister {
-
- private static final Logger logger = LoggerFactory.getLogger(McpSyncServerResourceRegister.class);
-
- private final Set> resourceClasses;
-
- public McpSyncServerResourceRegister(Set> resourceClasses) {
- this.resourceClasses = resourceClasses;
- }
-
- @Override
- public void registerTo(McpSyncServer server) {
- for (Class> resourceClass : resourceClasses) {
- Set methods = ReflectionHelper.getMethodsAnnotatedWith(resourceClass, McpResource.class);
- for (Method method : methods) {
- McpServerFeatures.SyncResourceSpecification resource = createComponentFrom(resourceClass, method);
- server.addResource(resource);
- }
- }
- }
-
- @Override
- public McpServerFeatures.SyncResourceSpecification createComponentFrom(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())
- );
- return new McpServerFeatures.SyncResourceSpecification(resource, (exchange, request) -> {
- Object result;
- try {
- result = ReflectionHelper.invokeMethod(clazz, method);
- } catch (Throwable 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));
- });
- }
-
-}
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
deleted file mode 100644
index a10d80c..0000000
--- a/src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpSyncServerToolRegister.java
+++ /dev/null
@@ -1,89 +0,0 @@
-package com.github.codeboyzhou.mcp.declarative.server;
-
-import com.github.codeboyzhou.mcp.declarative.annotation.McpTool;
-import com.github.codeboyzhou.mcp.declarative.annotation.McpToolParam;
-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.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-public class McpSyncServerToolRegister
- implements McpServerComponentRegister {
-
- 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;
- }
-
- @Override
- public void registerTo(McpSyncServer server) {
- for (Class> toolClass : toolClasses) {
- Set methods = ReflectionHelper.getMethodsAnnotatedWith(toolClass, McpTool.class);
- for (Method method : methods) {
- McpServerFeatures.SyncToolSpecification tool = createComponentFrom(toolClass, method);
- server.addTool(tool);
- }
- }
- }
-
- @Override
- public McpServerFeatures.SyncToolSpecification createComponentFrom(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);
- return new McpServerFeatures.SyncToolSpecification(tool, (exchange, params) -> {
- Object result;
- boolean isError = false;
- try {
- result = ReflectionHelper.invokeMethod(clazz, method, paramSchema, params);
- } catch (Throwable 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);
- });
- }
-
- private McpSchema.JsonSchema createJsonSchema(Method method) {
- Map properties = 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();
-
- Map parameterProperties = Map.of(
- "type", parameterType,
- "description", toolParam.description()
- );
- properties.put(parameterName, parameterProperties);
-
- if (toolParam.required()) {
- required.add(parameterName);
- }
- }
-
- return new McpSchema.JsonSchema(OBJECT_TYPE_NAME, properties, required, false);
- }
-
-}
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
new file mode 100644
index 0000000..a463ba1
--- /dev/null
+++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/AbstractConfigurableMcpServerFactory.java
@@ -0,0 +1,34 @@
+package com.github.codeboyzhou.mcp.declarative.server.factory;
+
+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.spec.McpSchema;
+import io.modelcontextprotocol.spec.McpServerTransportProvider;
+
+public abstract class AbstractConfigurableMcpServerFactory implements ConfigurableMcpServerFactory {
+
+ protected final McpServerConfiguration configuration;
+
+ protected AbstractConfigurableMcpServerFactory(McpServerConfiguration configuration) {
+ this.configuration = configuration;
+ }
+
+ @Override
+ public McpSchema.ServerCapabilities serverCapabilities() {
+ 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/factory/AbstractMcpServerComponentFactory.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/AbstractMcpServerComponentFactory.java
new file mode 100644
index 0000000..9253bdd
--- /dev/null
+++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/AbstractMcpServerComponentFactory.java
@@ -0,0 +1,45 @@
+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 = loadResourceBundle();
+ }
+
+ protected String getDescription(String descriptionI18nKey, String description) {
+ if (!descriptionI18nKey.isBlank() && bundle != null && bundle.containsKey(descriptionI18nKey)) {
+ return bundle.getString(descriptionI18nKey);
+ }
+ if (!description.isBlank()) {
+ return 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/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..520dc5e
--- /dev/null
+++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/AbstractMcpServerFactory.java
@@ -0,0 +1,18 @@
+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 {
+
+ @Override
+ public 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/ConfigurableMcpHttpSseServerFactory.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/ConfigurableMcpHttpSseServerFactory.java
new file mode 100644
index 0000000..442293a
--- /dev/null
+++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/ConfigurableMcpHttpSseServerFactory.java
@@ -0,0 +1,45 @@
+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) {
+ 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);
+ }
+
+ @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/ConfigurableMcpServerFactory.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/ConfigurableMcpServerFactory.java
new file mode 100644
index 0000000..07ed42e
--- /dev/null
+++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/ConfigurableMcpServerFactory.java
@@ -0,0 +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 {
+
+ T transportProvider();
+
+ McpAsyncServer create();
+
+ 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..ab4aa0c
--- /dev/null
+++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/ConfigurableMcpStdioServerFactory.java
@@ -0,0 +1,32 @@
+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) {
+ super(configuration);
+ }
+
+ @Override
+ 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();
+ }
+
+}
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..9e5e0e3
--- /dev/null
+++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpHttpSseServerFactory.java
@@ -0,0 +1,38 @@
+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;
+import io.modelcontextprotocol.server.McpAsyncServer;
+import io.modelcontextprotocol.server.McpServer;
+import io.modelcontextprotocol.server.transport.HttpServletSseServerTransportProvider;
+
+import java.util.concurrent.Executors;
+
+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());
+ 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/McpServerComponentFactory.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerComponentFactory.java
new file mode 100644
index 0000000..553a17e
--- /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.McpAsyncServer;
+
+import java.lang.reflect.Method;
+
+public interface McpServerComponentFactory {
+
+ T create(Class> clazz, Method method);
+
+ void registerTo(McpAsyncServer 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
new file mode 100644
index 0000000..8080f3c
--- /dev/null
+++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerFactory.java
@@ -0,0 +1,16 @@
+package com.github.codeboyzhou.mcp.declarative.server.factory;
+
+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 {
+
+ T transportProvider(S serverInfo);
+
+ McpAsyncServer create(S serverInfo);
+
+ McpSchema.ServerCapabilities serverCapabilities();
+
+}
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
new file mode 100644
index 0000000..8722146
--- /dev/null
+++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerPromptFactory.java
@@ -0,0 +1,110 @@
+package com.github.codeboyzhou.mcp.declarative.server.factory;
+
+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.common.BufferQueue;
+import com.github.codeboyzhou.mcp.declarative.util.JsonHelper;
+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.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;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Stream;
+
+public class McpServerPromptFactory extends AbstractMcpServerComponentFactory {
+
+ private static final Logger logger = LoggerFactory.getLogger(McpServerPromptFactory.class);
+
+ @Inject
+ protected McpServerPromptFactory(Injector injector) {
+ super(injector);
+ }
+
+ @Override
+ 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 = 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));
+ 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(McpAsyncServer 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.AsyncPromptSpecification prompt = create(promptClass, method);
+ queue.submit(prompt);
+ }
+ }
+ queue.consume(prompt -> server.addPrompt(prompt).subscribe());
+ }
+
+ private List createPromptArguments(Method method) {
+ 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 = getDescription(promptParam.descriptionI18nKey(), promptParam.description());
+ final boolean required = promptParam.required();
+ McpSchema.PromptArgument promptArgument = new McpSchema.PromptArgument(name, description, required);
+ promptArguments.add(promptArgument);
+ }
+ 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/factory/McpServerResourceFactory.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerResourceFactory.java
new file mode 100644
index 0000000..3835552
--- /dev/null
+++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerResourceFactory.java
@@ -0,0 +1,72 @@
+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.McpAsyncServer;
+import io.modelcontextprotocol.server.McpServerFeatures;
+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 {
+
+ private static final Logger logger = LoggerFactory.getLogger(McpServerResourceFactory.class);
+
+ @Inject
+ protected McpServerResourceFactory(Injector injector) {
+ super(injector);
+ }
+
+ @Override
+ public McpServerFeatures.AsyncResourceSpecification create(Class> clazz, Method method) {
+ McpResource res = method.getAnnotation(McpResource.class);
+ final String name = res.name().isBlank() ? method.getName() : res.name();
+ 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(() -> {
+ 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(McpAsyncServer 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.AsyncResourceSpecification resource = create(resourceClass, method);
+ queue.submit(resource);
+ }
+ }
+ 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
new file mode 100644
index 0000000..a8896ba
--- /dev/null
+++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerToolFactory.java
@@ -0,0 +1,173 @@
+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.McpAsyncServer;
+import io.modelcontextprotocol.server.McpServerFeatures;
+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;
+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;
+import java.util.stream.Stream;
+
+public class McpServerToolFactory extends AbstractMcpServerComponentFactory {
+
+ private static final Logger logger = LoggerFactory.getLogger(McpServerToolFactory.class);
+
+ @Inject
+ protected McpServerToolFactory(Injector injector) {
+ super(injector);
+ }
+
+ @Override
+ 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();
+ 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(() -> {
+ 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(McpAsyncServer 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.AsyncToolSpecification tool = create(toolClass, method);
+ queue.submit(tool);
+ }
+ }
+ queue.consume(tool -> server.addTool(tool).subscribe());
+ }
+
+ private McpSchema.JsonSchema createJsonSchema(Method method) {
+ Map properties = new LinkedHashMap<>();
+ Map definitions = new LinkedHashMap<>();
+ List required = new ArrayList<>();
+
+ 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 = param.getType();
+ Map property = new HashMap<>();
+
+ if (parameterType.getAnnotation(McpJsonSchemaDefinition.class) == null) {
+ property.put("type", parameterType.getSimpleName().toLowerCase());
+ property.put("description", getDescription(toolParam.descriptionI18nKey(), 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);
+ }
+ }
+
+ final boolean hasAdditionalProperties = false;
+ return new McpSchema.JsonSchema(JsonSchemaDataType.OBJECT.getType(), properties, required, hasAdditionalProperties, definitions, definitions);
+ }
+
+ private Map createJsonSchemaDefinition(Class> definitionClass) {
+ Map definitionJsonSchema = new HashMap<>();
+ definitionJsonSchema.put("type", JsonSchemaDataType.OBJECT.getType());
+
+ Map properties = new LinkedHashMap<>();
+ List required = new ArrayList<>();
+
+ 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) {
+ continue;
+ }
+
+ Map fieldProperties = new HashMap<>();
+ fieldProperties.put("type", field.getType().getSimpleName().toLowerCase());
+ fieldProperties.put("description", getDescription(property.descriptionI18nKey(), 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;
+ }
+
+ @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/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/util/JsonHelper.java b/src/main/java/com/github/codeboyzhou/mcp/declarative/util/JsonHelper.java
new file mode 100644
index 0000000..5045b0e
--- /dev/null
+++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/util/JsonHelper.java
@@ -0,0 +1,25 @@
+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;
+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);
+ } catch (JsonProcessingException e) {
+ throw new McpServerException("Error converting object to JSON", e);
+ }
+ }
+
+}
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 e53e30b..0000000
--- a/src/main/java/com/github/codeboyzhou/mcp/declarative/util/ReflectionHelper.java
+++ /dev/null
@@ -1,68 +0,0 @@
-package com.github.codeboyzhou.mcp.declarative.util;
-
-import io.modelcontextprotocol.spec.McpSchema;
-
-import java.lang.annotation.Annotation;
-import java.lang.reflect.Method;
-import java.lang.reflect.Parameter;
-import java.util.LinkedHashMap;
-import java.util.Map;
-import java.util.Set;
-
-import static java.util.stream.Collectors.toSet;
-
-public final class ReflectionHelper {
-
- public static Set getMethodsAnnotatedWith(Class> clazz, Class extends Annotation> annotation) {
- Method[] methods = clazz.getMethods();
- return Set.of(methods).stream().filter(m -> m.isAnnotationPresent(annotation)).collect(toSet());
- }
-
- public static Set getParametersAnnotatedWith(Method method, Class extends Annotation> annotation) {
- Parameter[] parameters = method.getParameters();
- return Set.of(parameters).stream().filter(p -> p.isAnnotationPresent(annotation)).collect(toSet());
- }
-
- 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, McpSchema.JsonSchema schema, Map parameters) throws Exception {
- Object object = clazz.getDeclaredConstructor().newInstance();
- Map typedParameters = asTypedParameters(schema, parameters);
- return method.invoke(object, typedParameters.values().toArray());
- }
-
- @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);
- 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)) {
- typedParameters.put(parameterName, 0);
- } else if (isTypeOf(Number.class, parameterType)) {
- typedParameters.put(parameterName, 0.0);
- } else if (isTypeOf(Boolean.class, parameterType)) {
- typedParameters.put(parameterName, false);
- }
- } else {
- typedParameters.put(parameterName, parameterValue);
- }
- });
-
- return typedParameters;
- }
-
- private static boolean isTypeOf(Class> clazz, String jsonSchemaType) {
- return clazz.getName().equalsIgnoreCase(jsonSchemaType) || clazz.getSimpleName().equalsIgnoreCase(jsonSchemaType);
- }
-
-}
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..337ec06
--- /dev/null
+++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/util/StringHelper.java
@@ -0,0 +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
new file mode 100644
index 0000000..816d9e3
--- /dev/null
+++ b/src/main/java/com/github/codeboyzhou/mcp/declarative/util/TypeConverter.java
@@ -0,0 +1,103 @@
+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);
+ }
+
+ 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 (JsonSchemaDataType.STRING.getType().equals(jsonSchemaType)) {
+ return valueAsString;
+ }
+ if (JsonSchemaDataType.INTEGER.getType().equals(jsonSchemaType)) {
+ return Integer.parseInt(valueAsString);
+ }
+ if (JsonSchemaDataType.NUMBER.getType().equals(jsonSchemaType)) {
+ return Double.parseDouble(valueAsString);
+ }
+ if (JsonSchemaDataType.BOOLEAN.getType().equals(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;
+ }
+
+}
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..13c2629 100644
--- a/src/test/java/com/github/codeboyzhou/mcp/declarative/McpServersTest.java
+++ b/src/test/java/com/github/codeboyzhou/mcp/declarative/McpServersTest.java
@@ -1,19 +1,29 @@
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.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;
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 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;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
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;
@@ -25,17 +35,30 @@ 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 GuiceInjectorModule(TestMcpComponentScanIsNull.class));
+ Reflections reflections = injector.getInstance(Reflections.class);
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;
}
@ParameterizedTest
@@ -50,28 +73,56 @@ void testRun(Class> applicationMainClass) {
}
@Test
- void testStartSyncStdioServer() {
+ void testStartStdioServer() {
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")
+ .requestTimeout(Duration.ofSeconds(20))
+ .name("test-mcp-sync-stdio-server")
+ .version("1.0.0")
+ .build();
+ servers.startStdioServer(serverInfo);
});
}
@Test
- void testStartSyncSseServer() {
- System.setProperty("mcp.declarative.java.sdk.testing", "true");
+ void testStartSseServer() {
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")
+ .requestTimeout(Duration.ofSeconds(20))
+ .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.startSseServer(serverInfo);
+ });
+ }
+
+ @Test
+ void testStartServer() {
+ assertDoesNotThrow(() -> {
+ McpServers servers = McpServers.run(TestMcpComponentScanIsNull.class, EMPTY_ARGS);
+ servers.startServer();
+ });
}
- 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;
+ @ParameterizedTest
+ @ValueSource(strings = {
+ "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);
+ });
}
}
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();
+ }
+
+}
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..b805bc9
--- /dev/null
+++ b/src/test/java/com/github/codeboyzhou/mcp/declarative/server/TestMcpPrompts.java
@@ -0,0 +1,28 @@
+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 {
+
+ @SuppressWarnings("unused")
+ @McpPrompt(name = "prompt1", description = "prompt1")
+ public static String prompt1(
+ @McpPromptParam(name = "argument1", description = "argument1", required = true) String argument1,
+ @McpPromptParam(name = "argument2", description = "argument2", required = true) String argument2
+ ) {
+ 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 = "argument1", description = "argument1") String argument1,
+ @McpPromptParam(name = "argument2", description = "argument2") String argument2
+ ) {
+ 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/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..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
@@ -7,20 +7,32 @@
@McpTools
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")
+ @McpTool(description = "tool3")
+ public static String tool3(
+ @McpToolParam(name = "complexJsonSchema", description = "complexJsonSchema")
+ TestMcpToolComplexJsonSchema complexJsonSchema
+ ) {
+ return String.format("This is tool3 for testing complex json schema: my name is %s, I am from %s",
+ complexJsonSchema.name(), complexJsonSchema.country());
}
}
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"));
+ }
+
+}
diff --git a/src/test/resources/mcp-server-async.yml b/src/test/resources/mcp-server-async.yml
new file mode 100644
index 0000000..e2cbac3
--- /dev/null
+++ b/src/test/resources/mcp-server-async.yml
@@ -0,0 +1,19 @@
+enabled: true
+stdio: true
+name: mcp-server
+version: 1.0.0
+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:8082
+ port: 8082
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..ed324a5
--- /dev/null
+++ b/src/test/resources/mcp-server-not-enabled.yml
@@ -0,0 +1,19 @@
+enabled: false
+stdio: true
+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:8083
+ port: 8083
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..483feee
--- /dev/null
+++ b/src/test/resources/mcp-server-sse-mode.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:8084
+ port: 8084
diff --git a/src/test/resources/mcp-server.yml b/src/test/resources/mcp-server.yml
new file mode 100644
index 0000000..7efe602
--- /dev/null
+++ b/src/test/resources/mcp-server.yml
@@ -0,0 +1,19 @@
+enabled: true
+stdio: false
+name: mcp-server-test
+version: 1.0.0
+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:8081
+ port: 8081