-
Notifications
You must be signed in to change notification settings - Fork 167
MCPServer for Membrane #2930
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
MCPServer for Membrane #2930
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 cd06f7f
feat: add JSON-RPC request, response, and MCP initialization support
predic8 2c12663
refactor: streamline MCP request processing and response generation
predic8 eee7cc9
refactor: remove redundant IOException declaration in toolsList method
predic8 dfed9b9
Enhance MCP server error handling with detailed response generation
christiangoerdes f1b99b6
Refactor MCP server to replace MCPResponse with JSONRPCResponse for c…
christiangoerdes 0ccddde
Add support for fetching Membrane runtime statistics in MCP server
christiangoerdes 4d7b3d3
Limit MCP server exchange retrieval to the last 100 entries
christiangoerdes 5de11e4
Refactor JSON-RPC request handling to distinguish between missing and…
christiangoerdes afaee6d
Add ResponseKind to JSONRPCResponse for explicit result/error distinc…
christiangoerdes 602d186
Improve JSON-RPC validation by rejecting notifications in MCPRequest …
christiangoerdes d56dc7d
Rename "lastExchanges" capability to "listChanged" in MCP server
christiangoerdes 5ab2c30
Replace `Objects.requireNonNull` with static import `requireNonNull` …
christiangoerdes f465969
Remove unnecessary `IOException` declarations from MCP server methods…
christiangoerdes ce5099c
Add response details (status, body, headers) to MCP server exchange s…
christiangoerdes 9a0440f
Reorder case blocks in `MembraneMCPServer` for consistency
christiangoerdes 819ed81
Refactor error response handling in `MembraneMCPServer` to improve cl…
christiangoerdes 7fa6d37
Refactor JSON-RPC utilities to improve validation, normalization, and…
christiangoerdes 22bb792
Add unit tests for `JSONRPCRequest` and `JSONRPCResponse` to validate…
christiangoerdes fe04d8f
Introduce MCP tool registration and utilities, including `McpToolRegi…
christiangoerdes 8b397b6
Add `McpPayloadSanitizer` and `McpSessionContext` utilities for MCP p…
christiangoerdes e29eb12
Remove unused imports in `McpPayloadSanitizer` for cleanup
christiangoerdes 6132374
Refactor and streamline MCP server request handling: add tool registr…
christiangoerdes bc522ff
Refactor `MembraneMCPServer`: improve HTTP 405 response handling and …
christiangoerdes b953c17
Add unit tests for `MembraneMCPServer` and `MCPInitialize` to validat…
christiangoerdes ab38255
Refactor `MembraneMCPServer` error handling: introduce `InvalidToolAr…
christiangoerdes 0f0049d
Merge branch 'master' into roadmap
christiangoerdes 32803b3
Update `toString` implementation in `MCPToolsCall` to display argumen…
christiangoerdes 69e6fb6
Extract utility methods from `MembraneMCPServer` to new `MCPUtil` cla…
christiangoerdes a911b22
Introduce session management in `MembraneMCPServer`: add `SESSION_HEA…
christiangoerdes File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
252 changes: 252 additions & 0 deletions
252
core/src/main/java/com/predic8/membrane/core/interceptor/mcp/MembraneMCPServer.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | ||
| } | ||
| } | ||
|
|
||
| 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"); | ||
| } | ||
| } | ||
|
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())); | ||
|
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); | ||
|
christiangoerdes marked this conversation as resolved.
Outdated
|
||
| return exc; | ||
|
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"; | ||
| } | ||
| } | ||
|
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); | ||
| } | ||
|
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; | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||
|
|
||
| @Override | ||
| public String getDisplayName() { | ||
| return "Membrane MCP Server"; | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.