Skip to content

Enhance MembraneMCPServer #2938

Merged
predic8 merged 19 commits into
masterfrom
mcp-exchange-config
May 18, 2026
Merged

Enhance MembraneMCPServer #2938
predic8 merged 19 commits into
masterfrom
mcp-exchange-config

Conversation

@christiangoerdes

@christiangoerdes christiangoerdes commented May 11, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • Tools list and exchange tool now support optional response-size capping, returning either a sized result or a full paged result with paging metadata.
  • Improvements

    • Added offset-based paging and richer page metadata (hasMore, nextOffset).
    • Centralized integer-range validation for tool arguments.
    • Consistent session/handshake state management for MCP interactions.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 11, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Centralizes MCP argument integer-range validation and widens an exception constructor; extends getExchanges to support offset paging and an optional maxResponseSize that produces either a normal page or a byte-budgeted, truncated response.

Changes

Integer Validation Refactoring & Size-Limited MCP Responses

Layer / File(s) Summary
MCPUtil Validation Refactoring
core/src/main/java/com/predic8/membrane/core/interceptor/mcp/MCPUtil.java
New private validateIntRange(name, parsed, minimum, maximum) enforces [minimum, maximum] on long inputs and returns int. getOptionalIntArgument and getOptionalSizeArgument delegate bounds checking to it. InvalidToolArgumentsException constructor changed to package-private.
getExchanges Tool: description & parsing
core/src/main/java/com/predic8/membrane/core/interceptor/mcp/MembraneMCPServer.java
Updates tool description to include filters/offsets and maxResponseSize; parses offset and optional maxResponseSize, delegates matching/paging to ExchangeToolSupport, and routes to a size-limited response builder when present.
getExchanges Schema & helper parsing
core/src/main/java/com/predic8/membrane/core/interceptor/mcp/ExchangeToolSupport.java
Provides getExchangesSchema(maxExchanges) and parseQuery(...) that validate and construct an ExchangeQuery including offset, limit, includeBodies, host/port/pathPattern, and optional maxResponseSize.
Exchange selection & description
core/src/main/java/com/predic8/membrane/core/interceptor/mcp/ExchangeToolSupport.java
findPage filters exchanges (reverse order), applies offset/limit, computes hasMore/nextOffset, and describeExchanges maps exchanges to sanitized JSON-ready descriptions (optional bodies).
Size-limited response builder
core/src/main/java/com/predic8/membrane/core/interceptor/mcp/ExchangeToolSupport.java
buildFullPageResponse returns an unrestricted page; buildSizedPageResponse enforces a maxResponseSize byte budget by measuring MCP envelope and per-exchange escaped JSON bytes and accumulating exchanges until the budget would be exceeded; throws InvalidToolArgumentsException when the provided size is too small.
McpSessionManager
core/src/main/java/com/predic8/membrane/core/interceptor/mcp/McpSessionManager.java
New package-private session manager storing sessions with last-access timestamps, TTL-based cleanup, create/get/markReady operations, and injectable clock for testability.
MembraneMCPServer: dispatcher & session enforcement
core/src/main/java/com/predic8/membrane/core/interceptor/mcp/MembraneMCPServer.java
Replaces prior session/payload fields with McpSessionManager and ExchangeToolSupport; centralizes POST JSON-RPC parsing/dispatch; creates sessions and returns header on initialize; enforces handshake phases via requireSession/requireReadySession and returns standard MCP errors for invalid states.
MembraneMCPServer: getExchanges orchestration
core/src/main/java/com/predic8/membrane/core/interceptor/mcp/MembraneMCPServer.java
Updates tool schema/definition to include paging and maxResponseSize, delegates parsing and paging to ExchangeToolSupport, and returns either full or sized page responses based on maxResponseSize.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Membrane as MembraneMCPServer
  participant SessionMgr as McpSessionManager
  participant Validator as MCPUtil.validateIntRange
  participant ExchangeSupport as ExchangeToolSupport
  Client->>Membrane: POST JSON-RPC (getExchanges)
  Membrane->>SessionMgr: requireReadySession(sessionId)
  Membrane->>Validator: validate offset/limit/maxResponseSize
  Membrane->>ExchangeSupport: parseQuery(args)
  Membrane->>ExchangeSupport: findPage(exchanges, query)
  ExchangeSupport-->>Membrane: ExchangePage
  alt maxResponseSize provided
    Membrane->>ExchangeSupport: buildSizedPageResponse(page, maxResponseSize)
    ExchangeSupport-->>Membrane: sized payload (truncated) + hasMore/nextOffset
    Membrane-->>Client: return size-limited response
  else
    Membrane->>ExchangeSupport: buildFullPageResponse(page)
    ExchangeSupport-->>Membrane: full payload + hasMore/nextOffset
    Membrane-->>Client: return normal JSON exchange page
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • predic8

