Skip to content

Commit 74126b2

Browse files
author
AgentScope Developer
committed
feat(mcp): Add native MCP server implementation with calculator tool example and docs
1 parent c3f302c commit 74126b2

48 files changed

Lines changed: 3364 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

agentscope-core/pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,12 @@
8484
<artifactId>jackson-datatype-jsr310</artifactId>
8585
</dependency>
8686

87+
<!-- Jackson Java 8 Optional support -->
88+
<dependency>
89+
<groupId>com.fasterxml.jackson.datatype</groupId>
90+
<artifactId>jackson-datatype-jdk8</artifactId>
91+
</dependency>
92+
8793
<!-- SLF4J API -->
8894
<dependency>
8995
<groupId>org.slf4j</groupId>
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package io.agentscope.core.mcp.handler;
2+
3+
import com.fasterxml.jackson.databind.ObjectMapper;
4+
import io.agentscope.core.mcp.message.JsonRpcError;
5+
import io.agentscope.core.mcp.message.JsonRpcMessage;
6+
import io.agentscope.core.mcp.message.JsonRpcNotification;
7+
import io.agentscope.core.mcp.message.JsonRpcRequest;
8+
import io.agentscope.core.mcp.message.JsonRpcResponse;
9+
import io.agentscope.core.mcp.transport.TransportException;
10+
11+
/**
12+
* Abstract base class for method handlers.
13+
*
14+
* <p>Provides common logic for handling requests and notifications.
15+
*/
16+
public abstract class AbstractMethodHandler implements MethodHandler {
17+
18+
protected ObjectMapper objectMapper = new ObjectMapper();
19+
20+
@Override
21+
public JsonRpcResponse handleMessage(JsonRpcMessage message) throws TransportException {
22+
try {
23+
if (message instanceof JsonRpcRequest) {
24+
JsonRpcRequest request = (JsonRpcRequest) message;
25+
Object result = handle(request.getParams());
26+
return new JsonRpcResponse(request.getId().orElse(null), result);
27+
} else if (message instanceof JsonRpcNotification) {
28+
JsonRpcNotification notification = (JsonRpcNotification) message;
29+
handle(notification.getParams());
30+
return null; // Notifications don't require responses
31+
}
32+
throw new TransportException("Unknown message type: " + message.getClass());
33+
} catch (Exception e) {
34+
if (message instanceof JsonRpcRequest) {
35+
JsonRpcRequest request = (JsonRpcRequest) message;
36+
JsonRpcError error =
37+
new JsonRpcError(
38+
JsonRpcError.ErrorCode.INTERNAL_ERROR,
39+
"Internal server error: " + e.getMessage(),
40+
e.toString());
41+
return new JsonRpcResponse(request.getId().orElse(null), error);
42+
}
43+
throw new TransportException("Error handling notification", e);
44+
}
45+
}
46+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package io.agentscope.core.mcp.handler;
2+
3+
import io.agentscope.core.mcp.schema.CallToolResult;
4+
import io.agentscope.core.mcp.tool.Tool;
5+
import io.agentscope.core.mcp.tool.ToolManager;
6+
import java.util.ArrayList;
7+
import java.util.HashMap;
8+
import java.util.List;
9+
import java.util.Map;
10+
import java.util.Optional;
11+
12+
/**
13+
* Handler for `tools/call` requests. Looks up a registered server-side Tool and executes it.
14+
*/
15+
public class CallToolHandler extends AbstractMethodHandler {
16+
17+
private final ToolManager toolManager;
18+
19+
public CallToolHandler(ToolManager toolManager) {
20+
this.toolManager = toolManager;
21+
}
22+
23+
@Override
24+
public String getMethod() {
25+
return "tools/call";
26+
}
27+
28+
@SuppressWarnings("unchecked")
29+
@Override
30+
public Object handle(Object params) throws Exception {
31+
if (!(params instanceof Map)) {
32+
throw new IllegalArgumentException("Invalid params for tools/call");
33+
}
34+
Map<String, Object> map = (Map<String, Object>) params;
35+
String name = (String) map.get("name");
36+
Object arguments = map.get("arguments");
37+
38+
if (name == null || name.isBlank()) {
39+
throw new IllegalArgumentException("Tool name is required");
40+
}
41+
42+
Optional<Tool> toolOpt = toolManager.get(name);
43+
if (toolOpt.isEmpty()) {
44+
throw new IllegalArgumentException("Tool not found: " + name);
45+
}
46+
47+
Tool tool = toolOpt.get();
48+
Object result = tool.execute(arguments);
49+
50+
// Normalize result into a content block list. Always wrap in a text block.
51+
Map<String, Object> block = new HashMap<>();
52+
block.put("type", "text");
53+
String text;
54+
if (result instanceof String) {
55+
text = (String) result;
56+
} else {
57+
text = objectMapper.writeValueAsString(result == null ? "" : result);
58+
}
59+
block.put("text", text);
60+
List<Object> content = new ArrayList<>();
61+
content.add(block);
62+
63+
return new CallToolResult(content, Optional.empty());
64+
}
65+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package io.agentscope.core.mcp.handler;
2+
3+
import java.util.Map;
4+
import java.util.Optional;
5+
import java.util.concurrent.ConcurrentHashMap;
6+
7+
/**
8+
* Registry for MCP method handlers.
9+
*
10+
* <p>Stores and retrieves handlers by method name.
11+
*/
12+
public class HandlerRegistry {
13+
14+
private final Map<String, MethodHandler> handlers = new ConcurrentHashMap<>();
15+
16+
/**
17+
* Register a handler for a method.
18+
*
19+
* @param method the method name
20+
* @param handler the handler
21+
*/
22+
public void register(String method, MethodHandler handler) {
23+
handlers.put(method, handler);
24+
}
25+
26+
/**
27+
* Get a handler for a method.
28+
*
29+
* @param method the method name
30+
* @return the handler, or empty if not found
31+
*/
32+
public Optional<MethodHandler> get(String method) {
33+
return Optional.ofNullable(handlers.get(method));
34+
}
35+
36+
/**
37+
* Check if a handler is registered for a method.
38+
*
39+
* @param method the method name
40+
* @return true if handler exists
41+
*/
42+
public boolean has(String method) {
43+
return handlers.containsKey(method);
44+
}
45+
46+
/**
47+
* Unregister a handler for a method.
48+
*
49+
* @param method the method name
50+
*/
51+
public void unregister(String method) {
52+
handlers.remove(method);
53+
}
54+
55+
/**
56+
* Get all registered method names.
57+
*
58+
* @return set of method names
59+
*/
60+
public Map<String, MethodHandler> getAll() {
61+
return new ConcurrentHashMap<>(handlers);
62+
}
63+
64+
/**
65+
* Clear all handlers.
66+
*/
67+
public void clear() {
68+
handlers.clear();
69+
}
70+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package io.agentscope.core.mcp.handler;
2+
3+
import io.agentscope.core.mcp.schema.InitializeResult;
4+
import io.agentscope.core.mcp.tool.ToolManager;
5+
import java.util.HashMap;
6+
import java.util.Map;
7+
8+
/**
9+
* Handler for `initialize` requests (handshake).
10+
*/
11+
public class InitializeHandler extends AbstractMethodHandler {
12+
13+
private final ToolManager toolManager;
14+
15+
public InitializeHandler(ToolManager toolManager) {
16+
this.toolManager = toolManager;
17+
}
18+
19+
@Override
20+
public String getMethod() {
21+
return "initialize";
22+
}
23+
24+
@Override
25+
public Object handle(Object params) throws Exception {
26+
Map<String, Object> capabilities = new HashMap<>();
27+
capabilities.put("tools", new HashMap<>());
28+
capabilities.put("protocol", "mcp");
29+
30+
Map<String, Object> serverInfo = new HashMap<>();
31+
serverInfo.put("name", "agentscope-core");
32+
serverInfo.put("version", "0.1.0");
33+
34+
return new InitializeResult(capabilities, "2.0", serverInfo);
35+
}
36+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package io.agentscope.core.mcp.handler;
2+
3+
import io.agentscope.core.mcp.schema.ListToolsResult;
4+
import io.agentscope.core.mcp.tool.Tool;
5+
import io.agentscope.core.mcp.tool.ToolManager;
6+
import java.util.ArrayList;
7+
import java.util.HashMap;
8+
import java.util.List;
9+
import java.util.Map;
10+
import java.util.Optional;
11+
12+
/**
13+
* Handler for `tools/list` requests. Returns metadata about registered tools.
14+
*/
15+
public class ListToolsHandler extends AbstractMethodHandler {
16+
17+
private final ToolManager toolManager;
18+
19+
public ListToolsHandler(ToolManager toolManager) {
20+
this.toolManager = toolManager;
21+
}
22+
23+
@Override
24+
public String getMethod() {
25+
return "tools/list";
26+
}
27+
28+
@Override
29+
public Object handle(Object params) throws Exception {
30+
List<Object> out = new ArrayList<>();
31+
for (Tool t : toolManager.list()) {
32+
Map<String, Object> m = new HashMap<>();
33+
m.put("name", t.getName());
34+
m.put("description", t.getDescription());
35+
m.put("inputSchema", t.getInputSchema());
36+
out.add(m);
37+
}
38+
39+
return new ListToolsResult(Optional.empty(), out);
40+
}
41+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package io.agentscope.core.mcp.handler;
2+
3+
import io.agentscope.core.mcp.message.JsonRpcError;
4+
import io.agentscope.core.mcp.message.JsonRpcMessage;
5+
import io.agentscope.core.mcp.message.JsonRpcRequest;
6+
import io.agentscope.core.mcp.message.JsonRpcResponse;
7+
import io.agentscope.core.mcp.transport.Transport;
8+
import io.agentscope.core.mcp.transport.TransportException;
9+
import java.util.Optional;
10+
import java.util.logging.Logger;
11+
12+
/**
13+
* Routes incoming messages to appropriate handlers.
14+
*
15+
* <p>Manages request/response correlation and error handling for MCP protocol messages.
16+
*/
17+
public class MessageRouter {
18+
19+
private static final Logger logger = Logger.getLogger(MessageRouter.class.getName());
20+
21+
private final Transport transport;
22+
private final HandlerRegistry handlerRegistry;
23+
24+
public MessageRouter(Transport transport, HandlerRegistry handlerRegistry) {
25+
this.transport = transport;
26+
this.handlerRegistry = handlerRegistry;
27+
}
28+
29+
/**
30+
* Process an incoming message.
31+
*
32+
* @param message the incoming message
33+
* @throws TransportException if processing fails
34+
*/
35+
public void handleMessage(JsonRpcMessage message) throws TransportException {
36+
String method = message.getMethod().orElse(null);
37+
38+
if (method == null) {
39+
logger.warning("Received message without method");
40+
return;
41+
}
42+
43+
Optional<MethodHandler> handler = handlerRegistry.get(method);
44+
if (!handler.isPresent()) {
45+
handleUnknownMethod(message, method);
46+
return;
47+
}
48+
49+
JsonRpcResponse response = handler.get().handleMessage(message);
50+
if (response != null && message instanceof JsonRpcRequest) {
51+
// Only send response for requests, not notifications
52+
transport.send(response);
53+
}
54+
}
55+
56+
/**
57+
* Start processing messages from the transport.
58+
*
59+
* <p>This method runs indefinitely until the transport is closed or an error occurs.
60+
*
61+
* @throws TransportException if an error occurs
62+
*/
63+
public void processMessages() throws TransportException {
64+
while (transport.isConnected()) {
65+
try {
66+
JsonRpcMessage message = transport.receive();
67+
handleMessage(message);
68+
} catch (TransportException e) {
69+
if (transport.isConnected()) {
70+
logger.warning("Error processing message: " + e.getMessage());
71+
}
72+
}
73+
}
74+
}
75+
76+
private void handleUnknownMethod(JsonRpcMessage message, String method)
77+
throws TransportException {
78+
if (message instanceof JsonRpcRequest) {
79+
JsonRpcRequest request = (JsonRpcRequest) message;
80+
JsonRpcError error =
81+
new JsonRpcError(
82+
JsonRpcError.ErrorCode.METHOD_NOT_FOUND, "Method not found: " + method);
83+
JsonRpcResponse response = new JsonRpcResponse(request.getId().orElse(null), error);
84+
transport.send(response);
85+
}
86+
}
87+
88+
/**
89+
* Register a method handler.
90+
*
91+
* @param method the method name
92+
* @param handler the handler
93+
*/
94+
public void register(String method, MethodHandler handler) {
95+
handlerRegistry.register(method, handler);
96+
}
97+
}

0 commit comments

Comments
 (0)