Skip to content

Commit 5d8b9fd

Browse files
committed
refactor(main): Refactor McpServers for improved functionality
1 parent 4b576dd commit 5d8b9fd

File tree

9 files changed

+284
-54
lines changed

9 files changed

+284
-54
lines changed

README.adoc

+94-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,96 @@
11
= Annotation-driven MCP Java SDK
22

3-
Declarative MCP (Model Context Protocol) Development with Java Annotations and No Spring Framework Required.
3+
Declarative https://github.com/modelcontextprotocol/java-sdk[MCP Java SDK] Development with Java Annotations - No Spring Framework Required.
4+
5+
Focus on your core logic (resources/prompts/tools) - Not SDK low-level details. Instant MCP Java server in 1 LOC.
6+
7+
== Showcase
8+
9+
Just put this one line code in your `main` method:
10+
11+
[source,java]
12+
----
13+
// You can specify the base package to scan for MCP resources, prompts, tools, but it's optional.
14+
@McpComponentScan(basePackage = "com.github.codeboyzhou.mcp.examples")
15+
public class MyMcpServer {
16+
17+
public static void main(String[] args) {
18+
// Start a STDIO MCP server
19+
McpServers.run(MyMcpServer.class, args).startSyncStdioServer("mcp-server", "1.0.0");
20+
// or a HTTP SSE MCP server
21+
// McpServers.run(MyMcpServer.class, args).startSyncSseServer("mcp-server", "1.0.0");
22+
}
23+
24+
}
25+
----
26+
27+
and don't need to care how to create the MCP resources, prompts, and tools. You only need to annotate them like this:
28+
29+
[source,java]
30+
----
31+
@McpResources
32+
public class MyMcpResources {
33+
34+
// This method defines a MCP resource to expose the OS env variables
35+
@McpResource(uri = "env://variables", name = "env", description = "OS env variables")
36+
public String getSystemEnv() {
37+
// Only need to put your logic code here, forget about the MCP SDK details.
38+
return System.getenv().toString();
39+
}
40+
41+
// Your other MCP resources here...
42+
}
43+
----
44+
45+
[source,java]
46+
----
47+
@McpTools
48+
public class MyMcpTools {
49+
50+
// This method defines a MCP tool to read a file
51+
@McpTool(name = "read_file", description = "Read complete file contents with UTF-8 encoding")
52+
public String readFile(
53+
@McpToolParam(name = "path", description = "filepath", required = true) String path) {
54+
// Only need to put your logic code here, forget about the MCP SDK details.
55+
return Files.readString(Path.of(path));
56+
}
57+
58+
// Your other MCP tools here...
59+
}
60+
----
61+
62+
Now it's all set, choose one MCP client you like and start your MCP exploration journey.
63+
64+
[WARNING]
65+
66+
Please note that this project is under development and is not ready for production use.
67+
68+
== Getting Started
69+
70+
=== Installation
71+
72+
Add the following Maven dependency to your project:
73+
74+
[source,xml]
75+
----
76+
<dependency>
77+
<groupId>io.github.codeboyzhou</groupId>
78+
<artifactId>mcp-declarative-java-sdk</artifactId>
79+
<version>0.1.0-SNAPSHOT</version>
80+
</dependency>
81+
----
82+
83+
== Requirements
84+
85+
- Java 17 or later (Restricted by MCP Java SDK)
86+
87+
== What is MCP?
88+
89+
The https://modelcontextprotocol.io[Model Context Protocol (MCP)] lets you build servers that expose data and functionality to LLM applications in a secure, standardized way. Think of it like a web API, but specifically designed for LLM interactions. MCP servers can:
90+
91+
- Expose data through **Resources** (think of these sort of like GET endpoints; they are used to load information into the LLM's context)
92+
- Provide functionality through **Tools** (sort of like POST endpoints; they are used to execute code or otherwise produce a side effect)
93+
- Define interaction patterns through **Prompts** (reusable templates for LLM interactions)
94+
- And more!
95+
96+
You can start exploring everything about *MCP* from https://modelcontextprotocol.io[here].

pom.xml

+62-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
<modelVersion>4.0.0</modelVersion>
77

8-
<groupId>com.github.codeboyzhou</groupId>
8+
<groupId>io.github.codeboyzhou</groupId>
99
<artifactId>mcp-declarative-java-sdk</artifactId>
1010
<version>0.1.0-SNAPSHOT</version>
1111

@@ -15,6 +15,11 @@
1515
<maven.compiler.target>${java.version}</maven.compiler.target>
1616
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
1717
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
18+
<!--==================== maven plugin versions ====================-->
19+
<central-publishing-maven-plugin.version>0.7.0</central-publishing-maven-plugin.version>
20+
<maven-gpg-plugin.version>3.1.0</maven-gpg-plugin.version>
21+
<maven-javadoc-plugin.version>3.6.3</maven-javadoc-plugin.version>
22+
<maven-source-plugin.version>3.3.0</maven-source-plugin.version>
1823
<!--==================== dependency versions ======================-->
1924
<mcp-sdk.version>0.8.1</mcp-sdk.version>
2025
<jetty.version>12.0.18</jetty.version>
@@ -50,4 +55,60 @@
5055
</dependency>
5156
</dependencies>
5257

58+
<build>
59+
<plugins>
60+
<plugin>
61+
<groupId>org.apache.maven.plugins</groupId>
62+
<artifactId>maven-gpg-plugin</artifactId>
63+
<version>${maven-gpg-plugin.version}</version>
64+
<executions>
65+
<execution>
66+
<id>sign-artifacts</id>
67+
<phase>verify</phase>
68+
<goals>
69+
<goal>sign</goal>
70+
</goals>
71+
</execution>
72+
</executions>
73+
</plugin>
74+
<plugin>
75+
<groupId>org.apache.maven.plugins</groupId>
76+
<artifactId>maven-javadoc-plugin</artifactId>
77+
<version>${maven-javadoc-plugin.version}</version>
78+
<executions>
79+
<execution>
80+
<id>attach-javadocs</id>
81+
<goals>
82+
<goal>jar</goal>
83+
</goals>
84+
</execution>
85+
</executions>
86+
</plugin>
87+
<plugin>
88+
<groupId>org.apache.maven.plugins</groupId>
89+
<artifactId>maven-source-plugin</artifactId>
90+
<version>${maven-source-plugin.version}</version>
91+
<executions>
92+
<execution>
93+
<id>attach-sources</id>
94+
<goals>
95+
<goal>jar-no-fork</goal>
96+
</goals>
97+
</execution>
98+
</executions>
99+
</plugin>
100+
<plugin>
101+
<groupId>org.sonatype.central</groupId>
102+
<artifactId>central-publishing-maven-plugin</artifactId>
103+
<version>${central-publishing-maven-plugin.version}</version>
104+
<extensions>true</extensions>
105+
<configuration>
106+
<autoPublish>true</autoPublish>
107+
<publishingServerId>central</publishingServerId>
108+
<deploymentName>${project.groupId}:${project.artifactId}:${project.version}</deploymentName>
109+
</configuration>
110+
</plugin>
111+
</plugins>
112+
</build>
113+
53114
</project>

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

+42-27
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import com.fasterxml.jackson.databind.ObjectMapper;
44
import com.github.codeboyzhou.mcp.declarative.annotation.*;
5-
import com.github.codeboyzhou.mcp.declarative.util.Annotations;
5+
import com.github.codeboyzhou.mcp.declarative.util.ReflectionHelper;
66
import io.modelcontextprotocol.server.McpServer;
77
import io.modelcontextprotocol.server.McpServerFeatures;
88
import io.modelcontextprotocol.server.McpSyncServer;
@@ -24,6 +24,8 @@ public class McpServers {
2424

2525
private static final Logger logger = LoggerFactory.getLogger(McpServers.class);
2626

27+
private static final McpServers INSTANCE = new McpServers();
28+
2729
private static final McpSchema.ServerCapabilities DEFAULT_SERVER_CAPABILITIES = McpSchema.ServerCapabilities
2830
.builder()
2931
.resources(true, true)
@@ -33,17 +35,29 @@ public class McpServers {
3335

3436
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
3537

36-
private static final String OBJECT_TYPE_NAME = Object.class.getName().toLowerCase();
37-
38-
private static final Reflections REFLECTIONS = new Reflections();
38+
private static final String OBJECT_TYPE_NAME = Object.class.getSimpleName().toLowerCase();
3939

4040
private static final String DEFAULT_MESSAGE_ENDPOINT = "/message";
4141

4242
private static final String DEFAULT_SSE_ENDPOINT = "/sse";
4343

4444
private static final int DEFAULT_HTTP_SERVER_PORT = 8080;
4545

46-
public static void startSyncStdioServer(String name, String version) {
46+
private static Reflections reflections;
47+
48+
public static McpServers run(Class<?> applicationMainClass, String[] args) {
49+
McpComponentScan scan = applicationMainClass.getAnnotation(McpComponentScan.class);
50+
if (scan == null) {
51+
reflections = new Reflections(applicationMainClass.getPackageName());
52+
} else if (!scan.basePackage().trim().isBlank()) {
53+
reflections = new Reflections(scan.basePackage());
54+
} else if (scan.basePackageClass() != Object.class) {
55+
reflections = new Reflections(scan.basePackageClass().getPackageName());
56+
}
57+
return INSTANCE;
58+
}
59+
60+
public void startSyncStdioServer(String name, String version) {
4761
McpSyncServer server = McpServer.sync(new StdioServerTransportProvider())
4862
.capabilities(DEFAULT_SERVER_CAPABILITIES)
4963
.serverInfo(name, version)
@@ -53,15 +67,15 @@ public static void startSyncStdioServer(String name, String version) {
5367
registerTools(server);
5468
}
5569

56-
public static void startSyncSseServer(String name, String version) {
70+
public void startSyncSseServer(String name, String version) {
5771
startSyncSseServer(name, version, DEFAULT_MESSAGE_ENDPOINT, DEFAULT_SSE_ENDPOINT, DEFAULT_HTTP_SERVER_PORT);
5872
}
5973

60-
public static void startSyncSseServer(String name, String version, int port) {
74+
public void startSyncSseServer(String name, String version, int port) {
6175
startSyncSseServer(name, version, DEFAULT_MESSAGE_ENDPOINT, DEFAULT_SSE_ENDPOINT, port);
6276
}
6377

64-
public static void startSyncSseServer(String name, String version, String messageEndpoint, String sseEndpoint, int port) {
78+
public void startSyncSseServer(String name, String version, String messageEndpoint, String sseEndpoint, int port) {
6579
HttpServletSseServerTransportProvider transport = new HttpServletSseServerTransportProvider(
6680
OBJECT_MAPPER, messageEndpoint, sseEndpoint
6781
);
@@ -77,7 +91,7 @@ public static void startSyncSseServer(String name, String version, String messag
7791
startHttpServer(server, transport, port);
7892
}
7993

80-
private static void startHttpServer(McpSyncServer server, HttpServletSseServerTransportProvider transport, int port) {
94+
private void startHttpServer(McpSyncServer server, HttpServletSseServerTransportProvider transport, int port) {
8195
ServletContextHandler servletContextHandler = new ServletContextHandler(ServletContextHandler.SESSIONS);
8296
servletContextHandler.setContextPath("/");
8397

@@ -109,25 +123,25 @@ private static void startHttpServer(McpSyncServer server, HttpServletSseServerTr
109123
}
110124
}
111125

112-
private static void registerResources(McpSyncServer server) {
113-
Set<Class<?>> resourceClasses = REFLECTIONS.getTypesAnnotatedWith(McpResources.class);
126+
private void registerResources(McpSyncServer server) {
127+
Set<Class<?>> resourceClasses = reflections.getTypesAnnotatedWith(McpResources.class);
114128
for (Class<?> resourceClass : resourceClasses) {
115-
Set<Method> methods = Annotations.getMethodsAnnotatedWith(resourceClass, McpResource.class);
129+
Set<Method> methods = ReflectionHelper.getMethodsAnnotatedWith(resourceClass, McpResource.class);
116130
for (Method method : methods) {
117131
McpResource resourceMethod = method.getAnnotation(McpResource.class);
118132
McpSchema.Resource resource = new McpSchema.Resource(
119133
resourceMethod.uri(),
120-
resourceMethod.name(),
134+
resourceMethod.name().isBlank() ? method.getName() : resourceMethod.name(),
121135
resourceMethod.description(),
122136
resourceMethod.mimeType(),
123137
new McpSchema.Annotations(List.of(resourceMethod.roles()), resourceMethod.priority())
124138
);
125139
server.addResource(new McpServerFeatures.SyncResourceSpecification(resource, (exchange, request) -> {
126140
Object result;
127141
try {
128-
Object resourceObject = resourceClass.getDeclaredConstructor().newInstance();
129-
result = method.invoke(resourceObject);
130-
} catch (Exception e) {
142+
result = ReflectionHelper.invokeMethod(resourceClass, method);
143+
} catch (Throwable e) {
144+
logger.error("Error invoking resource method", e);
131145
result = e + ": " + e.getMessage();
132146
}
133147
McpSchema.ResourceContents contents = new McpSchema.TextResourceContents(
@@ -139,21 +153,22 @@ private static void registerResources(McpSyncServer server) {
139153
}
140154
}
141155

142-
private static void registerTools(McpSyncServer server) {
143-
Set<Class<?>> toolClasses = REFLECTIONS.getTypesAnnotatedWith(McpTools.class);
156+
private void registerTools(McpSyncServer server) {
157+
Set<Class<?>> toolClasses = reflections.getTypesAnnotatedWith(McpTools.class);
144158
for (Class<?> toolClass : toolClasses) {
145-
Set<Method> methods = Annotations.getMethodsAnnotatedWith(toolClass, McpTool.class);
159+
Set<Method> methods = ReflectionHelper.getMethodsAnnotatedWith(toolClass, McpTool.class);
146160
for (Method method : methods) {
147161
McpTool toolMethod = method.getAnnotation(McpTool.class);
148162
McpSchema.JsonSchema paramSchema = createJsonSchema(method);
149-
McpSchema.Tool tool = new McpSchema.Tool(toolMethod.name(), toolMethod.description(), paramSchema);
163+
final String toolName = toolMethod.name().isBlank() ? method.getName() : toolMethod.name();
164+
McpSchema.Tool tool = new McpSchema.Tool(toolName, toolMethod.description(), paramSchema);
150165
server.addTool(new McpServerFeatures.SyncToolSpecification(tool, (exchange, params) -> {
151166
Object result;
152167
boolean isError = false;
153168
try {
154-
Object toolObject = toolClass.getDeclaredConstructor().newInstance();
155-
result = method.invoke(toolObject, params.values());
156-
} catch (Exception e) {
169+
result = ReflectionHelper.invokeMethod(toolClass, method, paramSchema, params);
170+
} catch (Throwable e) {
171+
logger.error("Error invoking tool method", e);
157172
result = e + ": " + e.getMessage();
158173
isError = true;
159174
}
@@ -164,15 +179,15 @@ private static void registerTools(McpSyncServer server) {
164179
}
165180
}
166181

167-
private static McpSchema.JsonSchema createJsonSchema(Method method) {
182+
private McpSchema.JsonSchema createJsonSchema(Method method) {
168183
Map<String, Object> properties = new HashMap<>();
169184
List<String> required = new ArrayList<>();
170185

171-
Set<Parameter> parameters = Annotations.getParametersAnnotatedWith(method, McpToolParam.class);
186+
Set<Parameter> parameters = ReflectionHelper.getParametersAnnotatedWith(method, McpToolParam.class);
172187
for (Parameter parameter : parameters) {
173-
final String parameterName = parameter.getName();
174-
final String parameterType = parameter.getType().getName().toLowerCase();
175188
McpToolParam toolParam = parameter.getAnnotation(McpToolParam.class);
189+
final String parameterName = toolParam.name();
190+
final String parameterType = parameter.getType().getName().toLowerCase();
176191

177192
Map<String, String> parameterProperties = Map.of(
178193
"type", parameterType,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.github.codeboyzhou.mcp.declarative.annotation;
2+
3+
import java.lang.annotation.ElementType;
4+
import java.lang.annotation.Retention;
5+
import java.lang.annotation.RetentionPolicy;
6+
import java.lang.annotation.Target;
7+
8+
@Target(ElementType.TYPE)
9+
@Retention(RetentionPolicy.RUNTIME)
10+
public @interface McpComponentScan {
11+
String basePackage() default "";
12+
13+
Class<?> basePackageClass() default Object.class;
14+
}

src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpResource.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@
1212
public @interface McpResource {
1313
String uri();
1414

15-
String name();
15+
String name() default "";
1616

1717
String description();
1818

19-
String mimeType();
19+
String mimeType() default "text/plain";
2020

2121
McpSchema.Role[] roles() default {McpSchema.Role.ASSISTANT, McpSchema.Role.USER};
2222

src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpTool.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
@Retention(RetentionPolicy.RUNTIME)
1010
public @interface McpTool {
1111

12-
String name();
12+
String name() default "";
1313

1414
String description();
1515
}

src/main/java/com/github/codeboyzhou/mcp/declarative/annotation/McpToolParam.java

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
@Target(ElementType.PARAMETER)
99
@Retention(RetentionPolicy.RUNTIME)
1010
public @interface McpToolParam {
11+
String name();
1112

1213
String description();
1314

0 commit comments

Comments
 (0)