Initial library commit
This commit is contained in:
141
pom.xml
Normal file
141
pom.xml
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||||
|
http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<groupId>io.github.mcp-java</groupId>
|
||||||
|
<artifactId>mcp-server-lib</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
|
<name>MCP Server Library</name>
|
||||||
|
<description>A library for building MCP servers in Java using Servlet containers</description>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.source>21</maven.compiler.source>
|
||||||
|
<maven.compiler.target>21</maven.compiler.target>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.modelcontextprotocol.sdk</groupId>
|
||||||
|
<artifactId>mcp</artifactId>
|
||||||
|
<version>0.12.1</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>jackson-databind</artifactId>
|
||||||
|
<version>2.15.2</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Eclipse Jetty for SSE Servlets (lightweight) -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.eclipse.jetty</groupId>
|
||||||
|
<artifactId>jetty-server</artifactId>
|
||||||
|
<version>11.0.20</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.eclipse.jetty</groupId>
|
||||||
|
<artifactId>jetty-servlet</artifactId>
|
||||||
|
<version>11.0.20</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- JSON processing -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.google.code.gson</groupId>
|
||||||
|
<artifactId>gson</artifactId>
|
||||||
|
<version>2.10.1</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Logging API -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.slf4j</groupId>
|
||||||
|
<artifactId>slf4j-api</artifactId>
|
||||||
|
<version>2.0.9</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Reflections library for classpath scanning -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.reflections</groupId>
|
||||||
|
<artifactId>reflections</artifactId>
|
||||||
|
<version>0.10.2</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Testing -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter-api</artifactId>
|
||||||
|
<version>5.10.0</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter-engine</artifactId>
|
||||||
|
<version>5.10.0</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.mockito</groupId>
|
||||||
|
<artifactId>mockito-core</artifactId>
|
||||||
|
<version>5.5.0</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.mockito</groupId>
|
||||||
|
<artifactId>mockito-junit-jupiter</artifactId>
|
||||||
|
<version>5.5.0</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.slf4j</groupId>
|
||||||
|
<artifactId>slf4j-simple</artifactId>
|
||||||
|
<version>2.0.9</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<version>3.11.0</version>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-jar-plugin</artifactId>
|
||||||
|
<version>3.3.0</version>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-source-plugin</artifactId>
|
||||||
|
<version>3.3.0</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>attach-sources</id>
|
||||||
|
<goals>
|
||||||
|
<goal>jar-no-fork</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-javadoc-plugin</artifactId>
|
||||||
|
<version>3.5.0</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>attach-javadocs</id>
|
||||||
|
<goals>
|
||||||
|
<goal>jar</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
39
src/main/java/mcp/registry/DynamicToolLoader.java
Normal file
39
src/main/java/mcp/registry/DynamicToolLoader.java
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package mcp.registry;
|
||||||
|
|
||||||
|
import mcp.tools.McpTool;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.net.URLClassLoader;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility for dynamically loading {@link McpTool} implementations from JAR files.
|
||||||
|
*/
|
||||||
|
public class DynamicToolLoader {
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(DynamicToolLoader.class);
|
||||||
|
|
||||||
|
public static McpTool loadTool(String jarPath, String className) throws Exception {
|
||||||
|
logger.debug("Loading tool {} from jar {}", className, jarPath);
|
||||||
|
File jarFile = new File(jarPath);
|
||||||
|
if (!jarFile.exists()) {
|
||||||
|
logger.error("JAR file not found: {}", jarPath);
|
||||||
|
throw new IllegalArgumentException("JAR file not found: " + jarPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
URL jarUrl = jarFile.toURI().toURL();
|
||||||
|
logger.trace("Creating URLClassLoader with jar URL: {}", jarUrl);
|
||||||
|
URLClassLoader classLoader = new URLClassLoader(new URL[]{jarUrl}, McpTool.class.getClassLoader());
|
||||||
|
|
||||||
|
logger.trace("Loading class {}", className);
|
||||||
|
Class<?> toolClass = Class.forName(className, true, classLoader);
|
||||||
|
if (!McpTool.class.isAssignableFrom(toolClass)) {
|
||||||
|
logger.error("Class {} does not implement McpTool", className);
|
||||||
|
throw new IllegalArgumentException("Class " + className + " does not implement McpTool");
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug("Successfully loaded tool class {}", className);
|
||||||
|
return (McpTool) toolClass.getDeclaredConstructor().newInstance();
|
||||||
|
}
|
||||||
|
}
|
||||||
123
src/main/java/mcp/registry/ToolRegistry.java
Normal file
123
src/main/java/mcp/registry/ToolRegistry.java
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
package mcp.registry;
|
||||||
|
|
||||||
|
import io.modelcontextprotocol.server.McpStatelessServerFeatures;
|
||||||
|
import io.modelcontextprotocol.server.McpStatelessSyncServer;
|
||||||
|
import io.modelcontextprotocol.spec.McpSchema;
|
||||||
|
import mcp.tools.DefaultMcpTool;
|
||||||
|
import mcp.tools.McpTool;
|
||||||
|
import org.reflections.Reflections;
|
||||||
|
import org.reflections.scanners.Scanners;
|
||||||
|
import org.reflections.util.ClasspathHelper;
|
||||||
|
import org.reflections.util.ConfigurationBuilder;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.net.URL;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
public class ToolRegistry {
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(ToolRegistry.class);
|
||||||
|
private final Map<String, McpTool> tools = new ConcurrentHashMap<>();
|
||||||
|
private McpStatelessSyncServer server;
|
||||||
|
private final Set<String> classpaths;
|
||||||
|
|
||||||
|
@Deprecated()
|
||||||
|
public ToolRegistry() {
|
||||||
|
this(Set.of("mcp.tools"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ToolRegistry(Set<String> classpaths) {
|
||||||
|
this.classpaths = classpaths;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a tool in the registry. If a server is already active, the tool
|
||||||
|
* is immediately added to it.
|
||||||
|
*
|
||||||
|
* @param tool The tool to register.
|
||||||
|
*/
|
||||||
|
public void register(McpTool tool) {
|
||||||
|
logger.debug("Registering tool: {}", tool.name());
|
||||||
|
tools.put(tool.name(), tool);
|
||||||
|
if (server != null) {
|
||||||
|
logger.debug("Adding tool {} to active server", tool.name());
|
||||||
|
addToolToServer(server, tool);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public McpTool get(String name) {
|
||||||
|
return tools.get(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void applyTo(McpStatelessSyncServer server) {
|
||||||
|
this.server = server;
|
||||||
|
logger.debug("Applying {} registered tools to server", tools.size());
|
||||||
|
for (McpTool tool : tools.values()) {
|
||||||
|
addToolToServer(server, tool);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addToolToServer(McpStatelessSyncServer server, McpTool tool) {
|
||||||
|
logger.trace("Adding tool {} to server with schema: {}", tool.name(), tool.schema());
|
||||||
|
server.addTool(new McpStatelessServerFeatures.SyncToolSpecification(
|
||||||
|
new McpSchema.Tool(tool.name(), tool.description(), tool.schema()),
|
||||||
|
(exchange, request) -> {
|
||||||
|
logger.debug("Tool call: {} with arguments: {}", tool.name(), request.arguments());
|
||||||
|
return tool.call(request, request.arguments());
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scans the configured classpaths for tools annotated with {@link DefaultMcpTool}
|
||||||
|
* and registers them if they are enabled.
|
||||||
|
*/
|
||||||
|
public void autoconfigure() {
|
||||||
|
logger.info("Starting tool autoconfiguration with classpaths: {}", classpaths);
|
||||||
|
ConfigurationBuilder builder = new ConfigurationBuilder()
|
||||||
|
.addScanners(Scanners.TypesAnnotated);
|
||||||
|
|
||||||
|
if (classpaths != null && !classpaths.isEmpty()) {
|
||||||
|
builder.forPackages(classpaths.toArray(new String[0]));
|
||||||
|
for (String path : classpaths) {
|
||||||
|
logger.debug("Scanning classpath path: {}", path);
|
||||||
|
// For package names
|
||||||
|
builder.addUrls(ClasspathHelper.forPackage(path));
|
||||||
|
// For potential jar paths or other URLs if the string is a URL
|
||||||
|
try {
|
||||||
|
if (path.startsWith("file:") || path.startsWith("http:") || path.startsWith("https:")) {
|
||||||
|
builder.addUrls(new URL(path));
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// ignore if not a valid URL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Reflections reflections = new Reflections(builder);
|
||||||
|
|
||||||
|
Set<Class<?>> annotatedClasses = reflections.getTypesAnnotatedWith(DefaultMcpTool.class);
|
||||||
|
logger.debug("Found {} classes annotated with @DefaultMcpTool", annotatedClasses.size());
|
||||||
|
|
||||||
|
for (Class<?> clazz : annotatedClasses) {
|
||||||
|
if (McpTool.class.isAssignableFrom(clazz)) {
|
||||||
|
DefaultMcpTool annotation = clazz.getAnnotation(DefaultMcpTool.class);
|
||||||
|
if (annotation.enabled()) {
|
||||||
|
try {
|
||||||
|
logger.debug("Instantiating tool class: {}", clazz.getName());
|
||||||
|
McpTool tool = (McpTool) clazz.getDeclaredConstructor().newInstance();
|
||||||
|
register(tool);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Failed to instantiate tool: " + clazz.getName(), e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.debug("Tool class {} is disabled via annotation", clazz.getName());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.warn("Class {} is annotated with @DefaultMcpTool but does not implement McpTool", clazz.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
123
src/main/java/mcp/server/McpServlet.java
Normal file
123
src/main/java/mcp/server/McpServlet.java
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
package mcp.server;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import io.modelcontextprotocol.server.McpServer;
|
||||||
|
import io.modelcontextprotocol.server.McpStatelessSyncServer;
|
||||||
|
import io.modelcontextprotocol.server.transport.HttpServletStatelessServerTransport;
|
||||||
|
import io.modelcontextprotocol.spec.McpSchema;
|
||||||
|
import jakarta.servlet.ServletConfig;
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServlet;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import mcp.registry.ToolRegistry;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Servlet that hosts an MCP server.
|
||||||
|
* <p>
|
||||||
|
* This servlet can be configured via init-parameters in web.xml or programmatically.
|
||||||
|
* Supported init-parameters:
|
||||||
|
* <ul>
|
||||||
|
* <li><code>serverName</code>: The name of the MCP server.</li>
|
||||||
|
* <li><code>serverVersion</code>: The version of the MCP server.</li>
|
||||||
|
* <li><code>classpaths</code>: A comma-separated list of packages/classpaths to scan for tools.</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
public class McpServlet extends HttpServlet {
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(McpServlet.class);
|
||||||
|
|
||||||
|
private HttpServletStatelessServerTransport transport;
|
||||||
|
private ToolRegistry toolRegistry;
|
||||||
|
private McpStatelessSyncServer syncServer;
|
||||||
|
|
||||||
|
private String serverName = "mcp-server";
|
||||||
|
private String serverVersion = "1.0.0";
|
||||||
|
|
||||||
|
public McpServlet() {
|
||||||
|
this(Collections.emptySet());
|
||||||
|
}
|
||||||
|
|
||||||
|
public McpServlet(Set<String> classpaths) {
|
||||||
|
this.toolRegistry = new ToolRegistry(classpaths);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(ServletConfig config) throws ServletException {
|
||||||
|
super.init(config);
|
||||||
|
logger.info("Initializing McpServlet...");
|
||||||
|
|
||||||
|
String nameParam = config.getInitParameter("serverName");
|
||||||
|
if (nameParam != null) {
|
||||||
|
this.serverName = nameParam;
|
||||||
|
logger.debug("Server name set from init param: {}", serverName);
|
||||||
|
}
|
||||||
|
|
||||||
|
String versionParam = config.getInitParameter("serverVersion");
|
||||||
|
if (versionParam != null) {
|
||||||
|
this.serverVersion = versionParam;
|
||||||
|
logger.debug("Server version set from init param: {}", serverVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
String classpathsParam = config.getInitParameter("classpaths");
|
||||||
|
if (classpathsParam != null && !classpathsParam.isEmpty()) {
|
||||||
|
Set<String> classpaths = Stream.of(classpathsParam.split(","))
|
||||||
|
.map(String::trim)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
this.toolRegistry = new ToolRegistry(classpaths);
|
||||||
|
logger.debug("ToolRegistry re-initialized with classpaths: {}", classpaths);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug("Starting tool autoconfiguration...");
|
||||||
|
this.toolRegistry.autoconfigure();
|
||||||
|
|
||||||
|
this.transport = HttpServletStatelessServerTransport.builder()
|
||||||
|
.objectMapper(new ObjectMapper())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
logger.debug("Creating McpStatelessSyncServer with info: {} version: {}", serverName, serverVersion);
|
||||||
|
this.syncServer = McpServer.sync(transport)
|
||||||
|
.serverInfo(serverName, serverVersion)
|
||||||
|
.capabilities(McpSchema.ServerCapabilities.builder()
|
||||||
|
.resources(false, true)
|
||||||
|
.tools(true)
|
||||||
|
.prompts(true)
|
||||||
|
.logging()
|
||||||
|
.completions()
|
||||||
|
.build())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
logger.debug("Applying tools from registry to server");
|
||||||
|
this.toolRegistry.applyTo(syncServer);
|
||||||
|
logger.info("McpServlet initialized successfully");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
|
||||||
|
logger.debug("McpServlet received request: {} {} (ServletPath: {}, PathInfo: {})",
|
||||||
|
req.getMethod(), req.getRequestURI(), req.getServletPath(), req.getPathInfo());
|
||||||
|
transport.service(req, resp);
|
||||||
|
if (resp.getStatus() == 404) {
|
||||||
|
logger.warn("Transport returned 404 for request: {} {}", req.getMethod(), req.getRequestURI());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ToolRegistry getToolRegistry() {
|
||||||
|
return toolRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setServerName(String serverName) {
|
||||||
|
this.serverName = serverName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setServerVersion(String serverVersion) {
|
||||||
|
this.serverVersion = serverVersion;
|
||||||
|
}
|
||||||
|
}
|
||||||
87
src/main/java/mcp/server/ToolRegistrationServlet.java
Normal file
87
src/main/java/mcp/server/ToolRegistrationServlet.java
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
package mcp.server;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import jakarta.servlet.http.HttpServlet;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import mcp.registry.DynamicToolLoader;
|
||||||
|
import mcp.registry.ToolRegistry;
|
||||||
|
import mcp.tools.McpTool;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import mcp.util.Err;
|
||||||
|
import mcp.util.Ok;
|
||||||
|
import mcp.util.Result;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Servlet that provides an endpoint for dynamic registration of external tools.
|
||||||
|
* <p>
|
||||||
|
* Expects a POST request with a JSON body:
|
||||||
|
* <pre>
|
||||||
|
* {
|
||||||
|
* "jarPath": "/path/to/tools.jar",
|
||||||
|
* "className": "com.example.MyTool"
|
||||||
|
* }
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
public class ToolRegistrationServlet extends HttpServlet {
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(ToolRegistrationServlet.class);
|
||||||
|
|
||||||
|
private final ToolRegistry toolRegistry;
|
||||||
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
|
public ToolRegistrationServlet(ToolRegistry toolRegistry) {
|
||||||
|
this.toolRegistry = toolRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
|
||||||
|
logger.debug("Received tool registration request");
|
||||||
|
try {
|
||||||
|
Map<String, String> body = objectMapper.readValue(req.getInputStream(), Map.class);
|
||||||
|
String jarPath = body.get("jarPath");
|
||||||
|
String className = body.get("className");
|
||||||
|
|
||||||
|
if (jarPath == null || className == null) {
|
||||||
|
logger.warn("Missing jarPath or className in registration request");
|
||||||
|
resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
|
||||||
|
resp.getWriter().write("Missing jarPath or className");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Attempting to register tool: {} from {}", className, jarPath);
|
||||||
|
final Result<McpTool, Exception> result = registerTool(jarPath, className);
|
||||||
|
switch (result) {
|
||||||
|
case Ok<McpTool, Exception> ok -> {
|
||||||
|
final McpTool tool = ok.value();
|
||||||
|
logger.info("Tool registered successfully: {}", tool.name());
|
||||||
|
resp.setStatus(HttpServletResponse.SC_OK);
|
||||||
|
resp.getWriter().write("Tool registered successfully: " + tool.name());
|
||||||
|
}
|
||||||
|
case Err<McpTool, Exception> err -> {
|
||||||
|
logger.error("Error registering tool: {}", err.throwable().getMessage());
|
||||||
|
resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
|
||||||
|
resp.getWriter().write("Error registering tool: " + err.throwable().getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Unexpected error during tool registration", e);
|
||||||
|
resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
|
||||||
|
resp.getWriter().write("Error registering tool: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Result<McpTool, Exception> registerTool(final String jarPath, final String className) {
|
||||||
|
try {
|
||||||
|
McpTool tool = DynamicToolLoader.loadTool(jarPath, className);
|
||||||
|
toolRegistry.register(tool);
|
||||||
|
return new Ok<>(tool);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return new Err<>(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/main/java/mcp/tools/DefaultMcpTool.java
Normal file
20
src/main/java/mcp/tools/DefaultMcpTool.java
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package mcp.tools;
|
||||||
|
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Annotation to mark an {@link McpTool} for automatic discovery and registration.
|
||||||
|
*/
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Target(ElementType.TYPE)
|
||||||
|
public @interface DefaultMcpTool {
|
||||||
|
/**
|
||||||
|
* Whether this tool is enabled for automatic registration.
|
||||||
|
*
|
||||||
|
* @return true if enabled, false otherwise.
|
||||||
|
*/
|
||||||
|
boolean enabled() default true;
|
||||||
|
}
|
||||||
18
src/main/java/mcp/tools/McpTool.java
Normal file
18
src/main/java/mcp/tools/McpTool.java
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package mcp.tools;
|
||||||
|
|
||||||
|
import io.modelcontextprotocol.spec.McpSchema;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for implementing MCP tools.
|
||||||
|
* <p>
|
||||||
|
* Implementations must have a public no-args constructor if they are to be
|
||||||
|
* discovered automatically via {@link DefaultMcpTool}.
|
||||||
|
*/
|
||||||
|
public interface McpTool {
|
||||||
|
String name();
|
||||||
|
String description();
|
||||||
|
String schema();
|
||||||
|
McpSchema.CallToolResult call(McpSchema.CallToolRequest request, Map<String, Object> arguments);
|
||||||
|
}
|
||||||
117
src/test/java/mcp/server/ToolRegistrationServletTest.java
Normal file
117
src/test/java/mcp/server/ToolRegistrationServletTest.java
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
package mcp.server;
|
||||||
|
|
||||||
|
import jakarta.servlet.ReadListener;
|
||||||
|
import jakarta.servlet.ServletInputStream;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import mcp.registry.ToolRegistry;
|
||||||
|
import mcp.tools.McpTool;
|
||||||
|
import mcp.util.Err;
|
||||||
|
import mcp.util.Ok;
|
||||||
|
import mcp.util.Result;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.io.StringWriter;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
public class ToolRegistrationServletTest {
|
||||||
|
|
||||||
|
private ToolRegistry toolRegistry;
|
||||||
|
private HttpServletRequest request;
|
||||||
|
private HttpServletResponse response;
|
||||||
|
private StringWriter responseWriter;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() throws IOException {
|
||||||
|
toolRegistry = new ToolRegistry(java.util.Set.of());
|
||||||
|
request = mock(HttpServletRequest.class);
|
||||||
|
response = mock(HttpServletResponse.class);
|
||||||
|
responseWriter = new StringWriter();
|
||||||
|
when(response.getWriter()).thenReturn(new PrintWriter(responseWriter));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDoPostSuccess() throws Exception {
|
||||||
|
McpTool mockTool = mock(McpTool.class);
|
||||||
|
when(mockTool.name()).thenReturn("EchoTool");
|
||||||
|
|
||||||
|
// Use anonymous subclass instead of spy to avoid instrumentation issues on Java 25
|
||||||
|
ToolRegistrationServlet servlet = new ToolRegistrationServlet(toolRegistry) {
|
||||||
|
@Override
|
||||||
|
public Result<McpTool, Exception> registerTool(String jarPath, String className) {
|
||||||
|
toolRegistry.register(mockTool);
|
||||||
|
return new Ok<McpTool, Exception>(mockTool);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
String json = "{\"jarPath\": \"/tmp/test.jar\", \"className\": \"com.example.EchoTool\"}";
|
||||||
|
mockRequestInput(json);
|
||||||
|
|
||||||
|
servlet.doPost(request, response);
|
||||||
|
|
||||||
|
verify(response).setStatus(HttpServletResponse.SC_OK);
|
||||||
|
assertTrue(responseWriter.toString().contains("Tool registered successfully: EchoTool"));
|
||||||
|
assertTrue(toolRegistry.get("EchoTool") != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDoPostMissingParams() throws Exception {
|
||||||
|
ToolRegistrationServlet servlet = new ToolRegistrationServlet(toolRegistry);
|
||||||
|
String json = "{\"jarPath\": \"/tmp/test.jar\"}"; // missing className
|
||||||
|
mockRequestInput(json);
|
||||||
|
|
||||||
|
servlet.doPost(request, response);
|
||||||
|
|
||||||
|
verify(response).setStatus(HttpServletResponse.SC_BAD_REQUEST);
|
||||||
|
assertTrue(responseWriter.toString().contains("Missing jarPath or className"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDoPostRegistrationError() throws Exception {
|
||||||
|
ToolRegistrationServlet servlet = new ToolRegistrationServlet(toolRegistry) {
|
||||||
|
@Override
|
||||||
|
public Result<McpTool, Exception> registerTool(String jarPath, String className) {
|
||||||
|
return new Err<McpTool, Exception>(new Exception("Loading failed"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
String json = "{\"jarPath\": \"/tmp/test.jar\", \"className\": \"com.example.FailTool\"}";
|
||||||
|
mockRequestInput(json);
|
||||||
|
|
||||||
|
servlet.doPost(request, response);
|
||||||
|
|
||||||
|
verify(response).setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
|
||||||
|
assertTrue(responseWriter.toString().contains("Error registering tool: Loading failed"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void mockRequestInput(String json) throws IOException {
|
||||||
|
ByteArrayInputStream bais = new ByteArrayInputStream(json.getBytes());
|
||||||
|
ServletInputStream sis = new ServletInputStream() {
|
||||||
|
@Override
|
||||||
|
public int read() throws IOException {
|
||||||
|
return bais.read();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isFinished() {
|
||||||
|
return bais.available() == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isReady() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setReadListener(ReadListener readListener) {
|
||||||
|
}
|
||||||
|
};
|
||||||
|
when(request.getInputStream()).thenReturn(sis);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user