Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
1184db7
Refactor ROADMAP.md for clarity and remove outdated sections
predic8 Apr 22, 2026
cd06f7f
feat: add JSON-RPC request, response, and MCP initialization support
predic8 Apr 23, 2026
2c12663
refactor: streamline MCP request processing and response generation
predic8 Apr 23, 2026
eee7cc9
refactor: remove redundant IOException declaration in toolsList method
predic8 Apr 23, 2026
dfed9b9
Enhance MCP server error handling with detailed response generation
christiangoerdes Apr 27, 2026
f1b99b6
Refactor MCP server to replace MCPResponse with JSONRPCResponse for c…
christiangoerdes Apr 27, 2026
0ccddde
Add support for fetching Membrane runtime statistics in MCP server
christiangoerdes Apr 27, 2026
4d7b3d3
Limit MCP server exchange retrieval to the last 100 entries
christiangoerdes Apr 27, 2026
5de11e4
Refactor JSON-RPC request handling to distinguish between missing and…
christiangoerdes Apr 27, 2026
afaee6d
Add ResponseKind to JSONRPCResponse for explicit result/error distinc…
christiangoerdes Apr 27, 2026
602d186
Improve JSON-RPC validation by rejecting notifications in MCPRequest …
christiangoerdes Apr 27, 2026
d56dc7d
Rename "lastExchanges" capability to "listChanged" in MCP server
christiangoerdes Apr 28, 2026
5ab2c30
Replace `Objects.requireNonNull` with static import `requireNonNull` …
christiangoerdes Apr 28, 2026
f465969
Remove unnecessary `IOException` declarations from MCP server methods…
christiangoerdes Apr 28, 2026
ce5099c
Add response details (status, body, headers) to MCP server exchange s…
christiangoerdes Apr 28, 2026
9a0440f
Reorder case blocks in `MembraneMCPServer` for consistency
christiangoerdes Apr 28, 2026
819ed81
Refactor error response handling in `MembraneMCPServer` to improve cl…
christiangoerdes Apr 29, 2026
7fa6d37
Refactor JSON-RPC utilities to improve validation, normalization, and…
christiangoerdes Apr 29, 2026
22bb792
Add unit tests for `JSONRPCRequest` and `JSONRPCResponse` to validate…
christiangoerdes Apr 29, 2026
fe04d8f
Introduce MCP tool registration and utilities, including `McpToolRegi…
christiangoerdes Apr 30, 2026
8b397b6
Add `McpPayloadSanitizer` and `McpSessionContext` utilities for MCP p…
christiangoerdes Apr 30, 2026
e29eb12
Remove unused imports in `McpPayloadSanitizer` for cleanup
christiangoerdes Apr 30, 2026
6132374
Refactor and streamline MCP server request handling: add tool registr…
christiangoerdes Apr 30, 2026
bc522ff
Refactor `MembraneMCPServer`: improve HTTP 405 response handling and …
christiangoerdes Apr 30, 2026
b953c17
Add unit tests for `MembraneMCPServer` and `MCPInitialize` to validat…
christiangoerdes Apr 30, 2026
ab38255
Refactor `MembraneMCPServer` error handling: introduce `InvalidToolAr…
christiangoerdes Apr 30, 2026
0f0049d
Merge branch 'master' into roadmap
christiangoerdes Apr 30, 2026
32803b3
Update `toString` implementation in `MCPToolsCall` to display argumen…
christiangoerdes May 4, 2026
69e6fb6
Extract utility methods from `MembraneMCPServer` to new `MCPUtil` cla…
christiangoerdes May 4, 2026
a911b22
Introduce session management in `MembraneMCPServer`: add `SESSION_HEA…
christiangoerdes May 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@

public interface ClientStatistics {

public int getCount();
int getCount();

public String getClient();
String getClient();

public long getMinDuration();
long getMinDuration();

public long getMaxDuration();
long getMaxDuration();

public long getAvgDuration();
long getAvgDuration();

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
package com.predic8.membrane.core.interceptor.mcp;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.predic8.membrane.annot.MCElement;
import com.predic8.membrane.core.exchange.AbstractExchange;
import com.predic8.membrane.core.exchange.Exchange;
import com.predic8.membrane.core.http.Response;
import com.predic8.membrane.core.interceptor.AbstractInterceptor;
import com.predic8.membrane.core.interceptor.Outcome;
import com.predic8.membrane.core.jsonrpc.JSONRPCRequest;
import com.predic8.membrane.core.jsonrpc.JSONRPCResponse;
import com.predic8.membrane.core.mcp.*;
import com.predic8.membrane.core.openapi.serviceproxy.APIProxy;
import com.predic8.membrane.core.proxies.Proxy;
import com.predic8.membrane.core.proxies.SOAPProxy;
import com.predic8.membrane.core.proxies.ServiceProxy;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

import static com.predic8.membrane.annot.Constants.VERSION;
import static com.predic8.membrane.core.http.MimeType.APPLICATION_JSON;
import static com.predic8.membrane.core.http.Response.ok;
import static com.predic8.membrane.core.interceptor.Outcome.RETURN;
import static com.predic8.membrane.core.jsonrpc.JSONRPCResponse.*;

/**
* @description MCP Server for Membrane. It allows to query Membrane's internal state and operation from an LLM
* Ask the LLM questions like:
* - What APIs are deployed?
* - Is the Membrane instance healthy?
* - Give me a summary about the requests
*/
@MCElement(name = "membraneMCPServer")
public class MembraneMCPServer extends AbstractInterceptor {

private static final Logger log = LoggerFactory.getLogger(MembraneMCPServer.class);

@Override
public Outcome handleRequest(Exchange exc) {
try {
JSONRPCRequest request;
try {
request = JSONRPCRequest.parse(exc.getRequest().getBodyAsStreamDecoded());
} catch (JsonProcessingException e) {
exc.setResponse(createResponse(createErrorResponse(exc, null, ERR_PARSE_ERROR, "Parse error", e)));
return RETURN;
} catch (IOException e) {
exc.setResponse(createResponse(createErrorResponse(exc, null, ERR_INVALID_REQUEST, "Invalid Request", e)));
return RETURN;
}

JSONRPCResponse rpcResponse;
try {
rpcResponse = processMCPRequest(request);
} catch (IllegalArgumentException e) {
exc.setResponse(createResponse(createErrorResponse(exc, request, JSONRPCResponse.ERR_INVALID_PARAMS, "Invalid params", e)));
return RETURN;
} catch (Exception e) {
exc.setResponse(createResponse(createErrorResponse(exc, request, ERR_INTERNAL_ERROR, "Internal error", e)));
return RETURN;
}
exc.setResponse(createResponse(rpcResponse));
return RETURN;

} catch (IOException e) {
throw new RuntimeException(e);
}
Comment thread
christiangoerdes marked this conversation as resolved.
}

private JSONRPCResponse createErrorResponse(Exchange exc, @Nullable JSONRPCRequest request, int code, String message, Exception e) {
if (code == ERR_INTERNAL_ERROR) {
log.warn("Failed to handle MCP request {} {}.", exc.getRequest().getMethod(), exc.getRequest().getUri(), e);
} else {
log.info("Rejected MCP request {} {}: {}", exc.getRequest().getMethod(), exc.getRequest().getUri(), e.getMessage());
}
return error(request == null ? null : request.getId(), code, message, e.getMessage());
}

private static Response createResponse(MCPResponse<?> mcpResponse) throws IOException {
if (mcpResponse == null) {
return Response.noContent().build();
}
return createResponse(mcpResponse.toRpcResponse());
}

private static Response createResponse(@Nullable JSONRPCResponse rpcResponse) throws IOException {
if (rpcResponse == null) {
return Response.noContent().build();
}
return ok().contentType(APPLICATION_JSON).body(rpcResponse.toJson()).build();
}

private @Nullable JSONRPCResponse processMCPRequest(JSONRPCRequest request) throws IOException {
switch (request.getMethod()) {
case "initialize" -> {
return initialize(request).toRpcResponse();
}
case "notifications/initialized" -> {
log.debug("MCP Client is ready");
return null;
}
case "tools/list" -> {
return toolsList(request).toRpcResponse();
}
case "tools/call" -> {
var response = toolsCall(request);
return response == null ? null : response.toRpcResponse();
}
default -> {
log.info("Unknown MCP Request: {}", request);
if (request.getId() != null) {
return error(request.getId(), ERR_METHOD_NOT_FOUND, "Method not found");
}
}
Comment thread
christiangoerdes marked this conversation as resolved.
Outdated
}
return null;
}

private MCPToolsCallResponse toolsCall(JSONRPCRequest request) {
var req = MCPToolsCall.from(request);

log.debug("Received MCP tools call: {}", req);

switch (req.getName()) {
case "listProxies" -> {
return listProxies(req);
}
case "getExchanges" -> {
return getExchanges(req);
}
case "getStatistics" -> {
return getStatistics(req);
}
default -> log.info("Unknown tools call: " + req.getName());
}

return null;
}

private MCPToolsCallResponse getStatistics(MCPToolsCall req) {
return MCPToolsCallResponse.from(req)
.withJson(getRouter().getStatistics());
}

private MCPToolsCallResponse getExchanges(MCPToolsCall req) {
return MCPToolsCallResponse.from(req)
.withJson(Map.of("exchanges", getRouter().getExchangeStore()
.getAllExchangesAsList().stream()
.map(MembraneMCPServer::getExchangeDescription)
.filter(Objects::nonNull).toList()));
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}

private static @Nullable HashMap<String, Object> getExchangeDescription(AbstractExchange e) {
if (e.getResponse() == null)
return null;

var exc = new HashMap<String, Object>();
exc.put("id", e.getId());
var request = new HashMap<String, Object>();
var response = new HashMap<String, Object>();

request.put("method", e.getRequest().getMethod());
request.put("path", e.getRequest().getUri());
request.put("body", e.getRequest().getBodyAsStringDecoded());
request.put("headers", e.getRequest().getHeader());

exc.put("request", request);
exc.put("response", response);
Comment thread
christiangoerdes marked this conversation as resolved.
Outdated
return exc;
Comment thread
christiangoerdes marked this conversation as resolved.
Outdated
}

private MCPToolsCallResponse listProxies(MCPToolsCall req) {
return MCPToolsCallResponse.from(req)
.withJson(Map.of("proxies", getRouter().getRuleManager().getRules().stream().map(this::getProxyDescription).toList()));
}

private @NotNull HashMap<String, Object> getProxyDescription(Proxy p) {
var proxy = new HashMap<String, Object>();
proxy.put("name", p.getName());

String type;
switch (p) {
case APIProxy ap -> {
type = "API";
// proxy.put("openapi", ap.getOpenapi());
}
case ServiceProxy s -> {
type = "serviceProxy";
}
case SOAPProxy sp -> {
type = "soapProxy";
proxy.put("wsdl", sp.getWsdl());
proxy.put("serviceName", sp.getServiceName());
}
default -> {
type = "unknown";
}
}
Comment thread
christiangoerdes marked this conversation as resolved.
Outdated

var interceptors = p.getFlow().stream().map(i -> {
Map<String, String> interceptor = new HashMap<>();
interceptor.put("name", i.getDisplayName());
return interceptor;
}).toList();

proxy.put("statistics", getRouter().getExchangeStore().getStatistics(p.getKey()));

proxy.put("interceptors", interceptors);
proxy.put("type", type);
proxy.put("rule", p.getKey().toString());
return proxy;
}

private MCPToolsListResponse toolsList(JSONRPCRequest request) {
log.debug("Tools list");
return MCPToolsListResponse.from(MCPToolsList.from(request))
.withTool(new MCPToolsListResponse.Tool(
"listProxies",
"Lists all the proxies, e.g. API, soapProxy", Map.of("type", "object")))
.withTool(new MCPToolsListResponse.Tool("getExchanges", "Gets the last 100 HTTP exchanges", Map.of("type", "object")))
.withTool(new MCPToolsListResponse.Tool("getStatistics", "Gets Membrane runtime statistics", Map.of("type", "object")));

// Map.of("type", "object",
// "properties", Map.of("query", Map.of("type", "string")),
// "required", List.of("query"))));

}

private MCPInitializeResponse initialize(JSONRPCRequest request) throws IOException {
return new MCPInitializeResponse(new MCPInitialize(request))
.withCapabilities(getCapabilities())
.withServerInfo("Membrane", VERSION);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

private static @NotNull HashMap<String, Object> getCapabilities() {
var capabilities = new HashMap<String, Object>();
capabilities.put("tools", Map.of("lastExchanges", false));
return capabilities;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

@Override
public String getDisplayName() {
return "Membrane MCP Server";
}
}
Loading
Loading