|
|
@@ -40,6 +40,8 @@ public class McpServer extends NanoHTTPD {
|
|
|
this.pluginManager = pluginManager;
|
|
|
}
|
|
|
|
|
|
+ private String sessionId = java.util.UUID.randomUUID().toString();
|
|
|
+
|
|
|
// ── HTTP routing ──────────────────────────────────────────────
|
|
|
|
|
|
@Override
|
|
|
@@ -50,14 +52,31 @@ public class McpServer extends NanoHTTPD {
|
|
|
// CORS preflight
|
|
|
if ("OPTIONS".equals(method)) {
|
|
|
Response r = newFixedLengthResponse(Response.Status.OK, "text/plain", "");
|
|
|
- addCors(r);
|
|
|
+ addCommonHeaders(r);
|
|
|
return r;
|
|
|
}
|
|
|
|
|
|
- // MCP JSON-RPC endpoint
|
|
|
- if ("POST".equals(method) && ("/".equals(uri) || "/mcp".equals(uri))) {
|
|
|
+ // MCP Streamable HTTP: POST JSON-RPC
|
|
|
+ if ("POST".equals(method) && ("/".equals(uri) || "/mcp".equals(uri) || uri.startsWith("/mcp"))) {
|
|
|
Response r = handleJsonRpc(session);
|
|
|
- addCors(r);
|
|
|
+ addCommonHeaders(r);
|
|
|
+ r.addHeader("Mcp-Session-Id", sessionId);
|
|
|
+ return r;
|
|
|
+ }
|
|
|
+
|
|
|
+ // MCP SSE endpoint (for server→client notifications)
|
|
|
+ if ("GET".equals(method) && "/mcp".equals(uri)) {
|
|
|
+ String accept = session.getHeaders().getOrDefault("accept", "").toLowerCase();
|
|
|
+ if (accept.contains("text/event-stream")) {
|
|
|
+ return handleSse();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // DELETE /mcp terminates session
|
|
|
+ if ("DELETE".equals(method) && ("/mcp".equals(uri))) {
|
|
|
+ sessionId = java.util.UUID.randomUUID().toString();
|
|
|
+ Response r = newFixedLengthResponse(Response.Status.OK, "application/json", "{}");
|
|
|
+ addCommonHeaders(r);
|
|
|
return r;
|
|
|
}
|
|
|
|
|
|
@@ -65,17 +84,25 @@ public class McpServer extends NanoHTTPD {
|
|
|
if ("GET".equals(method) && "/health".equals(uri)) {
|
|
|
Response r = newFixedLengthResponse(Response.Status.OK, "application/json",
|
|
|
"{\"status\":\"ok\",\"initialized\":" + initialized + "}");
|
|
|
- addCors(r);
|
|
|
+ addCommonHeaders(r);
|
|
|
return r;
|
|
|
}
|
|
|
|
|
|
return newFixedLengthResponse(Response.Status.NOT_FOUND, "text/plain", "Not Found");
|
|
|
}
|
|
|
|
|
|
- private void addCors(Response r) {
|
|
|
+ private void addCommonHeaders(Response r) {
|
|
|
r.addHeader("Access-Control-Allow-Origin", "*");
|
|
|
- r.addHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
|
|
|
- r.addHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
|
+ r.addHeader("Access-Control-Allow-Methods", "POST, GET, DELETE, OPTIONS");
|
|
|
+ r.addHeader("Access-Control-Allow-Headers", "Content-Type, Accept, Mcp-Session-Id");
|
|
|
+ r.addHeader("Access-Control-Expose-Headers", "Mcp-Session-Id");
|
|
|
+ }
|
|
|
+
|
|
|
+ /** SSE 端点:发送 endpoint 事件并保持连接 */
|
|
|
+ private Response handleSse() {
|
|
|
+ return newFixedLengthResponse(Response.Status.OK, "text/event-stream",
|
|
|
+ "event: endpoint\r\n" +
|
|
|
+ "data: /mcp?sessionId=" + sessionId + "\r\n\r\n");
|
|
|
}
|
|
|
|
|
|
// ── JSON-RPC dispatch ─────────────────────────────────────────
|