You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@juneau.apache.org by ja...@apache.org on 2026/10/26 22:23:08 UTC
(juneau) branch master updated: Drop the wall-clock deadline from MCP JSON safety checks (new JsonValueSafety.NO_DEADLINE): server-authored schemas, tool input and structured output are node-bounded, so a slow host no longer rejects valid configs or results. Mark an MCP config validated only after its schemas pass, so a bad config keeps failing, and restore the input schema in the serializer-parity tests.
This is an automated email from the ASF dual-hosted git repository.
jamesbognar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/juneau.git
The following commit(s) were added to refs/heads/master by this push:
new 455b4d34d7 Drop the wall-clock deadline from MCP JSON safety checks (new JsonValueSafety.NO_DEADLINE): server-authored schemas, tool input and structured output are node-bounded, so a slow host no longer rejects valid configs or results. Mark an MCP config validated only after its schemas pass, so a bad config keeps failing, and restore the input schema in the serializer-parity tests.
455b4d34d7 is described below
commit 455b4d34d7e48c4d6f7efe886996ec2c8d2ca40c
Author: James Bognar <ja...@apache.org>
AuthorDate: Mon Oct 26 18:23:01 2026 -0400
Drop the wall-clock deadline from MCP JSON safety checks (new JsonValueSafety.NO_DEADLINE): server-authored schemas, tool input and structured output are node-bounded, so a slow host no longer rejects valid configs or results. Mark an MCP config validated only after its schemas pass, so a bad config keeps failing, and restore the input schema in the serializer-parity tests.
---
.../juneau/commons/utils/JsonValueSafety.java | 9 +++++
.../rest/server/mcp/McpSerializerParity_Test.java | 13 +++----
.../rest/server/mcp/v20250618/McpRevision.java | 16 ++++++---
.../server/mcp/v20250618/McpSchemaCheck_Test.java | 41 ++++++++++++++++++++++
.../v20250618/McpStructuredOutputSafety_Test.java | 33 +++++++++++++++++
.../rest/server/mcp/v20260728/McpRevision.java | 5 +--
.../rest/server/mcp/v20260728/McpSchemaSafety.java | 15 ++++----
.../server/mcp/v20260728/McpSchemaSafety_Test.java | 38 ++++++++++++++++++++
.../mcp/v20260728/McpSerializerParity_Test.java | 13 +++----
.../v20260728/McpStructuredOutputSafety_Test.java | 33 +++++++++++++++++
.../juneau/rest/server/mcp/McpTypedHandlers.java | 2 +-
11 files changed, 184 insertions(+), 34 deletions(-)
diff --git a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/utils/JsonValueSafety.java b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/utils/JsonValueSafety.java
index caa01f7c18..378508ae1a 100644
--- a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/utils/JsonValueSafety.java
+++ b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/utils/JsonValueSafety.java
@@ -46,6 +46,15 @@ public final class JsonValueSafety {
/** Maximum allowed wall-clock time, in milliseconds, for a single traversal. */
public static final long MAX_TRAVERSAL_MILLIS = 100;
+ /**
+ * Deadline value meaning "no wall-clock limit": the depth, {@link #MAX_NODES node-count} and cycle bounds still apply.
+ *
+ * <p>
+ * Use this for server-authored or already size-bounded values, where a slow walk (a loaded host, a GC pause)
+ * must not be reported as a violation.
+ */
+ public static final long NO_DEADLINE = Long.MAX_VALUE;
+
private JsonValueSafety() {}
/**
diff --git a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/McpSerializerParity_Test.java b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/McpSerializerParity_Test.java
index 086e0d3bc9..c657c71559 100644
--- a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/McpSerializerParity_Test.java
+++ b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/McpSerializerParity_Test.java
@@ -80,17 +80,11 @@ class McpSerializerParity_Test extends TestBase {
}
/**
- * Typed tool adapted then re-exposed <em>without</em> an input schema.
+ * Typed tool adapted then re-exposed with its derived schemas.
*
* <p>
- * {@link Result} still drives output-schema {@code $defs}/{@code $ref} and the structured-content
- * URI/fragment fields under test. Input schema is deliberately omitted: {@code tools/call} runs
- * {@code McpSchemaSafety.validateInput}, which shares a 100 ms wall-clock
- * {@link org.apache.juneau.commons.utils.JsonValueSafety} deadline across the schema and argument walks.
- * Under CI load ({@code forkCount=8}) one of the two sequential servlet/mixin calls can trip
- * {@code Tool input schema traversal exceeded 100 ms} while the other succeeds, breaking byte-identity.
- * A null input schema makes {@code validateInput} a no-op so both sides stay on the success path;
- * serializer policy under test does not depend on input-schema validation.
+ * {@link Result} drives output-schema {@code $defs}/{@code $ref} and the structured-content
+ * URI/fragment fields under test.
*
* <p>
* Argument type remains {@code Map.class} (not a bean {@code Args}) so the typed adapter's schema
@@ -108,6 +102,7 @@ class McpSerializerParity_Test extends TestBase {
@Override public McpToolSpec descriptor() {
return new McpToolSpec()
.setName(adapted.descriptor().getName())
+ .setInputSchema(adapted.descriptor().getInputSchema())
.setOutputSchema(adapted.descriptor().getOutputSchema());
}
@Override public McpToolOutcome call(Map<String,Object> arguments, BeanStore ctx) {
diff --git a/juneau-rest/juneau-rest-server-mcp-v20250618/src/main/java/org/apache/juneau/rest/server/mcp/v20250618/McpRevision.java b/juneau-rest/juneau-rest-server-mcp-v20250618/src/main/java/org/apache/juneau/rest/server/mcp/v20250618/McpRevision.java
index c431bc9184..5fc5349c16 100644
--- a/juneau-rest/juneau-rest-server-mcp-v20250618/src/main/java/org/apache/juneau/rest/server/mcp/v20250618/McpRevision.java
+++ b/juneau-rest/juneau-rest-server-mcp-v20250618/src/main/java/org/apache/juneau/rest/server/mcp/v20250618/McpRevision.java
@@ -128,8 +128,10 @@ public final class McpRevision implements org.apache.juneau.rest.server.mcp.McpR
*
* <p>
* Before walking, each schema is passed through {@link JsonValueSafety} to bound nesting depth,
- * node count, traversal time, and cyclic/shared references, so a pathological config cannot stall
- * or exhaust the walk.
+ * node count, and cyclic/shared references, so a pathological config cannot stall or exhaust the
+ * walk. The wall-clock deadline is not applied: the schemas are server-authored and the node bound
+ * already limits the work, so a slow walk (a loaded CI host, a GC pause) must not reject a valid
+ * config.
*
* <p>
* Note this cannot be a complete guarantee against every possible wire shape: {@code JsonSchema}
@@ -155,7 +157,8 @@ public final class McpRevision implements org.apache.juneau.rest.server.mcp.McpR
if (schema == null)
return;
var raw = schema.toJsonMap();
- JsonValueSafety.check(raw, "Tool '" + toolName + "' " + role + " for MCP revision 2025-06-18");
+ // No deadline: a server-authored schema must not fail validation because the host is slow.
+ JsonValueSafety.check(raw, "Tool '" + toolName + "' " + role + " for MCP revision 2025-06-18", JsonValueSafety.NO_DEADLINE);
if (neq(raw.get("type"), "object"))
throw iaex("Tool ''%s'' %s root type ''%s'' is not object; MCP revision 2025-06-18 requires object schemas.",
toolName, role, raw.get("type"));
@@ -231,8 +234,10 @@ public final class McpRevision implements org.apache.juneau.rest.server.mcp.McpR
reqnn("config", config);
reqnn("ctx", ctx);
- if (VALIDATED.add(config))
+ if (! VALIDATED.contains(config)) {
validateSchemas(config);
+ VALIDATED.add(config); // Only after success, so a bad config keeps failing on every request.
+ }
var req = exchange.request();
if (req == null)
@@ -338,7 +343,8 @@ public final class McpRevision implements org.apache.juneau.rest.server.mcp.McpR
throw new McpException(CODE_INTERNAL_ERROR,
"Tool structuredContent must be a JSON object for MCP revision 2025-06-18");
try {
- JsonValueSafety.check(outcome.getStructuredContent(), "Tool structuredContent");
+ // No deadline: the node bound caps the walk, and a slow host must not fail a valid result.
+ JsonValueSafety.check(outcome.getStructuredContent(), "Tool structuredContent", JsonValueSafety.NO_DEADLINE);
} catch (IllegalArgumentException e) {
throw new McpException(CODE_INTERNAL_ERROR, e.getMessage());
}
diff --git a/juneau-rest/juneau-rest-server-mcp-v20250618/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/McpSchemaCheck_Test.java b/juneau-rest/juneau-rest-server-mcp-v20250618/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/McpSchemaCheck_Test.java
index e49173ae46..e869852be7 100644
--- a/juneau-rest/juneau-rest-server-mcp-v20250618/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/McpSchemaCheck_Test.java
+++ b/juneau-rest/juneau-rest-server-mcp-v20250618/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/McpSchemaCheck_Test.java
@@ -21,6 +21,7 @@ import static org.junit.jupiter.api.Assertions.*;
import java.util.*;
import org.apache.juneau.commons.inject.*;
+import org.apache.juneau.commons.utils.*;
import org.apache.juneau.marshall.collections.*;
import org.apache.juneau.rest.server.mcp.*;
import org.junit.jupiter.api.*;
@@ -295,4 +296,44 @@ class McpSchemaCheck_Test {
var config = new McpServerConfig().addTool(tool("t", schema, null));
assertThrows(IllegalArgumentException.class, () -> McpRevision.validateSchemas(config));
}
+
+ /** A valid schema whose first walk stalls past the {@link JsonValueSafety} deadline, like a loaded host or GC pause. */
+ @SuppressWarnings({
+ "java:S2160" // Test double: equality is irrelevant, only the stalled entrySet() matters.
+ })
+ private static final class StallingSchema extends JsonMap {
+ private static final long serialVersionUID = 1L;
+ private boolean stalled;
+
+ @Override
+ public Set<Map.Entry<String,Object>> entrySet() {
+ if (! stalled) {
+ stalled = true;
+ try {
+ Thread.sleep(JsonValueSafety.MAX_TRAVERSAL_MILLIS + 50);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ return super.entrySet();
+ }
+ }
+
+ @Test
+ void e04_validateSchemas_slowWalk_notRejected() {
+ var schema = new StallingSchema();
+ schema.put("type", "object");
+ var config = new McpServerConfig().addTool(tool("t", null, schema));
+ assertDoesNotThrow(() -> McpRevision.validateSchemas(config));
+ }
+
+ @Test
+ void e05_badConfig_failsOnEveryRequest() {
+ var config = new McpServerConfig().addTool(tool("risky", JsonMap.of("type", "object", "oneOf", List.of()), null));
+ var revision = new McpRevision(null);
+ var exchange = ping();
+ assertThrows(IllegalArgumentException.class, () -> revision.dispatch(exchange, config, ctx));
+ // A config is marked validated only after its schemas pass, so the second request still fails.
+ assertThrows(IllegalArgumentException.class, () -> revision.dispatch(exchange, config, ctx));
+ }
}
diff --git a/juneau-rest/juneau-rest-server-mcp-v20250618/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/McpStructuredOutputSafety_Test.java b/juneau-rest/juneau-rest-server-mcp-v20250618/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/McpStructuredOutputSafety_Test.java
index 616a7aae4e..953d8563e1 100644
--- a/juneau-rest/juneau-rest-server-mcp-v20250618/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/McpStructuredOutputSafety_Test.java
+++ b/juneau-rest/juneau-rest-server-mcp-v20250618/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/McpStructuredOutputSafety_Test.java
@@ -24,6 +24,7 @@ import java.util.*;
import org.apache.juneau.bean.jsonrpc.*;
import org.apache.juneau.bean.mcp.v20250618.*;
import org.apache.juneau.commons.inject.*;
+import org.apache.juneau.commons.utils.*;
import org.apache.juneau.marshall.collections.*;
import org.apache.juneau.rest.server.mcp.McpExchange;
import org.apache.juneau.rest.server.mcp.McpResponseResult;
@@ -172,4 +173,36 @@ class McpStructuredOutputSafety_Test {
assertInstanceOf(CallToolResult.class, resp.getResult());
});
}
+
+ /** A valid map whose first walk stalls past the {@link JsonValueSafety} deadline, like a loaded host or GC pause. */
+ @SuppressWarnings({
+ "java:S2160", // Test double: equality is irrelevant, only the stalled entrySet() matters.
+ "java:S2925" // The sleep simulates a stalled host, not a wait for a condition.
+ })
+ private static final class StallingMap extends LinkedHashMap<String,Object> {
+ private static final long serialVersionUID = 1L;
+ private boolean stalled;
+
+ @Override
+ public Set<Map.Entry<String,Object>> entrySet() {
+ if (! stalled) {
+ stalled = true;
+ try {
+ Thread.sleep(JsonValueSafety.MAX_TRAVERSAL_MILLIS + 50);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ return super.entrySet();
+ }
+ }
+
+ @Test
+ void b03_slowWalk_notRejected() {
+ var slow = new StallingMap();
+ slow.put("a", 1);
+ var resp = send(configReturning(slow), requestWithId(1));
+ assertNull(resp.getError());
+ assertInstanceOf(CallToolResult.class, resp.getResult());
+ }
}
diff --git a/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/McpRevision.java b/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/McpRevision.java
index 2dc5d01a77..65eb2b7989 100644
--- a/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/McpRevision.java
+++ b/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/McpRevision.java
@@ -694,7 +694,8 @@ public final class McpRevision implements org.apache.juneau.rest.server.mcp.McpR
if (outcome == null || outcome.getStructuredContent() == null)
return;
try {
- JsonValueSafety.check(outcome.getStructuredContent(), "Tool structuredContent");
+ // No deadline: the node bound caps the walk, and a slow host must not fail a valid result.
+ JsonValueSafety.check(outcome.getStructuredContent(), "Tool structuredContent", JsonValueSafety.NO_DEADLINE);
} catch (IllegalArgumentException e) {
throw new McpException(CODE_INTERNAL_ERROR, e.getMessage());
}
@@ -1071,7 +1072,7 @@ public final class McpRevision implements org.apache.juneau.rest.server.mcp.McpR
*/
static String argumentsHash(Map<String,Object> arguments) {
try {
- JsonValueSafety.check(arguments, PARAM_ARGUMENTS);
+ JsonValueSafety.check(arguments, PARAM_ARGUMENTS, JsonValueSafety.NO_DEADLINE); // Node-bounded; no wall clock.
} catch (IllegalArgumentException e) {
throw new McpException(CODE_INVALID_PARAMS, e.getMessage());
}
diff --git a/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/McpSchemaSafety.java b/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/McpSchemaSafety.java
index 32b0086917..0b961cd2a7 100644
--- a/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/McpSchemaSafety.java
+++ b/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/McpSchemaSafety.java
@@ -35,10 +35,10 @@ import org.apache.juneau.rest.server.mcp.McpSchema;
* <p>
* The neutral {@link McpSchema} is an unconstrained JSON object carrier, and a client can send an
* arbitrarily large or deep argument object. Before any validation runs, both the schema graph and the
- * argument graph are checked against the shared {@link JsonValueSafety} depth/node/deadline bounds,
+ * argument graph are checked against the shared {@link JsonValueSafety} depth/node bounds
+ * (no wall-clock deadline: the node bound caps the work, and a slow host must not reject valid input),
* so a hostile or accidentally-huge input is rejected up front rather than blowing the stack or spinning
- * indefinitely. The two checks share a single deadline, so a slow schema graph cannot buy the argument
- * graph extra time (or vice versa).
+ * indefinitely.
*
* <p>
* <b>No external fetches.</b> The schema is converted to a {@link JsonSchema} bean and validated as-is. No
@@ -172,10 +172,10 @@ final class McpSchemaSafety {
if (schema == null)
return;
var schemaMap = schema.toJsonMap();
- var deadline = JsonValueSafety.deadlineNanos();
try {
- JsonValueSafety.check(schemaMap, "Tool input schema", deadline);
- JsonValueSafety.check(args, "Tool input", deadline);
+ // No wall clock here: both walks are node-bounded, and the validation step below has its own compute budget.
+ JsonValueSafety.check(schemaMap, "Tool input schema", JsonValueSafety.NO_DEADLINE);
+ JsonValueSafety.check(args, "Tool input", JsonValueSafety.NO_DEADLINE);
} catch (IllegalArgumentException e) {
throw new McpException(McpRevision.CODE_INVALID_PARAMS, e.getMessage());
}
@@ -186,8 +186,7 @@ final class McpSchemaSafety {
/**
* Runs {@link JsonSchemaValidator} on a daemon thread and enforces a fresh {@link #MAX_VALIDATION_MILLIS}
* compute budget against the task's own compute time - deliberately <b>not</b> the caller's remaining share
- * of the structural-traversal deadline computed in {@link #validateInput}. That deadline is a wall-clock
- * budget that is already partially spent by the two {@link JsonValueSafety#check} calls and the
+ * of any structural-traversal wall clock. Such a budget would already be partially spent by the two {@link JsonValueSafety#check} calls and the
* schema-to-bean conversion above, both of which run on the (possibly preempted) request thread under load;
* deriving the validation budget from whatever wall-clock happened to remain would let purely-external
* scheduling pressure - not any property of the schema or arguments being validated - trip a false-positive
diff --git a/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/McpSchemaSafety_Test.java b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/McpSchemaSafety_Test.java
index cafa15e3df..960e88b328 100644
--- a/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/McpSchemaSafety_Test.java
+++ b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/McpSchemaSafety_Test.java
@@ -497,4 +497,42 @@ class McpSchemaSafety_Test {
return null;
}
}
+
+ /** A valid map whose first walk stalls past the {@link JsonValueSafety} deadline, like a loaded host or GC pause. */
+ @SuppressWarnings({
+ "java:S2160", // Test double: equality is irrelevant, only the stalled entrySet() matters.
+ "java:S2925" // The sleep simulates a stalled host, not a wait for a condition.
+ })
+ private static final class StallingMap extends LinkedHashMap<String,Object> {
+ private static final long serialVersionUID = 1L;
+ private boolean stalled;
+
+ @Override
+ public Set<Map.Entry<String,Object>> entrySet() {
+ if (! stalled) {
+ stalled = true;
+ try {
+ Thread.sleep(JsonValueSafety.MAX_TRAVERSAL_MILLIS + 50);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ return super.entrySet();
+ }
+ }
+
+ @Test
+ void d09_validateInput_slowArgumentWalk_notRejected() {
+ var schema = McpSchema.of(JsonMap.of("type", "object"));
+ var args = new StallingMap();
+ args.put("a", 1);
+ assertDoesNotThrow(() -> McpSchemaSafety.validateInput(schema, args));
+ }
+
+ @Test
+ void d10_argumentsHash_slowWalk_notRejected() {
+ var args = new StallingMap();
+ args.put("a", 1);
+ assertDoesNotThrow(() -> McpRevision.argumentsHash(args));
+ }
}
diff --git a/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/McpSerializerParity_Test.java b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/McpSerializerParity_Test.java
index 17b20eb5a7..a8a2cb6fc9 100644
--- a/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/McpSerializerParity_Test.java
+++ b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/McpSerializerParity_Test.java
@@ -84,17 +84,11 @@ class McpSerializerParity_Test extends TestBase {
}
/**
- * Typed tool adapted then re-exposed <em>without</em> an input schema.
+ * Typed tool adapted then re-exposed with its derived schemas.
*
* <p>
- * {@link Result} still drives output-schema {@code $defs}/{@code $ref} and the structured-content
- * URI/fragment fields under test. Input schema is deliberately omitted: {@code tools/call} runs
- * {@code McpSchemaSafety.validateInput}, which shares a 100 ms wall-clock
- * {@link org.apache.juneau.commons.utils.JsonValueSafety} deadline across the schema and argument walks.
- * Under CI load ({@code forkCount=8}) one of the two sequential servlet/mixin calls can trip
- * {@code Tool input schema traversal exceeded 100 ms} while the other succeeds, breaking byte-identity.
- * A null input schema makes {@code validateInput} a no-op so both sides stay on the success path;
- * serializer policy under test does not depend on input-schema validation.
+ * {@link Result} drives output-schema {@code $defs}/{@code $ref} and the structured-content
+ * URI/fragment fields under test.
*
* <p>
* Argument type remains {@code Map.class} (not a bean {@code Args}) so the typed adapter's schema
@@ -112,6 +106,7 @@ class McpSerializerParity_Test extends TestBase {
@Override public McpToolSpec descriptor() {
return new McpToolSpec()
.setName(adapted.descriptor().getName())
+ .setInputSchema(adapted.descriptor().getInputSchema())
.setOutputSchema(adapted.descriptor().getOutputSchema());
}
@Override public McpToolOutcome call(Map<String,Object> arguments, BeanStore ctx) {
diff --git a/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/McpStructuredOutputSafety_Test.java b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/McpStructuredOutputSafety_Test.java
index cea58b5999..330bbd65cd 100644
--- a/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/McpStructuredOutputSafety_Test.java
+++ b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/McpStructuredOutputSafety_Test.java
@@ -24,6 +24,7 @@ import java.util.*;
import org.apache.juneau.bean.jsonrpc.*;
import org.apache.juneau.bean.mcp.v20260728.*;
import org.apache.juneau.commons.inject.*;
+import org.apache.juneau.commons.utils.*;
import org.apache.juneau.marshall.collections.*;
import org.apache.juneau.rest.server.mcp.McpExchange;
import org.apache.juneau.rest.server.mcp.McpResponseResult;
@@ -199,4 +200,36 @@ class McpStructuredOutputSafety_Test {
assertNull(resp.getError());
assertInstanceOf(CallToolResult.class, resp.getResult());
}
+
+ /** A valid map whose first walk stalls past the {@link JsonValueSafety} deadline, like a loaded host or GC pause. */
+ @SuppressWarnings({
+ "java:S2160", // Test double: equality is irrelevant, only the stalled entrySet() matters.
+ "java:S2925" // The sleep simulates a stalled host, not a wait for a condition.
+ })
+ private static final class StallingMap extends LinkedHashMap<String,Object> {
+ private static final long serialVersionUID = 1L;
+ private boolean stalled;
+
+ @Override
+ public Set<Map.Entry<String,Object>> entrySet() {
+ if (! stalled) {
+ stalled = true;
+ try {
+ Thread.sleep(JsonValueSafety.MAX_TRAVERSAL_MILLIS + 50);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ return super.entrySet();
+ }
+ }
+
+ @Test
+ void b07_slowWalk_notRejected() {
+ var slow = new StallingMap();
+ slow.put("a", 1);
+ var resp = send(configReturning(slow), requestWithId(1));
+ assertNull(resp.getError());
+ assertInstanceOf(CallToolResult.class, resp.getResult());
+ }
}
diff --git a/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpTypedHandlers.java b/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpTypedHandlers.java
index 4ad947a62c..eaf4c411fa 100644
--- a/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpTypedHandlers.java
+++ b/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpTypedHandlers.java
@@ -84,7 +84,7 @@ public final class McpTypedHandlers {
var result = typed.call(bound, ctx);
var tree = canonicalize(result);
try {
- JsonValueSafety.check(tree, "Tool structuredContent");
+ JsonValueSafety.check(tree, "Tool structuredContent", JsonValueSafety.NO_DEADLINE); // Node-bounded; no wall clock.
} catch (IllegalArgumentException e) {
throw new McpException(-32603, e.getMessage());
}