diff --git a/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/McpConfiguration.java b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/McpConfiguration.java index ce0176c8a5..bead6742ba 100644 --- a/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/McpConfiguration.java +++ b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/McpConfiguration.java @@ -84,7 +84,7 @@ public class McpConfiguration extends Configuration MCP_CLIENT_VERSION = config.property(String.class, "client.version", (c, v) -> v, McpConfiguration::defaultServerVersion); MCP_INACTIVITY_TIMEOUT = config.property(Duration.class, "inactivity.timeout", - (c, v) -> Duration.parse(v), "PT60S"); + (c, v) -> Duration.parse(v), "PT300S"); MCP_KEEPALIVE_TOLERANCE = config.property("keepalive.tolerance", 2); MCP_SSE_KEEPALIVE_INTERVAL = config.property(Duration.class, "sse.keepalive.interval", (c, v) -> Duration.parse(v), "PT15S"); diff --git a/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/McpServerFactory.java b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/McpServerFactory.java index 25c00f1007..fec27e8f23 100644 --- a/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/McpServerFactory.java +++ b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/McpServerFactory.java @@ -130,6 +130,7 @@ public final class McpServerFactory implements McpStreamFactory private static final String STATUS_400 = "400"; private static final String STATUS_401 = "401"; private static final String STATUS_403 = "403"; + private static final String STATUS_404 = "404"; private static final String STATUS_405 = "405"; private static final String STATUS_406 = "406"; private static final String STATUS_410 = "410"; @@ -478,7 +479,7 @@ public MessageConsumer newStream( case "DELETE": if (session == null) { - newStream = new McpRejectHandler(sender, STATUS_400)::onNetBegin; + newStream = new McpRejectHandler(sender, STATUS_404)::onNetBegin; } else { @@ -541,7 +542,7 @@ else if (!acceptIncludesEventStream(accept)) else if (session == null) { deauthorize.run(); - newStream = new McpRejectHandler(sender, STATUS_400)::onNetBegin; + newStream = new McpRejectHandler(sender, STATUS_404)::onNetBegin; } else if (session.eventsUnsupported) { @@ -1008,7 +1009,7 @@ private int decodeJsonRpcMethod( { if (server.session == null) { - server.onDecodeInvalidRequest(traceId, authorization); + server.onDecodeSessionNotFound(traceId, authorization); server.decoder = decodeIgnore; break decode; } @@ -2554,6 +2555,19 @@ private void onDecodeInvalidRequest( "Invalid request"); } + private void onDecodeSessionNotFound( + long traceId, + long authorization) + { + doNetBegin(traceId, authorization, + httpBeginExRW.wrap(codecBuffer, 0, codecBuffer.capacity()) + .typeId(httpTypeId) + .headersItem(h -> h.name(HTTP_HEADER_STATUS).value(STATUS_404)) + .inject(this::injectAltSvc) + .build()); + doNetEnd(traceId, authorization); + } + private void onDecodeMethodNotFound( long traceId, long authorization) diff --git a/runtime/binding-mcp/src/test/java/io/aklivity/zilla/runtime/binding/mcp/internal/McpConfigurationTest.java b/runtime/binding-mcp/src/test/java/io/aklivity/zilla/runtime/binding/mcp/internal/McpConfigurationTest.java index 7d9cfaa7b6..308ccd7344 100644 --- a/runtime/binding-mcp/src/test/java/io/aklivity/zilla/runtime/binding/mcp/internal/McpConfigurationTest.java +++ b/runtime/binding-mcp/src/test/java/io/aklivity/zilla/runtime/binding/mcp/internal/McpConfigurationTest.java @@ -37,6 +37,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThrows; +import java.time.Duration; import java.util.UUID; import java.util.function.LongFunction; @@ -86,6 +87,14 @@ public void shouldVerifyConstants() throws Exception assertEquals(MCP_LEASE_RETRY.name(), MCP_LEASE_RETRY_NAME); } + @Test + public void shouldDefaultInactivityTimeoutToFiveMinutes() + { + McpConfiguration config = new McpConfiguration(); + + assertEquals(Duration.ofMinutes(5), config.inactivityTimeout()); + } + @Test public void shouldSupplyDefaultSessionId() { diff --git a/runtime/binding-mcp/src/test/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/McpServerIT.java b/runtime/binding-mcp/src/test/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/McpServerIT.java index 22c595b416..72724df40e 100644 --- a/runtime/binding-mcp/src/test/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/McpServerIT.java +++ b/runtime/binding-mcp/src/test/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/McpServerIT.java @@ -869,6 +869,24 @@ public void shouldRejectRequestMethodUnknown() throws Exception k3po.finish(); } + @Test + @Configuration("server.yaml") + @Specification({ + "${net}/reject.request.session.unknown/client"}) + public void shouldRejectRequestSessionUnknown() throws Exception + { + k3po.finish(); + } + + @Test + @Configuration("server.yaml") + @Specification({ + "${net}/reject.request.session.missing/client"}) + public void shouldRejectRequestSessionMissing() throws Exception + { + k3po.finish(); + } + @Test @Configuration("server.yaml") @Specification({ diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/network/reject.request.session.missing/client.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/network/reject.request.session.missing/client.rpt new file mode 100644 index 0000000000..a1e435b2e0 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/network/reject.request.session.missing/client.rpt @@ -0,0 +1,41 @@ +# +# Copyright 2021-2026 Aklivity Inc +# +# Licensed under the Aklivity Community License (the "License"); you may not use +# this file except in compliance with the License. You may obtain a copy of the +# License at +# +# https://www.aklivity.io/aklivity-community-license/ +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. +# + +connect "zilla://streams/net0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${http:beginEx() + .typeId(zilla:id("http")) + .header(":method", "POST") + .header(":scheme", "http") + .header(":authority", "localhost:8080") + .header(":path", "/mcp") + .header("content-type", "application/json") + .header("accept", "application/json, text/event-stream") + .header("mcp-protocol-version", "2025-11-25") + .build()} + +connected + +write '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' +write close + +read zilla:begin.ext ${http:matchBeginEx() + .typeId(zilla:id("http")) + .header(":status", "404") + .build()} + +read closed diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/network/reject.request.session.missing/server.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/network/reject.request.session.missing/server.rpt new file mode 100644 index 0000000000..2ee7b5dc27 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/network/reject.request.session.missing/server.rpt @@ -0,0 +1,42 @@ +# +# Copyright 2021-2026 Aklivity Inc +# +# Licensed under the Aklivity Community License (the "License"); you may not use +# this file except in compliance with the License. You may obtain a copy of the +# License at +# +# https://www.aklivity.io/aklivity-community-license/ +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. +# + +accept "zilla://streams/net0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +accepted + +read zilla:begin.ext ${http:matchBeginEx() + .typeId(zilla:id("http")) + .header(":method", "POST") + .header(":path", "/mcp") + .header("content-type", "application/json") + .header("accept", "application/json, text/event-stream") + .header("mcp-protocol-version", "2025-11-25") + .build()} + +connected + +read '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' +read closed + +write zilla:begin.ext ${http:beginEx() + .typeId(zilla:id("http")) + .header(":status", "404") + .build()} +write flush + +write close diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/network/reject.request.session.unknown/client.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/network/reject.request.session.unknown/client.rpt new file mode 100644 index 0000000000..0b47912401 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/network/reject.request.session.unknown/client.rpt @@ -0,0 +1,42 @@ +# +# Copyright 2021-2026 Aklivity Inc +# +# Licensed under the Aklivity Community License (the "License"); you may not use +# this file except in compliance with the License. You may obtain a copy of the +# License at +# +# https://www.aklivity.io/aklivity-community-license/ +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. +# + +connect "zilla://streams/net0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${http:beginEx() + .typeId(zilla:id("http")) + .header(":method", "POST") + .header(":scheme", "http") + .header(":authority", "localhost:8080") + .header(":path", "/mcp") + .header("content-type", "application/json") + .header("accept", "application/json, text/event-stream") + .header("mcp-protocol-version", "2025-11-25") + .header("mcp-session-id", "5ca1ab1e-c0de-4a11-1057-000000000000") + .build()} + +connected + +write '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' +write close + +read zilla:begin.ext ${http:matchBeginEx() + .typeId(zilla:id("http")) + .header(":status", "404") + .build()} + +read closed diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/network/reject.request.session.unknown/server.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/network/reject.request.session.unknown/server.rpt new file mode 100644 index 0000000000..62066e9571 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/network/reject.request.session.unknown/server.rpt @@ -0,0 +1,43 @@ +# +# Copyright 2021-2026 Aklivity Inc +# +# Licensed under the Aklivity Community License (the "License"); you may not use +# this file except in compliance with the License. You may obtain a copy of the +# License at +# +# https://www.aklivity.io/aklivity-community-license/ +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. +# + +accept "zilla://streams/net0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +accepted + +read zilla:begin.ext ${http:matchBeginEx() + .typeId(zilla:id("http")) + .header(":method", "POST") + .header(":path", "/mcp") + .header("content-type", "application/json") + .header("accept", "application/json, text/event-stream") + .header("mcp-protocol-version", "2025-11-25") + .header("mcp-session-id", "5ca1ab1e-c0de-4a11-1057-000000000000") + .build()} + +connected + +read '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' +read closed + +write zilla:begin.ext ${http:beginEx() + .typeId(zilla:id("http")) + .header(":status", "404") + .build()} +write flush + +write close diff --git a/specs/binding-mcp.spec/src/test/java/io/aklivity/zilla/specs/binding/mcp/streams/network/NetworkIT.java b/specs/binding-mcp.spec/src/test/java/io/aklivity/zilla/specs/binding/mcp/streams/network/NetworkIT.java index 4a28b1af5c..3fdcc0f0e3 100644 --- a/specs/binding-mcp.spec/src/test/java/io/aklivity/zilla/specs/binding/mcp/streams/network/NetworkIT.java +++ b/specs/binding-mcp.spec/src/test/java/io/aklivity/zilla/specs/binding/mcp/streams/network/NetworkIT.java @@ -252,6 +252,24 @@ public void shouldRejectRequestMethodUnknown() throws Exception k3po.finish(); } + @Test + @Specification({ + "${net}/reject.request.session.unknown/client", + "${net}/reject.request.session.unknown/server"}) + public void shouldRejectRequestSessionUnknown() throws Exception + { + k3po.finish(); + } + + @Test + @Specification({ + "${net}/reject.request.session.missing/client", + "${net}/reject.request.session.missing/server"}) + public void shouldRejectRequestSessionMissing() throws Exception + { + k3po.finish(); + } + @Test @Specification({ "${net}/lifecycle.initialize.reject.capabilities.invalid/client",