Poem

🐰 I nibble bounds with gentle care,
Counting bytes in JSON air,
Pages trimmed to fit the crate,
Offsets hop to mark the state,
A rabbit guards each tiny share.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.45% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Enhance MembraneMCPServer' is vague and overly generic, using non-descriptive language that doesn't convey the specific changes. Consider a more specific title that highlights the main change, such as 'Add pagination support to getExchanges MCP tool' or 'Refactor MembraneMCPServer with session management and paginated responses'.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch mcp-exchange-config

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@membrane-ci-server

Copy link
Copy Markdown

This pull request needs "/ok-to-test" from an authorized committer.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
core/src/main/java/com/predic8/membrane/core/interceptor/mcp/MembraneMCPServer.java (1)

276-282: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Remove the duplicated getOptionalPort helper.

This class contains the same getOptionalPort(MCPToolsCall) method twice at lines 276–278 and 280–282, which will cause a compilation error.

🐛 Proposed fix
-    private static `@Nullable` Integer getOptionalPort(MCPToolsCall call) {
-        return call.getArgument("port") == null ? null : getOptionalIntArgument(call, "port", -1, 1, 65535);
-    }
-
     private static `@Nullable` Integer getOptionalPort(MCPToolsCall call) {
         return call.getArgument("port") == null ? null : getOptionalIntArgument(call, "port", -1, 1, 65535);
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@core/src/main/java/com/predic8/membrane/core/interceptor/mcp/MembraneMCPServer.java`
around lines 276 - 282, Remove the duplicated helper method
getOptionalPort(MCPToolsCall) so there is only one definition; locate both
identical methods named getOptionalPort in MembraneMCPServer and delete one of
them, leaving the single implementation that returns call.getArgument("port") ==
null ? null : getOptionalIntArgument(call, "port", -1, 1, 65535); (ensure
getOptionalIntArgument and MCPToolsCall references remain unchanged).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@core/src/main/java/com/predic8/membrane/core/interceptor/mcp/MembraneMCPServer.java`:
- Line 257: Change the response-size tracking variables in the capped-response
path from int to long: declare maxResponseSize as long (use -1L for the
sentinel) and update any local counters/accumulators used to sum response bytes
(e.g., the variables that accumulate bytes in the capped-response logic) to long
as well; adjust the call to getOptionalSizeArgument by adding/using a
long-returning variant or cast its result to long, and update all comparisons
and arithmetic to use long literals (e.g., 1L, Long.MAX_VALUE) so the size cap
checks (the accumulation and the maxResponseSize comparison) cannot overflow.

---

Outside diff comments:
In
`@core/src/main/java/com/predic8/membrane/core/interceptor/mcp/MembraneMCPServer.java`:
- Around line 276-282: Remove the duplicated helper method
getOptionalPort(MCPToolsCall) so there is only one definition; locate both
identical methods named getOptionalPort in MembraneMCPServer and delete one of
them, leaving the single implementation that returns call.getArgument("port") ==
null ? null : getOptionalIntArgument(call, "port", -1, 1, 65535); (ensure
getOptionalIntArgument and MCPToolsCall references remain unchanged).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 20377016-c431-4367-abfd-2964a64d81e8

📥 Commits

Reviewing files that changed from the base of the PR and between 1af0001 and c467035.

📒 Files selected for processing (2)
  • core/src/main/java/com/predic8/membrane/core/interceptor/mcp/MCPUtil.java
  • core/src/main/java/com/predic8/membrane/core/interceptor/mcp/MembraneMCPServer.java

@christiangoerdes

Copy link
Copy Markdown
Collaborator Author

/ok-to-test

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
core/src/main/java/com/predic8/membrane/core/interceptor/mcp/MembraneMCPServer.java (2)

336-364: 🏗️ Heavy lift

Consider incremental size tracking to improve performance.

The current implementation serializes the full response for each exchange added (line 347), resulting in O(n²) serialization complexity. For 100 exchanges, this means approximately 5,050 serialization operations.

While the current approach is correct and handles all edge cases properly (including the excellent error message at line 351), an incremental size-tracking approach could significantly improve performance for users who specify maxResponseSize.

The tradeoff is complexity: incremental tracking requires careful accounting of JSON structure, escaping, and delimiters (as attempted in earlier code versions). The current approach prioritizes correctness and maintainability over performance.

Given that maxResponseSize is optional and the feature is new, deferring this optimization is reasonable. However, if this feature sees heavy use with large page sizes, the performance impact could become noticeable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@core/src/main/java/com/predic8/membrane/core/interceptor/mcp/MembraneMCPServer.java`
around lines 336 - 364, The loop currently calls
responseSize(createExchangePageResponse(...)) each iteration causing O(n²)
serialization; replace this with incremental size tracking: precompute the
fixed-size overhead of the outer response (fields from
createExchangePageResponse except the exchanges array) once, then for each added
entry use requireExchangeDescription(...) and compute its serialized byte size
(e.g., ObjectMapper.writeValueAsBytes or JsonGenerator to bytes) plus 1 byte for
the comma/delimiter, add that to a running total (trackedSize), and compare
trackedSize to maxResponseSize instead of serializing the whole candidate; keep
the same removal/exception behavior when exceeding maxResponseSize and still
call createExchangePageResponse only once at the end, referencing
describedExchanges, createExchangePageResponse, responseSize,
requireExchangeDescription, nextOffset, and maxResponseSize to locate and update
the logic.

210-210: 💤 Low value

Consider mentioning response-size limits in the tool description.

The description mentions "offsets for follow-up calls" but doesn't mention the new maxResponseSize parameter that allows clients to cap response sizes. Consider adding ", and optional response-size limits" for completeness.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@core/src/main/java/com/predic8/membrane/core/interceptor/mcp/MembraneMCPServer.java`
at line 210, Update the tool description string in MembraneMCPServer (the
description used for the "Gets recent HTTP exchanges..." tool) to mention the
new maxResponseSize parameter by appending text like ", and optional
response-size limits" so the description now references sanitized headers,
optional bodies, request filters, offsets for follow-up calls, and optional
response-size limits.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In
`@core/src/main/java/com/predic8/membrane/core/interceptor/mcp/MembraneMCPServer.java`:
- Around line 336-364: The loop currently calls
responseSize(createExchangePageResponse(...)) each iteration causing O(n²)
serialization; replace this with incremental size tracking: precompute the
fixed-size overhead of the outer response (fields from
createExchangePageResponse except the exchanges array) once, then for each added
entry use requireExchangeDescription(...) and compute its serialized byte size
(e.g., ObjectMapper.writeValueAsBytes or JsonGenerator to bytes) plus 1 byte for
the comma/delimiter, add that to a running total (trackedSize), and compare
trackedSize to maxResponseSize instead of serializing the whole candidate; keep
the same removal/exception behavior when exceeding maxResponseSize and still
call createExchangePageResponse only once at the end, referencing
describedExchanges, createExchangePageResponse, responseSize,
requireExchangeDescription, nextOffset, and maxResponseSize to locate and update
the logic.
- Line 210: Update the tool description string in MembraneMCPServer (the
description used for the "Gets recent HTTP exchanges..." tool) to mention the
new maxResponseSize parameter by appending text like ", and optional
response-size limits" so the description now references sanitized headers,
optional bodies, request filters, offsets for follow-up calls, and optional
response-size limits.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 64b51325-5ea3-462b-9a92-d674904e2adb

📥 Commits

Reviewing files that changed from the base of the PR and between a6cad9b and 2939625.

📒 Files selected for processing (1)
  • core/src/main/java/com/predic8/membrane/core/interceptor/mcp/MembraneMCPServer.java

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
core/src/main/java/com/predic8/membrane/core/interceptor/mcp/ExchangeToolSupport.java (1)

135-157: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use long for capped-response byte accounting.

Lines 138-157 and 209-258 still total response bytes in int. Since maxResponseSize allows values up to Integer.MAX_VALUE, these additions can wrap negative near the limit and let an oversized response slip past the cap. This is the same overflow issue that was previously flagged before the logic moved into this helper.

Proposed fix
-        int prefixBytes = measureEscapedJsonStringContentSize(EXCHANGES_PAYLOAD_PREFIX);
-        int separatorBytes = measureEscapedJsonStringContentSize(EXCHANGES_PAYLOAD_SEPARATOR);
-        int minimumResponseSize = responseEnvelope.fixedBytes() + prefixBytes + measureExchangePageSuffixBytes(false, null);
+        long prefixBytes = measureEscapedJsonStringContentSize(EXCHANGES_PAYLOAD_PREFIX);
+        long separatorBytes = measureEscapedJsonStringContentSize(EXCHANGES_PAYLOAD_SEPARATOR);
+        long minimumResponseSize = responseEnvelope.fixedBytes() + prefixBytes + measureExchangePageSuffixBytes(false, null);
@@
-        int exchangesBytes = 0;
+        long exchangesBytes = 0L;
@@
-            int additionalExchangeBytes = measureExchangeBytes(description, describedExchanges.isEmpty(), separatorBytes);
+            long additionalExchangeBytes = measureExchangeBytes(description, describedExchanges.isEmpty(), separatorBytes);
@@
-            int trackedSize = responseEnvelope.fixedBytes()
+            long trackedSize = responseEnvelope.fixedBytes()
                     + prefixBytes
                     + exchangesBytes
                     + additionalExchangeBytes
                     + measureExchangePageSuffixBytes(hasMore, candidateNextOffset);
@@
-    private int measureExchangeBytes(Map<String, Object> description, boolean firstExchange, int separatorBytes) {
+    private long measureExchangeBytes(Map<String, Object> description, boolean firstExchange, long separatorBytes) {
         return measureEscapedJsonStringContentSize(serializeJson(description)) + (firstExchange ? 0 : separatorBytes);
     }
@@
-    private int measureExchangePageSuffixBytes(boolean hasMore, `@Nullable` Integer nextOffset) {
+    private long measureExchangePageSuffixBytes(boolean hasMore, `@Nullable` Integer nextOffset) {
         return measureEscapedJsonStringContentSize(buildExchangePageSuffix(hasMore, nextOffset));
     }
@@
-    private int measureEscapedJsonStringContentSize(String value) {
+    private long measureEscapedJsonStringContentSize(String value) {
         try {
             return OM.writeValueAsBytes(value).length - 2;
         } catch (JsonProcessingException e) {
             throw new RuntimeException("Failed to serialize JSON string content", e);
         }
     }
@@
-    private static int utf8Size(String value) {
+    private static long utf8Size(String value) {
         return value.getBytes(StandardCharsets.UTF_8).length;
     }
@@
-    private record TextResponseEnvelope(int prefixBytes, int suffixBytes) {
-        private int fixedBytes() {
+    private record TextResponseEnvelope(long prefixBytes, long suffixBytes) {
+        private long fixedBytes() {
             return prefixBytes + suffixBytes;
         }
     }

Also applies to: 209-258, 283-286

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@core/src/main/java/com/predic8/membrane/core/interceptor/mcp/ExchangeToolSupport.java`
around lines 135 - 157, The code uses int for cumulative byte accounting which
can overflow near Integer.MAX_VALUE; change all size/count variables used for
summing and comparisons to long (e.g., prefixBytes, separatorBytes,
minimumResponseSize, exchangesBytes, additionalExchangeBytes, trackedSize) and
ensure comparisons against maxResponseSize are done safely by promoting
maxResponseSize to long (or casting it to long) so the check in the loop in
ExchangeToolSupport (around measureTextResponseEnvelope,
measureEscapedJsonStringContentSize, measureExchangePageSuffixBytes,
describeExchangeOrThrow, measureExchangeBytes, nextOffset) cannot overflow;
apply the same long-based fix to the other similar summations referenced in the
file (the other exchange-page accounting blocks).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In
`@core/src/main/java/com/predic8/membrane/core/interceptor/mcp/ExchangeToolSupport.java`:
- Around line 135-157: The code uses int for cumulative byte accounting which
can overflow near Integer.MAX_VALUE; change all size/count variables used for
summing and comparisons to long (e.g., prefixBytes, separatorBytes,
minimumResponseSize, exchangesBytes, additionalExchangeBytes, trackedSize) and
ensure comparisons against maxResponseSize are done safely by promoting
maxResponseSize to long (or casting it to long) so the check in the loop in
ExchangeToolSupport (around measureTextResponseEnvelope,
measureEscapedJsonStringContentSize, measureExchangePageSuffixBytes,
describeExchangeOrThrow, measureExchangeBytes, nextOffset) cannot overflow;
apply the same long-based fix to the other similar summations referenced in the
file (the other exchange-page accounting blocks).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 83e61806-afab-4655-b4ba-e4c67f2a021c

📥 Commits

Reviewing files that changed from the base of the PR and between 33a8a28 and bc2b9ca.

📒 Files selected for processing (3)
  • core/src/main/java/com/predic8/membrane/core/interceptor/mcp/ExchangeToolSupport.java
  • core/src/main/java/com/predic8/membrane/core/interceptor/mcp/McpSessionManager.java
  • core/src/main/java/com/predic8/membrane/core/interceptor/mcp/MembraneMCPServer.java

@christiangoerdes christiangoerdes requested a review from predic8 May 18, 2026 09:28
@predic8 predic8 merged commit 003b0e9 into master May 18, 2026
5 checks passed
@predic8 predic8 deleted the mcp-exchange-config branch May 18, 2026 10:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants