diff --git a/runtime/binding-kafka/src/main/java/io/aklivity/zilla/runtime/binding/kafka/internal/stream/KafkaClientConnectionPool.java b/runtime/binding-kafka/src/main/java/io/aklivity/zilla/runtime/binding/kafka/internal/stream/KafkaClientConnectionPool.java index ba92efb769..61bd4d770f 100644 --- a/runtime/binding-kafka/src/main/java/io/aklivity/zilla/runtime/binding/kafka/internal/stream/KafkaClientConnectionPool.java +++ b/runtime/binding-kafka/src/main/java/io/aklivity/zilla/runtime/binding/kafka/internal/stream/KafkaClientConnectionPool.java @@ -20,6 +20,7 @@ import static io.aklivity.zilla.runtime.engine.concurrent.Signaler.NO_CANCEL_ID; import static java.lang.System.currentTimeMillis; +import java.time.Instant; import java.util.List; import java.util.function.Consumer; import java.util.function.IntConsumer; @@ -591,6 +592,15 @@ public long signalAt( return delegate.signalAt(timeMillis, signalId, handler); } + @Override + public long signalAt( + Instant time, + int signalId, + IntConsumer handler) + { + return delegate.signalAt(time, signalId, handler); + } + @Override public void signalNow( long originId, @@ -639,6 +649,19 @@ public long signalAt( return stream.doStreamSignalAt(traceId, timeMillis, signalId); } + @Override + public long signalAt( + Instant time, + long originId, + long routedId, + long streamId, + long traceId, + int signalId, + int contextId) + { + return signalAt(time.toEpochMilli(), originId, routedId, streamId, traceId, signalId, contextId); + } + @Override public long signalTask( Runnable task, diff --git a/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/config/McpAuthorizationConfig.java b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/config/McpAuthorizationConfig.java index 7cbb13518d..291d11620a 100644 --- a/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/config/McpAuthorizationConfig.java +++ b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/config/McpAuthorizationConfig.java @@ -21,6 +21,7 @@ public final class McpAuthorizationConfig { public final String name; + public final String credentials; public transient String qname; @@ -36,8 +37,10 @@ public static McpAuthorizationConfigBuilder builder( } McpAuthorizationConfig( - String name) + String name, + String credentials) { this.name = name; + this.credentials = credentials; } } diff --git a/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/config/McpAuthorizationConfigBuilder.java b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/config/McpAuthorizationConfigBuilder.java index ec5620079c..1e22193648 100644 --- a/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/config/McpAuthorizationConfigBuilder.java +++ b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/config/McpAuthorizationConfigBuilder.java @@ -23,6 +23,7 @@ public final class McpAuthorizationConfigBuilder extends ConfigBuilder mapper; private String name; + private String credentials; McpAuthorizationConfigBuilder( Function mapper) @@ -44,9 +45,16 @@ public McpAuthorizationConfigBuilder name( return this; } + public McpAuthorizationConfigBuilder credentials( + String credentials) + { + this.credentials = credentials; + return this; + } + @Override public T build() { - return mapper.apply(new McpAuthorizationConfig(name)); + return mapper.apply(new McpAuthorizationConfig(name, credentials)); } } diff --git a/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/config/McpCacheConfig.java b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/config/McpCacheConfig.java new file mode 100644 index 0000000000..f839ac7355 --- /dev/null +++ b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/config/McpCacheConfig.java @@ -0,0 +1,48 @@ +/* + * Copyright 2021-2024 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. + */ +package io.aklivity.zilla.runtime.binding.mcp.config; + +import static java.util.function.Function.identity; + +import java.time.Duration; +import java.util.function.Function; + +public final class McpCacheConfig +{ + public final String store; + public final Duration ttl; + public final McpAuthorizationConfig authorization; + + McpCacheConfig( + String store, + Duration ttl, + McpAuthorizationConfig authorization) + { + this.store = store; + this.ttl = ttl; + this.authorization = authorization; + } + + public static McpCacheConfigBuilder builder() + { + return new McpCacheConfigBuilder<>(identity()); + } + + public static McpCacheConfigBuilder builder( + Function mapper) + { + return new McpCacheConfigBuilder<>(mapper); + } +} diff --git a/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/config/McpCacheConfigBuilder.java b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/config/McpCacheConfigBuilder.java new file mode 100644 index 0000000000..60b01d82a3 --- /dev/null +++ b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/config/McpCacheConfigBuilder.java @@ -0,0 +1,74 @@ +/* + * Copyright 2021-2024 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. + */ +package io.aklivity.zilla.runtime.binding.mcp.config; + +import java.time.Duration; +import java.util.function.Function; + +import io.aklivity.zilla.runtime.engine.config.ConfigBuilder; + +public final class McpCacheConfigBuilder extends ConfigBuilder> +{ + private final Function mapper; + + private String store; + private Duration ttl; + private McpAuthorizationConfig authorization; + + McpCacheConfigBuilder( + Function mapper) + { + this.mapper = mapper; + } + + @Override + @SuppressWarnings("unchecked") + protected Class> thisType() + { + return (Class>) getClass(); + } + + public McpCacheConfigBuilder store( + String store) + { + this.store = store; + return this; + } + + public McpCacheConfigBuilder ttl( + Duration ttl) + { + this.ttl = ttl; + return this; + } + + public McpCacheConfigBuilder authorization( + McpAuthorizationConfig authorization) + { + this.authorization = authorization; + return this; + } + + public McpAuthorizationConfigBuilder> authorization() + { + return McpAuthorizationConfig.builder(this::authorization); + } + + @Override + public T build() + { + return mapper.apply(new McpCacheConfig(store, ttl, authorization)); + } +} diff --git a/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/config/McpOptionsConfig.java b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/config/McpOptionsConfig.java index 54ed19a0c0..ffb2419a6d 100644 --- a/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/config/McpOptionsConfig.java +++ b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/config/McpOptionsConfig.java @@ -24,15 +24,18 @@ public final class McpOptionsConfig extends OptionsConfig public final List prompts; public final McpElicitationConfig elicitation; public final McpAuthorizationConfig authorization; + public final McpCacheConfig cache; - public McpOptionsConfig( + McpOptionsConfig( List prompts, McpElicitationConfig elicitation, - McpAuthorizationConfig authorization) + McpAuthorizationConfig authorization, + McpCacheConfig cache) { this.prompts = prompts; this.elicitation = elicitation; this.authorization = authorization; + this.cache = cache; } public static McpOptionsConfigBuilder builder() diff --git a/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/config/McpOptionsConfigBuilder.java b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/config/McpOptionsConfigBuilder.java index 2af1eee33a..f1eb98aa82 100644 --- a/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/config/McpOptionsConfigBuilder.java +++ b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/config/McpOptionsConfigBuilder.java @@ -28,6 +28,7 @@ public final class McpOptionsConfigBuilder extends ConfigBuilder prompts; private McpElicitationConfig elicitation; private McpAuthorizationConfig authorization; + private McpCacheConfig cache; public McpOptionsConfigBuilder( Function mapper) @@ -71,6 +72,18 @@ public McpAuthorizationConfigBuilder> authorization() return McpAuthorizationConfig.builder(this::authorization); } + public McpOptionsConfigBuilder cache( + McpCacheConfig cache) + { + this.cache = cache; + return this; + } + + public McpCacheConfigBuilder> cache() + { + return McpCacheConfig.builder(this::cache); + } + @Override @SuppressWarnings("unchecked") protected Class> thisType() @@ -81,6 +94,6 @@ protected Class> thisType() @Override public T build() { - return mapper.apply(new McpOptionsConfig(prompts, elicitation, authorization)); + return mapper.apply(new McpOptionsConfig(prompts, elicitation, authorization, cache)); } } 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 a0f0613cea..4e2e1f54fa 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 @@ -14,6 +14,9 @@ */ package io.aklivity.zilla.runtime.binding.mcp.internal; +import static io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.McpBeginExFW.KIND_PROMPTS_LIST; +import static io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.McpBeginExFW.KIND_RESOURCES_LIST; +import static io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.McpBeginExFW.KIND_TOOLS_LIST; import static io.aklivity.zilla.runtime.engine.EngineConfiguration.ENGINE_WORKERS; import java.lang.invoke.MethodHandle; @@ -21,8 +24,11 @@ import java.lang.invoke.MethodType; import java.security.SecureRandom; import java.time.Duration; +import java.util.HashSet; import java.util.HexFormat; +import java.util.Set; import java.util.UUID; +import java.util.function.IntPredicate; import java.util.function.Supplier; import org.agrona.LangUtil; @@ -46,6 +52,9 @@ public class McpConfiguration extends Configuration public static final PropertyDef MCP_SSE_KEEPALIVE_INTERVAL; public static final BooleanPropertyDef MCP_ALT_SVC_ENABLED; public static final PropertyDef MCP_ALT_SVC_MAX_AGE; + public static final PropertyDef MCP_HYDRATE_FILTER; + public static final PropertyDef MCP_LEASE_TTL; + public static final PropertyDef MCP_LEASE_RETRY; static { @@ -72,6 +81,12 @@ public class McpConfiguration extends Configuration MCP_ALT_SVC_ENABLED = config.property("alt.svc.enabled", McpConfiguration::defaultAltSvcEnabled); MCP_ALT_SVC_MAX_AGE = config.property(Duration.class, "alt.svc.max.age", (c, v) -> Duration.parse(v), "PT24H"); + MCP_HYDRATE_FILTER = config.property(IntPredicate.class, "hydrate.filter", + McpConfiguration::decodeHydrateFilter, McpConfiguration::defaultHydrateFilter); + MCP_LEASE_TTL = config.property(Duration.class, "lease.ttl", + (c, v) -> Duration.parse(v), "PT30S"); + MCP_LEASE_RETRY = config.property(Duration.class, "lease.retry", + (c, v) -> Duration.parse(v), "PT0.1S"); MCP_CONFIG = config; } @@ -146,6 +161,21 @@ public Duration altSvcMaxAge() return MCP_ALT_SVC_MAX_AGE.get(this); } + public IntPredicate hydrateFilter() + { + return MCP_HYDRATE_FILTER.get(this); + } + + public Duration leaseTtl() + { + return MCP_LEASE_TTL.get(this); + } + + public Duration leaseRetry() + { + return MCP_LEASE_RETRY.get(this); + } + @FunctionalInterface public interface SessionIdSupplier { @@ -258,6 +288,36 @@ private static ElicitationIdSupplier decodeElicitationIdSupplier( return supplier; } + private static IntPredicate decodeHydrateFilter( + String value) + { + final Set kinds = new HashSet<>(); + for (String name : value.split("\\s+")) + { + switch (name) + { + case "tools": + kinds.add(KIND_TOOLS_LIST); + break; + case "resources": + kinds.add(KIND_RESOURCES_LIST); + break; + case "prompts": + kinds.add(KIND_PROMPTS_LIST); + break; + default: + break; + } + } + return kinds::contains; + } + + private static boolean defaultHydrateFilter( + int kind) + { + return true; + } + private static String defaultElicitationIdSupplier() { final byte[] bytes = new byte[4]; diff --git a/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/config/McpBindingConfig.java b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/config/McpBindingConfig.java index f43e4b127b..3e970c0d74 100644 --- a/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/config/McpBindingConfig.java +++ b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/config/McpBindingConfig.java @@ -18,13 +18,18 @@ import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Optional; -import java.util.function.LongFunction; import java.util.stream.Collectors; +import org.agrona.collections.Object2ObjectHashMap; + import io.aklivity.zilla.runtime.binding.mcp.config.McpOptionsConfig; +import io.aklivity.zilla.runtime.binding.mcp.internal.McpConfiguration; +import io.aklivity.zilla.runtime.binding.mcp.internal.stream.cache.McpProxyCache; import io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.HttpBeginExFW; import io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.McpBeginExFW; +import io.aklivity.zilla.runtime.engine.EngineContext; import io.aklivity.zilla.runtime.engine.config.BindingConfig; import io.aklivity.zilla.runtime.engine.guard.GuardHandler; @@ -36,27 +41,34 @@ public final class McpBindingConfig public final long id; public final McpOptionsConfig options; public final GuardHandler guard; + public final McpProxyCache cache; + public final Map sessions; private final List routes; - public McpBindingConfig( - BindingConfig binding) - { - this(binding, null); - } - public McpBindingConfig( BindingConfig binding, - LongFunction supplyGuard) + McpConfiguration config, + EngineContext context) { this.id = binding.id; this.options = (McpOptionsConfig) binding.options; this.routes = binding.routes.stream() .map(McpRouteConfig::new) .collect(Collectors.toList()); - this.guard = supplyGuard != null && options != null && options.authorization != null - ? supplyGuard.apply(binding.resolveId.applyAsLong(options.authorization.name)) - : null; + + this.guard = Optional.ofNullable(options) + .map(o -> o.authorization) + .map(a -> a.name) + .map(binding.resolveId::applyAsLong) + .map(context::supplyGuard) + .orElse(null); + + this.cache = Optional.ofNullable(options) + .map(o -> o.cache) + .map(cache -> new McpProxyCache(binding, config, context, cache)) + .orElse(null); + this.sessions = new Object2ObjectHashMap<>(); } public McpRouteConfig resolve( diff --git a/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/config/McpOptionsConfigAdapter.java b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/config/McpOptionsConfigAdapter.java index 9e7bce4753..a7ac8dbdf1 100644 --- a/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/config/McpOptionsConfigAdapter.java +++ b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/config/McpOptionsConfigAdapter.java @@ -14,13 +14,18 @@ */ package io.aklivity.zilla.runtime.binding.mcp.internal.config; +import java.time.Duration; + import jakarta.json.Json; import jakarta.json.JsonArray; import jakarta.json.JsonArrayBuilder; import jakarta.json.JsonObject; import jakarta.json.JsonObjectBuilder; +import jakarta.json.JsonString; import jakarta.json.bind.adapter.JsonbAdapter; +import io.aklivity.zilla.runtime.binding.mcp.config.McpCacheConfig; +import io.aklivity.zilla.runtime.binding.mcp.config.McpCacheConfigBuilder; import io.aklivity.zilla.runtime.binding.mcp.config.McpElicitationConfig; import io.aklivity.zilla.runtime.binding.mcp.config.McpOptionsConfig; import io.aklivity.zilla.runtime.binding.mcp.config.McpOptionsConfigBuilder; @@ -40,6 +45,13 @@ public final class McpOptionsConfigAdapter implements OptionsConfigAdapterSpi, J private static final String AUTHORIZATION_NAME = "authorization"; + private static final String CACHE_NAME = "cache"; + private static final String CACHE_STORE_NAME = "store"; + private static final String CACHE_TTL_NAME = "ttl"; + private static final String CACHE_TTL_DEFAULT = "PT5M"; + private static final String CACHE_AUTHORIZATION_NAME = "authorization"; + private static final String CACHE_AUTHORIZATION_CREDENTIALS_NAME = "credentials"; + @Override public Kind kind() { @@ -90,6 +102,32 @@ public JsonObject adaptToJson( object.add(AUTHORIZATION_NAME, authorization); } + if (mcpOptions.cache != null) + { + JsonObjectBuilder cache = Json.createObjectBuilder(); + McpCacheConfig cacheConfig = mcpOptions.cache; + cache.add(CACHE_STORE_NAME, cacheConfig.store); + + if (cacheConfig.ttl != null) + { + cache.add(CACHE_TTL_NAME, cacheConfig.ttl.toString()); + } + + if (cacheConfig.authorization != null) + { + JsonObjectBuilder authorization = Json.createObjectBuilder(); + JsonObjectBuilder guardObject = Json.createObjectBuilder(); + if (cacheConfig.authorization.credentials != null) + { + guardObject.add(CACHE_AUTHORIZATION_CREDENTIALS_NAME, cacheConfig.authorization.credentials); + } + authorization.add(cacheConfig.authorization.name, guardObject); + cache.add(CACHE_AUTHORIZATION_NAME, authorization); + } + + object.add(CACHE_NAME, cache); + } + return object.build(); } @@ -132,6 +170,35 @@ public OptionsConfig adaptFromJson( .build(); } + if (object.containsKey(CACHE_NAME)) + { + JsonObject cache = object.getJsonObject(CACHE_NAME); + McpCacheConfigBuilder> cacheBuilder = builder.cache() + .store(cache.getString(CACHE_STORE_NAME)); + + cacheBuilder.ttl(Duration.parse(cache.containsKey(CACHE_TTL_NAME) + ? cache.getString(CACHE_TTL_NAME) + : CACHE_TTL_DEFAULT)); + + if (cache.containsKey(CACHE_AUTHORIZATION_NAME)) + { + JsonObject authorization = cache.getJsonObject(CACHE_AUTHORIZATION_NAME); + authorization.forEach((guard, value) -> + { + JsonObject guardObject = (JsonObject) value; + String credentials = guardObject.containsKey(CACHE_AUTHORIZATION_CREDENTIALS_NAME) + ? ((JsonString) guardObject.get(CACHE_AUTHORIZATION_CREDENTIALS_NAME)).getString() + : null; + cacheBuilder.authorization() + .name(guard) + .credentials(credentials) + .build(); + }); + } + + cacheBuilder.build(); + } + return builder.build(); } } diff --git a/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/config/McpProxySession.java b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/config/McpProxySession.java new file mode 100644 index 0000000000..b0068fca8f --- /dev/null +++ b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/config/McpProxySession.java @@ -0,0 +1,19 @@ +/* + * Copyright 2021-2024 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. + */ +package io.aklivity.zilla.runtime.binding.mcp.internal.config; + +public interface McpProxySession +{ +} diff --git a/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/config/McpRoutePrefix.java b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/config/McpRoutePrefix.java index 1e60927de4..f4890542df 100644 --- a/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/config/McpRoutePrefix.java +++ b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/config/McpRoutePrefix.java @@ -14,8 +14,10 @@ */ package io.aklivity.zilla.runtime.binding.mcp.internal.config; +import io.aklivity.zilla.runtime.binding.mcp.internal.types.String8FW; + public record McpRoutePrefix( long resolvedId, - String prefix) + String8FW prefix) { } diff --git a/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/McpClientFactory.java b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/McpClientFactory.java index bfa2534c9d..d9cb000afd 100644 --- a/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/McpClientFactory.java +++ b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/McpClientFactory.java @@ -30,7 +30,6 @@ import java.util.List; import java.util.Map; import java.util.function.Function; -import java.util.function.LongFunction; import java.util.function.LongUnaryOperator; import jakarta.json.stream.JsonParser; @@ -159,6 +158,7 @@ public final class McpClientFactory implements McpStreamFactory private final DirectBufferInputStreamEx inputRO = new DirectBufferInputStreamEx(); + private final EngineContext context; private final MutableDirectBuffer writeBuffer; private final MutableDirectBuffer extBuffer; private final MutableDirectBuffer codecBuffer; @@ -167,7 +167,6 @@ public final class McpClientFactory implements McpStreamFactory private final BindingHandler streamFactory; private final LongUnaryOperator supplyInitialId; private final LongUnaryOperator supplyReplyId; - private final LongFunction supplyGuard; private final int httpTypeId; private final int mcpTypeId; private final int decodeMax; @@ -198,6 +197,7 @@ public final class McpClientFactory implements McpStreamFactory private static final int SSE_IGNORE_VALUE = 4; private final JsonParserFactory parserFactory; + private final McpConfiguration config; private final Long2ObjectHashMap bindings; private final Map sessions = new Object2ObjectHashMap<>(); private final Int2ObjectHashMap resolvers; @@ -207,6 +207,8 @@ public McpClientFactory( McpConfiguration config, EngineContext context) { + this.config = config; + this.context = context; this.writeBuffer = context.writeBuffer(); this.extBuffer = new UnsafeBuffer(new byte[context.writeBuffer().capacity()]); this.codecBuffer = new UnsafeBuffer(new byte[context.writeBuffer().capacity()]); @@ -215,7 +217,6 @@ public McpClientFactory( this.streamFactory = context.streamFactory(); this.supplyInitialId = context::supplyInitialId; this.supplyReplyId = context::supplyReplyId; - this.supplyGuard = context::supplyGuard; this.bindings = new Long2ObjectHashMap<>(); this.httpTypeId = context.supplyTypeId(HTTP_TYPE_NAME); this.mcpTypeId = context.supplyTypeId(MCP_TYPE_NAME); @@ -1275,7 +1276,7 @@ public int routedTypeId() public void attach( BindingConfig binding) { - McpBindingConfig newBinding = new McpBindingConfig(binding, supplyGuard); + McpBindingConfig newBinding = new McpBindingConfig(binding, config, context); bindings.put(binding.id, newBinding); } diff --git a/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/McpProxyFactory.java b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/McpProxyFactory.java index 7349cc5caf..0223ed09f8 100644 --- a/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/McpProxyFactory.java +++ b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/McpProxyFactory.java @@ -21,123 +21,66 @@ import static io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.McpBeginExFW.KIND_RESOURCES_READ; import static io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.McpBeginExFW.KIND_TOOLS_CALL; import static io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.McpBeginExFW.KIND_TOOLS_LIST; -import static io.aklivity.zilla.runtime.engine.buffer.BufferPool.NO_SLOT; -import java.nio.charset.StandardCharsets; -import java.util.ArrayDeque; -import java.util.Deque; -import java.util.List; -import java.util.Map; -import java.util.function.LongUnaryOperator; - -import jakarta.json.stream.JsonParser; -import jakarta.json.stream.JsonParserFactory; +import java.util.function.Function; import org.agrona.DirectBuffer; -import org.agrona.MutableDirectBuffer; +import org.agrona.collections.Int2ObjectHashMap; import org.agrona.collections.Long2ObjectHashMap; -import org.agrona.collections.Object2ObjectHashMap; -import org.agrona.concurrent.UnsafeBuffer; import io.aklivity.zilla.runtime.binding.mcp.internal.McpConfiguration; import io.aklivity.zilla.runtime.binding.mcp.internal.config.McpBindingConfig; -import io.aklivity.zilla.runtime.binding.mcp.internal.config.McpRouteConfig; -import io.aklivity.zilla.runtime.binding.mcp.internal.config.McpRoutePrefix; -import io.aklivity.zilla.runtime.binding.mcp.internal.types.Flyweight; +import io.aklivity.zilla.runtime.binding.mcp.internal.stream.cache.McpProxyCache; +import io.aklivity.zilla.runtime.binding.mcp.internal.stream.cache.McpProxyCacheManager; import io.aklivity.zilla.runtime.binding.mcp.internal.types.OctetsFW; -import io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.AbortFW; import io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.BeginFW; -import io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.ChallengeFW; -import io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.DataFW; -import io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.EndFW; -import io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.FlushFW; import io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.McpBeginExFW; -import io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.McpChallengeExFW; -import io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.ResetFW; -import io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.WindowFW; -import io.aklivity.zilla.runtime.common.json.DirectBufferInputStreamEx; -import io.aklivity.zilla.runtime.common.json.StreamingJson; import io.aklivity.zilla.runtime.engine.EngineContext; import io.aklivity.zilla.runtime.engine.binding.BindingHandler; import io.aklivity.zilla.runtime.engine.binding.function.MessageConsumer; -import io.aklivity.zilla.runtime.engine.buffer.BufferPool; import io.aklivity.zilla.runtime.engine.config.BindingConfig; public final class McpProxyFactory implements McpStreamFactory { private static final String MCP_TYPE_NAME = "mcp"; - private static final List TOOLS_LIST_ITEM_JSON_PATH_INCLUDES = List.of("/tools/-/name"); - private static final List PROMPTS_LIST_ITEM_JSON_PATH_INCLUDES = List.of("/prompts/-/name"); - private static final List RESOURCES_LIST_ITEM_JSON_PATH_INCLUDES = List.of("/resources/-/uri"); - private final BeginFW beginRO = new BeginFW(); - private final DataFW dataRO = new DataFW(); - private final EndFW endRO = new EndFW(); - private final AbortFW abortRO = new AbortFW(); - private final FlushFW flushRO = new FlushFW(); - private final WindowFW windowRO = new WindowFW(); - private final ResetFW resetRO = new ResetFW(); - private final ChallengeFW challengeRO = new ChallengeFW(); private final McpBeginExFW mcpBeginExRO = new McpBeginExFW(); - private final OctetsFW emptyRO = new OctetsFW().wrap(new UnsafeBuffer(), 0, 0); - private final DirectBufferInputStreamEx inputRO = new DirectBufferInputStreamEx(); - private final DirectBuffer listReplyToolsOpenRO = - new UnsafeBuffer("{\"tools\":[".getBytes(StandardCharsets.UTF_8)); - private final DirectBuffer listReplyPromptsOpenRO = - new UnsafeBuffer("{\"prompts\":[".getBytes(StandardCharsets.UTF_8)); - private final DirectBuffer listReplyResourcesOpenRO = - new UnsafeBuffer("{\"resources\":[".getBytes(StandardCharsets.UTF_8)); - private final DirectBuffer listReplyCloseRO = - new UnsafeBuffer("]}".getBytes(StandardCharsets.UTF_8)); - private final DirectBuffer listReplySeparatorRO = - new UnsafeBuffer(",".getBytes(StandardCharsets.UTF_8)); - private final BeginFW.Builder beginRW = new BeginFW.Builder(); - private final DataFW.Builder dataRW = new DataFW.Builder(); - private final EndFW.Builder endRW = new EndFW.Builder(); - private final AbortFW.Builder abortRW = new AbortFW.Builder(); - private final FlushFW.Builder flushRW = new FlushFW.Builder(); - private final WindowFW.Builder windowRW = new WindowFW.Builder(); - private final ResetFW.Builder resetRW = new ResetFW.Builder(); - private final ChallengeFW.Builder challengeRW = new ChallengeFW.Builder(); - private final McpBeginExFW.Builder mcpBeginExRW = new McpBeginExFW.Builder(); - private final McpChallengeExFW.Builder mcpChallengeExRW = new McpChallengeExFW.Builder(); - - private final MutableDirectBuffer writeBuffer; - private final MutableDirectBuffer codecBuffer; - private final BindingHandler streamFactory; - private final BufferPool bufferPool; - private final LongUnaryOperator supplyInitialId; - private final LongUnaryOperator supplyReplyId; + private final McpConfiguration config; + private final EngineContext context; private final int mcpTypeId; private final Long2ObjectHashMap bindings; - private final Map sessions; - - private final JsonParserFactory toolsListItemParserFactory; - private final JsonParserFactory promptsListItemParserFactory; - private final JsonParserFactory resourcesListItemParserFactory; + private final Long2ObjectHashMap managers; + private final Int2ObjectHashMap factories; + private final Function supplyManager; public McpProxyFactory( McpConfiguration config, EngineContext context) { - this.writeBuffer = context.writeBuffer(); - this.codecBuffer = new UnsafeBuffer(new byte[context.writeBuffer().capacity()]); - this.streamFactory = context.streamFactory(); - this.bufferPool = context.bufferPool(); - this.supplyInitialId = context::supplyInitialId; - this.supplyReplyId = context::supplyReplyId; + this.config = config; + this.context = context; this.bindings = new Long2ObjectHashMap<>(); - this.sessions = new Object2ObjectHashMap<>(); + this.managers = new Long2ObjectHashMap<>(); + this.factories = new Int2ObjectHashMap<>(); + this.supplyManager = new McpProxyCacheManager.Factory(config, context)::create; + this.factories.put(KIND_LIFECYCLE, + new McpProxyLifecycleFactory(config, context, bindings::get)); + this.factories.put(KIND_TOOLS_CALL, + new McpProxyToolsCallFactory(config, context, bindings::get)); + this.factories.put(KIND_PROMPTS_GET, + new McpProxyPromptsGetFactory(config, context, bindings::get)); + this.factories.put(KIND_RESOURCES_READ, + new McpProxyResourcesReadFactory(config, context, bindings::get)); + this.factories.put(KIND_TOOLS_LIST, + new McpProxyToolsListFactory(config, context, bindings::get)); + this.factories.put(KIND_PROMPTS_LIST, + new McpProxyPromptsListFactory(config, context, bindings::get)); + this.factories.put(KIND_RESOURCES_LIST, + new McpProxyResourcesListFactory(config, context, bindings::get)); this.mcpTypeId = context.supplyTypeId(MCP_TYPE_NAME); - this.toolsListItemParserFactory = StreamingJson.createParserFactory( - Map.of(StreamingJson.PATH_INCLUDES, TOOLS_LIST_ITEM_JSON_PATH_INCLUDES)); - this.promptsListItemParserFactory = StreamingJson.createParserFactory( - Map.of(StreamingJson.PATH_INCLUDES, PROMPTS_LIST_ITEM_JSON_PATH_INCLUDES)); - this.resourcesListItemParserFactory = StreamingJson.createParserFactory( - Map.of(StreamingJson.PATH_INCLUDES, RESOURCES_LIST_ITEM_JSON_PATH_INCLUDES)); } @Override @@ -150,8 +93,14 @@ public int originTypeId() public void attach( BindingConfig binding) { - McpBindingConfig newBinding = new McpBindingConfig(binding); + McpBindingConfig newBinding = new McpBindingConfig(binding, config, context); bindings.put(binding.id, newBinding); + if (newBinding.cache != null) + { + McpProxyCacheManager manager = supplyManager.apply(newBinding.cache); + managers.put(binding.id, manager); + manager.start(); + } } @Override @@ -159,6 +108,12 @@ public void detach( long bindingId) { bindings.remove(bindingId); + McpProxyCacheManager manager = managers.remove(bindingId); + + if (manager != null) + { + manager.stop(); + } } @Override @@ -170,2903 +125,21 @@ public MessageConsumer newStream( MessageConsumer sender) { final BeginFW begin = beginRO.wrap(buffer, index, index + length); - final long originId = begin.originId(); - final long routedId = begin.routedId(); - final long initialId = begin.streamId(); - final long affinity = begin.affinity(); - final long authorization = begin.authorization(); final OctetsFW extension = begin.extension(); - final McpBindingConfig binding = bindings.get(routedId); - MessageConsumer newStream = null; final McpBeginExFW beginEx = extension.get(mcpBeginExRO::tryWrap); - if (binding != null && beginEx != null) + if (beginEx != null) { - final int kind = beginEx.kind(); - final String sessionId = sessionId(beginEx); - - if (kind == KIND_LIFECYCLE) - { - final McpRouteConfig route = binding.resolve(beginEx, authorization); - if (route != null) - { - final int clientCapabilities = beginEx.lifecycle().capabilities(); - final McpLifecycleServer lifecycle = new McpLifecycleServer( - sender, originId, routedId, initialId, affinity, authorization, - clientCapabilities, sessionId); - sessions.put(sessionId, lifecycle); - newStream = lifecycle::onServerMessage; - } - } - else + final BindingHandler factory = factories.get(beginEx.kind()); + if (factory != null) { - final McpLifecycleServer lifecycle = sessions.get(sessionId); - if (lifecycle != null) - { - if (isListKind(kind)) - { - final List prefixes = binding.resolveAll(beginEx, authorization) - .stream() - .map(r -> new McpRoutePrefix(r.id, r.prefix(kind))) - .toList(); - newStream = new McpListServer( - lifecycle, - kind, - initialId, - affinity, - authorization, - prefixes)::onServerMessage; - } - else - { - final McpRouteConfig route = binding.resolve(beginEx, authorization); - if (route != null) - { - final String identifier = route.strip(beginEx); - final String prefix = route.prefix(beginEx); - - newStream = new McpServer( - lifecycle, - kind, - sender, - originId, - routedId, - initialId, - route.id, - affinity, - authorization, - identifier, - prefix)::onServerMessage; - } - } - } + newStream = factory.newStream(msgTypeId, buffer, index, length, sender); } } return newStream; } - - private final class McpServer - { - private final McpLifecycleServer lifecycle; - private final int kind; - private final MessageConsumer sender; - private final long originId; - private final long routedId; - private final long initialId; - private final long replyId; - private final long affinity; - private final long authorization; - private final String identifier; - private final String prefix; - private final McpClient client; - - private int state; - - private long initialSeq; - private long initialAck; - private int initialMax; - private int initialPad; - - private long replySeq; - private long replyAck; - private int replyMax; - private int replyPad; - - private McpServer( - McpLifecycleServer lifecycle, - int kind, - MessageConsumer sender, - long originId, - long routedId, - long initialId, - long resolvedId, - long affinity, - long authorization, - String identifier, - String prefix) - { - this.lifecycle = lifecycle; - this.kind = kind; - this.sender = sender; - this.originId = originId; - this.routedId = routedId; - this.initialId = initialId; - this.replyId = supplyReplyId.applyAsLong(initialId); - this.affinity = affinity; - this.authorization = authorization; - this.identifier = identifier; - this.prefix = prefix; - this.client = new McpClient(this, resolvedId); - } - - private String sessionId() - { - return lifecycle.sessionId; - } - - private void onServerMessage( - int msgTypeId, - DirectBuffer buffer, - int index, - int length) - { - switch (msgTypeId) - { - case BeginFW.TYPE_ID: - final BeginFW begin = beginRO.wrap(buffer, index, index + length); - onServerBegin(begin); - break; - case DataFW.TYPE_ID: - final DataFW data = dataRO.wrap(buffer, index, index + length); - onServerData(data); - break; - case EndFW.TYPE_ID: - final EndFW end = endRO.wrap(buffer, index, index + length); - onServerEnd(end); - break; - case AbortFW.TYPE_ID: - final AbortFW abort = abortRO.wrap(buffer, index, index + length); - onServerAbort(abort); - break; - case FlushFW.TYPE_ID: - final FlushFW flush = flushRO.wrap(buffer, index, index + length); - onServerFlush(flush); - break; - case WindowFW.TYPE_ID: - final WindowFW window = windowRO.wrap(buffer, index, index + length); - onServerWindow(window); - break; - case ResetFW.TYPE_ID: - final ResetFW reset = resetRO.wrap(buffer, index, index + length); - onServerReset(reset); - break; - case ChallengeFW.TYPE_ID: - final ChallengeFW challenge = challengeRO.wrap(buffer, index, index + length); - onServerChallenge(challenge); - break; - default: - break; - } - } - - private void onServerChallenge( - ChallengeFW challenge) - { - client.doClientChallenge(challenge.traceId(), challenge.authorization(), challenge.extension()); - } - - private void onServerBegin( - BeginFW begin) - { - final long sequence = begin.sequence(); - final long acknowledge = begin.acknowledge(); - final long traceId = begin.traceId(); - - initialSeq = sequence; - initialAck = acknowledge; - - state = McpState.openingInitial(state); - - client.doClientBegin(traceId); - - flushServerWindow(traceId, 0L, 0, 0L, 0); - } - - private void onServerData( - DataFW data) - { - final long sequence = data.sequence(); - final long acknowledge = data.acknowledge(); - final long traceId = data.traceId(); - final long budgetId = data.budgetId(); - final int flags = data.flags(); - final int reserved = data.reserved(); - final OctetsFW payload = data.payload(); - - assert acknowledge <= sequence; - assert sequence >= initialSeq; - assert acknowledge <= initialAck; - - initialSeq = sequence + reserved; - - assert initialAck <= initialSeq; - - client.doClientData(traceId, budgetId, flags, reserved, - payload.buffer(), payload.offset(), payload.sizeof()); - } - - private void onServerEnd( - EndFW end) - { - final long sequence = end.sequence(); - final long acknowledge = end.acknowledge(); - final long traceId = end.traceId(); - - assert acknowledge <= sequence; - assert sequence >= initialSeq; - assert acknowledge <= initialAck; - - initialSeq = sequence; - - assert initialAck <= initialSeq; - - state = McpState.closedInitial(state); - - client.doClientEnd(traceId); - } - - private void onServerAbort( - AbortFW abort) - { - final long sequence = abort.sequence(); - final long acknowledge = abort.acknowledge(); - final long traceId = abort.traceId(); - - assert acknowledge <= sequence; - assert sequence >= initialSeq; - assert acknowledge <= initialAck; - - initialSeq = sequence; - - assert initialAck <= initialSeq; - - state = McpState.closedInitial(state); - - client.doClientAbort(traceId); - } - - private void onServerFlush( - FlushFW flush) - { - client.doClientFlush(flush.traceId(), flush.authorization(), - flush.budgetId(), flush.reserved(), flush.extension()); - } - - private void onServerWindow( - WindowFW window) - { - final long sequence = window.sequence(); - final long acknowledge = window.acknowledge(); - final int maximum = window.maximum(); - final long traceId = window.traceId(); - final long budgetId = window.budgetId(); - final int padding = window.padding(); - - assert acknowledge <= sequence; - assert sequence <= replySeq; - assert acknowledge >= replyAck; - assert maximum + acknowledge >= replyMax + replyAck; - - replyAck = acknowledge; - replyMax = maximum; - replyPad = padding; - - assert replyAck <= replySeq; - - client.flushClientWindow(traceId, budgetId, padding, replySeq - replyAck, replyMax); - } - - private void onServerReset( - ResetFW reset) - { - final long sequence = reset.sequence(); - final long acknowledge = reset.acknowledge(); - final long traceId = reset.traceId(); - - assert acknowledge <= sequence; - assert sequence <= replySeq; - assert acknowledge >= replyAck; - - replyAck = acknowledge; - - assert replyAck <= replySeq; - - state = McpState.closedReply(state); - - client.doClientReset(traceId); - } - - private void doServerBegin( - long traceId, - Flyweight extension) - { - doBegin(sender, originId, routedId, replyId, replySeq, replyAck, replyMax, traceId, authorization, - affinity, extension); - state = McpState.openedReply(state); - } - - private void doServerData( - long traceId, - long budgetId, - int flags, - int reserved, - DirectBuffer payload, - int offset, - int length) - { - doData(sender, originId, routedId, replyId, replySeq, replyAck, replyMax, traceId, authorization, - flags, budgetId, reserved, payload, offset, length); - replySeq += reserved; - } - - private void doServerEnd( - long traceId) - { - if (!McpState.replyClosed(state)) - { - doEnd(sender, originId, routedId, replyId, replySeq, replyAck, replyMax, traceId, authorization); - state = McpState.closedReply(state); - } - } - - private void doServerAbort( - long traceId) - { - if (!McpState.replyClosed(state)) - { - doAbort(sender, originId, routedId, replyId, replySeq, replyAck, replyMax, traceId, authorization); - state = McpState.closedReply(state); - } - } - - private void doServerFlush( - long traceId, - long authorization, - long budgetId, - int reserved, - OctetsFW extension) - { - doFlush(sender, originId, routedId, replyId, replySeq, replyAck, replyMax, - traceId, authorization, budgetId, reserved, extension); - } - - private void doServerChallenge( - long traceId, - long authorization, - OctetsFW extension) - { - doChallenge(sender, originId, routedId, initialId, initialSeq, initialAck, initialMax, - traceId, authorization, extension); - } - - private void doServerWindow( - long traceId, - long budgetId, - int padding) - { - state = McpState.openedInitial(state); - doWindow(sender, originId, routedId, initialId, initialSeq, initialAck, initialMax, traceId, authorization, - budgetId, padding); - } - - private void flushServerWindow( - long traceId, - long budgetId, - int padding, - long minInitialNoAck, - int minInitialMax) - { - final long newInitialAck = Math.max(initialAck, initialSeq - minInitialNoAck); - final int newInitialMax = Math.max(initialMax, minInitialMax); - - if (newInitialAck > initialAck || newInitialMax > initialMax || !McpState.initialOpened(state)) - { - initialAck = newInitialAck; - initialMax = newInitialMax; - doServerWindow(traceId, budgetId, padding); - } - } - - private void doServerReset( - long traceId) - { - if (!McpState.initialClosed(state)) - { - doReset(sender, originId, routedId, initialId, initialSeq, initialAck, initialMax, traceId, authorization, - emptyRO); - state = McpState.closedInitial(state); - } - } - } - - private final class McpClient - { - private final McpServer server; - private final long resolvedId; - private final McpLifecycleClient lifecycle; - - private final long initialId; - private final long replyId; - - private MessageConsumer sender; - private int state; - - private long initialSeq; - private long initialAck; - private int initialMax; - private int initialPad; - - private long replySeq; - private long replyAck; - private int replyMax; - private int replyPad; - - private McpClient( - McpServer server, - long resolvedId) - { - this.server = server; - this.resolvedId = resolvedId; - this.lifecycle = server.lifecycle.supplyClient(resolvedId); - this.initialId = supplyInitialId.applyAsLong(resolvedId); - this.replyId = supplyReplyId.applyAsLong(initialId); - } - - private void doClientBegin( - long traceId) - { - lifecycle.doClientBegin(traceId); - - final String identifier = server.identifier; - final String upstreamSessionId = lifecycle.sessionId; - final String outboundSessionId = upstreamSessionId != null - ? upstreamSessionId - : server.sessionId(); - final McpBeginExFW beginEx = mcpBeginExRW - .wrap(codecBuffer, 0, codecBuffer.capacity()) - .typeId(mcpTypeId) - .inject(b -> - { - switch (server.kind) - { - case KIND_TOOLS_CALL -> b.toolsCall(t -> t.sessionId(outboundSessionId).name(identifier)); - case KIND_PROMPTS_GET -> b.promptsGet(p -> p.sessionId(outboundSessionId).name(identifier)); - case KIND_RESOURCES_READ -> b.resourcesRead(r -> r.sessionId(outboundSessionId).uri(identifier)); - default -> throw new IllegalStateException("unexpected McpBeginEx kind: " + server.kind); - } - }) - .build(); - - sender = newStream(this::onClientMessage, server.lifecycle.originId, resolvedId, initialId, - initialSeq, initialAck, initialMax, traceId, server.authorization, server.affinity, beginEx); - state = McpState.openingInitial(state); - } - - private void doClientData( - long traceId, - long budgetId, - int flags, - int reserved, - DirectBuffer payload, - int offset, - int length) - { - if (!McpState.closed(state)) - { - doData(sender, server.lifecycle.originId, resolvedId, initialId, - initialSeq, initialAck, initialMax, traceId, server.authorization, - flags, budgetId, reserved, payload, offset, length); - initialSeq += reserved; - } - } - - private void doClientFlush( - long traceId, - long authorization, - long budgetId, - int reserved, - OctetsFW extension) - { - doFlush(sender, server.lifecycle.originId, resolvedId, initialId, - initialSeq, initialAck, initialMax, - traceId, authorization, budgetId, reserved, extension); - } - - private void doClientEnd( - long traceId) - { - if (!McpState.initialClosed(state)) - { - doEnd(sender, server.lifecycle.originId, resolvedId, initialId, - initialSeq, initialAck, initialMax, traceId, server.authorization); - state = McpState.closedInitial(state); - } - } - - private void doClientAbort( - long traceId) - { - if (!McpState.initialClosed(state)) - { - doAbort(sender, server.lifecycle.originId, resolvedId, initialId, - initialSeq, initialAck, initialMax, traceId, server.authorization); - state = McpState.closedInitial(state); - } - } - - private void doClientWindow( - long traceId, - long budgetId, - int padding) - { - state = McpState.openedReply(state); - doWindow(sender, server.lifecycle.originId, resolvedId, replyId, - replySeq, replyAck, replyMax, traceId, server.authorization, budgetId, padding); - } - - private void flushClientWindow( - long traceId, - long budgetId, - int padding, - long minReplyNoAck, - int minReplyMax) - { - final long newReplyAck = Math.max(replyAck, replySeq - minReplyNoAck); - final int newReplyMax = Math.max(replyMax, minReplyMax); - - if (newReplyAck > replyAck || newReplyMax > replyMax || !McpState.replyOpened(state)) - { - replyAck = newReplyAck; - replyMax = newReplyMax; - doClientWindow(traceId, budgetId, padding); - } - } - - private void doClientReset( - long traceId) - { - if (!McpState.replyClosed(state)) - { - doReset(sender, server.lifecycle.originId, resolvedId, replyId, - replySeq, replyAck, replyMax, traceId, server.authorization, emptyRO); - state = McpState.closedReply(state); - } - } - - private void doClientChallenge( - long traceId, - long authorization, - Flyweight extension) - { - doChallenge(sender, server.lifecycle.originId, resolvedId, replyId, - replySeq, replyAck, replyMax, traceId, authorization, extension); - } - - private void onClientMessage( - int msgTypeId, - DirectBuffer buffer, - int index, - int length) - { - switch (msgTypeId) - { - case BeginFW.TYPE_ID: - final BeginFW begin = beginRO.wrap(buffer, index, index + length); - onClientBegin(begin); - break; - case DataFW.TYPE_ID: - final DataFW data = dataRO.wrap(buffer, index, index + length); - onClientData(data); - break; - case EndFW.TYPE_ID: - final EndFW end = endRO.wrap(buffer, index, index + length); - onClientEnd(end); - break; - case AbortFW.TYPE_ID: - final AbortFW abort = abortRO.wrap(buffer, index, index + length); - onClientAbort(abort); - break; - case FlushFW.TYPE_ID: - final FlushFW flush = flushRO.wrap(buffer, index, index + length); - onClientFlush(flush); - break; - case WindowFW.TYPE_ID: - final WindowFW window = windowRO.wrap(buffer, index, index + length); - onClientWindow(window); - break; - case ResetFW.TYPE_ID: - final ResetFW reset = resetRO.wrap(buffer, index, index + length); - onClientReset(reset); - break; - case ChallengeFW.TYPE_ID: - final ChallengeFW challenge = challengeRO.wrap(buffer, index, index + length); - onClientChallenge(challenge); - break; - default: - break; - } - } - - private void onClientFlush( - FlushFW flush) - { - server.doServerFlush(flush.traceId(), flush.authorization(), - flush.budgetId(), flush.reserved(), flush.extension()); - } - - private void onClientChallenge( - ChallengeFW challenge) - { - server.doServerChallenge(challenge.traceId(), challenge.authorization(), challenge.extension()); - } - - private void onClientBegin( - BeginFW begin) - { - final long sequence = begin.sequence(); - final long acknowledge = begin.acknowledge(); - final long traceId = begin.traceId(); - final OctetsFW extension = begin.extension(); - - replySeq = sequence; - replyAck = acknowledge; - - state = McpState.openedInitial(state); - - final McpBeginExFW beginEx = extension.get(mcpBeginExRO::tryWrap); - final Flyweight replyExtension = beginEx != null - ? rewriteReplyBeginEx(beginEx) - : emptyRO; - - server.doServerBegin(traceId, replyExtension); - - flushClientWindow(traceId, 0L, 0, 0L, 0); - } - - private Flyweight rewriteReplyBeginEx( - McpBeginExFW beginEx) - { - final int kind = beginEx.kind(); - if (kind != KIND_TOOLS_CALL && kind != KIND_PROMPTS_GET && kind != KIND_RESOURCES_READ) - { - return beginEx; - } - - final String sid = server.sessionId(); - return mcpBeginExRW - .wrap(codecBuffer, 0, codecBuffer.capacity()) - .typeId(mcpTypeId) - .inject(b -> - { - switch (kind) - { - case KIND_TOOLS_CALL -> - b.toolsCall(t -> t.sessionId(sid).name(beginEx.toolsCall().name().asString())); - case KIND_PROMPTS_GET -> - b.promptsGet(p -> p.sessionId(sid).name(beginEx.promptsGet().name().asString())); - case KIND_RESOURCES_READ -> - b.resourcesRead(r -> r.sessionId(sid).uri(beginEx.resourcesRead().uri().asString())); - default -> throw new IllegalStateException("unexpected McpBeginEx kind: " + kind); - } - }) - .build(); - } - - private void onClientData( - DataFW data) - { - final long sequence = data.sequence(); - final long acknowledge = data.acknowledge(); - final long traceId = data.traceId(); - final long budgetId = data.budgetId(); - final int flags = data.flags(); - final int reserved = data.reserved(); - final OctetsFW payload = data.payload(); - - assert acknowledge <= sequence; - assert sequence >= replySeq; - assert acknowledge <= replyAck; - - replySeq = sequence + reserved; - - assert replyAck <= replySeq; - - server.doServerData(traceId, budgetId, flags, reserved, - payload.buffer(), payload.offset(), payload.sizeof()); - } - - private void onClientEnd( - EndFW end) - { - final long sequence = end.sequence(); - final long acknowledge = end.acknowledge(); - final long traceId = end.traceId(); - - assert acknowledge <= sequence; - assert sequence >= replySeq; - assert acknowledge <= replyAck; - - replySeq = sequence; - - assert replyAck <= replySeq; - - state = McpState.closedReply(state); - server.doServerEnd(traceId); - } - - private void onClientAbort( - AbortFW abort) - { - final long sequence = abort.sequence(); - final long acknowledge = abort.acknowledge(); - final long traceId = abort.traceId(); - - assert acknowledge <= sequence; - assert sequence >= replySeq; - assert acknowledge <= replyAck; - - replySeq = sequence; - - assert replyAck <= replySeq; - - state = McpState.closedReply(state); - server.doServerAbort(traceId); - } - - private void onClientWindow( - WindowFW window) - { - final long sequence = window.sequence(); - final long acknowledge = window.acknowledge(); - final int maximum = window.maximum(); - final long traceId = window.traceId(); - final long budgetId = window.budgetId(); - final int padding = window.padding(); - - assert acknowledge <= sequence; - assert sequence <= initialSeq; - assert acknowledge >= initialAck; - assert maximum + acknowledge >= initialMax + initialAck; - - initialAck = acknowledge; - initialMax = maximum; - initialPad = padding; - - assert initialAck <= initialSeq; - - server.flushServerWindow(traceId, budgetId, padding, initialSeq - initialAck, initialMax); - } - - private void onClientReset( - ResetFW reset) - { - final long sequence = reset.sequence(); - final long acknowledge = reset.acknowledge(); - final long traceId = reset.traceId(); - - assert acknowledge <= sequence; - assert sequence <= initialSeq; - assert acknowledge >= initialAck; - - initialAck = acknowledge; - - assert initialAck <= initialSeq; - - state = McpState.closedInitial(state); - - server.doServerReset(traceId); - } - } - - private final class McpLifecycleServer - { - private final MessageConsumer sender; - private final long originId; - private final long routedId; - private final long initialId; - private final long replyId; - private final long affinity; - private final long authorization; - private final int clientCapabilities; - private final String sessionId; - private final Long2ObjectHashMap clients; - - private int state; - private boolean resumePending; - - private long initialSeq; - private long initialAck; - private int initialMax; - private int initialPad; - - private long replySeq; - private long replyAck; - private int replyMax; - private int replyPad; - - private McpLifecycleServer( - MessageConsumer sender, - long originId, - long routedId, - long initialId, - long affinity, - long authorization, - int clientCapabilities, - String sessionId) - { - this.sender = sender; - this.originId = originId; - this.routedId = routedId; - this.initialId = initialId; - this.replyId = supplyReplyId.applyAsLong(initialId); - this.affinity = affinity; - this.authorization = authorization; - this.clientCapabilities = clientCapabilities; - this.sessionId = sessionId; - this.clients = new Long2ObjectHashMap<>(); - } - - private McpLifecycleClient supplyClient( - long routedId) - { - return clients.computeIfAbsent(routedId, id -> new McpLifecycleClient(this, id)); - } - - private void onServerMessage( - int msgTypeId, - DirectBuffer buffer, - int index, - int length) - { - switch (msgTypeId) - { - case BeginFW.TYPE_ID: - final BeginFW begin = beginRO.wrap(buffer, index, index + length); - onServerBegin(begin); - break; - case DataFW.TYPE_ID: - // no-op: proxy terminates lifecycle locally, no DATA is forwarded - break; - case EndFW.TYPE_ID: - final EndFW end = endRO.wrap(buffer, index, index + length); - onServerEnd(end); - break; - case AbortFW.TYPE_ID: - final AbortFW abort = abortRO.wrap(buffer, index, index + length); - onServerAbort(abort); - break; - case WindowFW.TYPE_ID: - final WindowFW window = windowRO.wrap(buffer, index, index + length); - onServerWindow(window); - break; - case ResetFW.TYPE_ID: - final ResetFW reset = resetRO.wrap(buffer, index, index + length); - onServerReset(reset); - break; - case ChallengeFW.TYPE_ID: - final ChallengeFW challenge = challengeRO.wrap(buffer, index, index + length); - onServerChallenge(challenge); - break; - default: - break; - } - } - - private void onServerChallenge( - ChallengeFW challenge) - { - resumePending = true; - - final long traceId = challenge.traceId(); - final long authorization = challenge.authorization(); - for (McpLifecycleClient client : clients.values()) - { - client.doClientResume(traceId, authorization); - } - } - - private void onServerBegin( - BeginFW begin) - { - final long sequence = begin.sequence(); - final long acknowledge = begin.acknowledge(); - final long traceId = begin.traceId(); - - initialSeq = sequence; - initialAck = acknowledge; - - state = McpState.openingInitial(state); - - final McpBindingConfig binding = bindings.get(routedId); - final int serverCapabilities = binding.serverCapabilities(authorization); - final String sid = sessionId; - final McpBeginExFW beginEx = mcpBeginExRW - .wrap(codecBuffer, 0, codecBuffer.capacity()) - .typeId(mcpTypeId) - .lifecycle(l -> l.sessionId(sid).capabilities(serverCapabilities)) - .build(); - - doServerBegin(traceId, beginEx); - - doServerWindow(traceId, 0L, 0); - } - - private void onServerEnd( - EndFW end) - { - final long sequence = end.sequence(); - final long acknowledge = end.acknowledge(); - final long traceId = end.traceId(); - - assert acknowledge <= sequence; - assert sequence >= initialSeq; - assert acknowledge <= initialAck; - - initialSeq = sequence; - - assert initialAck <= initialSeq; - - state = McpState.closedInitial(state); - - cleanup(traceId); - - doServerEnd(traceId); - } - - private void onServerAbort( - AbortFW abort) - { - final long sequence = abort.sequence(); - final long acknowledge = abort.acknowledge(); - final long traceId = abort.traceId(); - - assert acknowledge <= sequence; - assert sequence >= initialSeq; - assert acknowledge <= initialAck; - - initialSeq = sequence; - - assert initialAck <= initialSeq; - - state = McpState.closedInitial(state); - - cleanup(traceId); - - doServerAbort(traceId); - } - - private void onServerWindow( - WindowFW window) - { - final long sequence = window.sequence(); - final long acknowledge = window.acknowledge(); - final int maximum = window.maximum(); - final int padding = window.padding(); - - assert acknowledge <= sequence; - assert sequence <= replySeq; - assert acknowledge >= replyAck; - assert maximum + acknowledge >= replyMax + replyAck; - - replyAck = acknowledge; - replyMax = maximum; - replyPad = padding; - - assert replyAck <= replySeq; - } - - private void onServerReset( - ResetFW reset) - { - final long sequence = reset.sequence(); - final long acknowledge = reset.acknowledge(); - final long traceId = reset.traceId(); - - assert acknowledge <= sequence; - assert sequence <= replySeq; - assert acknowledge >= replyAck; - - replyAck = acknowledge; - - assert replyAck <= replySeq; - - state = McpState.closedReply(state); - - cleanup(traceId); - } - - private void doServerBegin( - long traceId, - Flyweight extension) - { - doBegin(sender, originId, routedId, replyId, replySeq, replyAck, replyMax, traceId, authorization, - affinity, extension); - state = McpState.openedReply(state); - } - - private void doServerEnd( - long traceId) - { - if (!McpState.replyClosed(state)) - { - doEnd(sender, originId, routedId, replyId, replySeq, replyAck, replyMax, traceId, authorization); - state = McpState.closedReply(state); - } - } - - private void doServerAbort( - long traceId) - { - if (!McpState.replyClosed(state)) - { - doAbort(sender, originId, routedId, replyId, replySeq, replyAck, replyMax, traceId, authorization); - state = McpState.closedReply(state); - } - } - - private void doServerFlush( - long traceId, - long authorization, - long budgetId, - int reserved, - OctetsFW extension) - { - doFlush(sender, originId, routedId, replyId, replySeq, replyAck, replyMax, - traceId, authorization, budgetId, reserved, extension); - } - - private void doServerWindow( - long traceId, - long budgetId, - int padding) - { - doWindow(sender, originId, routedId, initialId, initialSeq, initialAck, initialMax, traceId, authorization, - budgetId, padding); - } - - private void cleanup( - long traceId) - { - sessions.remove(sessionId); - - for (McpLifecycleClient upstream : clients.values()) - { - upstream.doClientEnd(traceId); - } - } - } - - private final class McpLifecycleClient - { - private final McpLifecycleServer server; - private final long routedId; - private final long initialId; - private final long replyId; - - private MessageConsumer sender; - private int state; - private String sessionId; // upstream-provided session id, set on BEGIN reply - - private long initialSeq; - private long initialAck; - private int initialMax; - private int initialPad; - - private long replySeq; - private long replyAck; - private int replyMax; - private int replyPad; - - private McpLifecycleClient( - McpLifecycleServer server, - long routedId) - { - this.server = server; - this.routedId = routedId; - this.initialId = supplyInitialId.applyAsLong(routedId); - this.replyId = supplyReplyId.applyAsLong(initialId); - } - - private void doClientBegin( - long traceId) - { - if (!McpState.initialOpening(state)) - { - final long originId = server.routedId; - final String sid = server.sessionId; - final int clientCapabilities = server.clientCapabilities; - final McpBeginExFW beginEx = mcpBeginExRW - .wrap(codecBuffer, 0, codecBuffer.capacity()) - .typeId(mcpTypeId) - .lifecycle(l -> l.sessionId(sid).capabilities(clientCapabilities)) - .build(); - - sender = newStream(this::onClientMessage, originId, routedId, initialId, - initialSeq, initialAck, initialMax, traceId, server.authorization, server.affinity, beginEx); - state = McpState.openingInitial(state); - } - } - - private void doClientEnd( - long traceId) - { - if (!McpState.initialClosed(state)) - { - final long originId = server.routedId; - doEnd(sender, originId, routedId, initialId, initialSeq, initialAck, initialMax, traceId, - server.authorization); - state = McpState.closedInitial(state); - } - } - - private void doClientAbort( - long traceId) - { - if (!McpState.initialClosed(state)) - { - final long originId = server.routedId; - doAbort(sender, originId, routedId, initialId, initialSeq, initialAck, initialMax, traceId, - server.authorization); - state = McpState.closedInitial(state); - } - } - - private void doClientReset( - long traceId) - { - if (!McpState.replyClosed(state)) - { - final long originId = server.routedId; - doReset(sender, originId, routedId, replyId, replySeq, replyAck, replyMax, traceId, - server.authorization, emptyRO); - state = McpState.closedReply(state); - } - } - - private void doClientChallenge( - long traceId, - long authorization, - Flyweight extension) - { - final long originId = server.routedId; - doChallenge(sender, originId, routedId, replyId, replySeq, replyAck, replyMax, - traceId, authorization, extension); - } - - private void doClientResume( - long traceId, - long authorization) - { - final McpChallengeExFW resumeEx = mcpChallengeExRW - .wrap(codecBuffer, 0, codecBuffer.capacity()) - .typeId(mcpTypeId) - .resume(b -> {}) - .build(); - doClientChallenge(traceId, authorization, resumeEx); - } - - private void doClientWindow( - long traceId, - long budgetId, - int padding) - { - final long originId = server.routedId; - doWindow(sender, originId, routedId, replyId, replySeq, replyAck, replyMax, traceId, server.authorization, - budgetId, padding); - } - - private void onClientMessage( - int msgTypeId, - DirectBuffer buffer, - int index, - int length) - { - switch (msgTypeId) - { - case BeginFW.TYPE_ID: - final BeginFW begin = beginRO.wrap(buffer, index, index + length); - onClientBegin(begin); - break; - case DataFW.TYPE_ID: - // lifecycle does not carry DATA in this proxy model - break; - case EndFW.TYPE_ID: - final EndFW end = endRO.wrap(buffer, index, index + length); - onClientEnd(end); - break; - case AbortFW.TYPE_ID: - final AbortFW abort = abortRO.wrap(buffer, index, index + length); - onClientAbort(abort); - break; - case FlushFW.TYPE_ID: - final FlushFW flush = flushRO.wrap(buffer, index, index + length); - onClientFlush(flush); - break; - case WindowFW.TYPE_ID: - final WindowFW window = windowRO.wrap(buffer, index, index + length); - onClientWindow(window); - break; - case ResetFW.TYPE_ID: - final ResetFW reset = resetRO.wrap(buffer, index, index + length); - onClientReset(reset); - break; - default: - break; - } - } - - private void onClientFlush( - FlushFW flush) - { - server.doServerFlush(flush.traceId(), flush.authorization(), - flush.budgetId(), flush.reserved(), flush.extension()); - } - - private void onClientBegin( - BeginFW begin) - { - final long sequence = begin.sequence(); - final long acknowledge = begin.acknowledge(); - final long traceId = begin.traceId(); - final long authorization = begin.authorization(); - final OctetsFW extension = begin.extension(); - - replySeq = sequence; - replyAck = acknowledge; - - state = McpState.openedInitial(state); - - final McpBeginExFW beginEx = extension.get(mcpBeginExRO::tryWrap); - if (beginEx != null && beginEx.kind() == KIND_LIFECYCLE) - { - sessionId = beginEx.lifecycle().sessionId().asString(); - } - - doClientWindow(traceId, 0L, 0); - - state = McpState.openedReply(state); - - if (server.resumePending) - { - doClientResume(traceId, authorization); - } - } - - private void onClientEnd( - EndFW end) - { - final long sequence = end.sequence(); - final long acknowledge = end.acknowledge(); - final long traceId = end.traceId(); - - assert acknowledge <= sequence; - assert sequence >= replySeq; - assert acknowledge <= replyAck; - - replySeq = sequence; - - assert replyAck <= replySeq; - - state = McpState.closedReply(state); - doClientEnd(traceId); - server.clients.remove(routedId, this); - } - - private void onClientAbort( - AbortFW abort) - { - final long sequence = abort.sequence(); - final long acknowledge = abort.acknowledge(); - final long traceId = abort.traceId(); - - assert acknowledge <= sequence; - assert sequence >= replySeq; - assert acknowledge <= replyAck; - - replySeq = sequence; - - assert replyAck <= replySeq; - - state = McpState.closedReply(state); - doClientAbort(traceId); - server.clients.remove(routedId, this); - } - - private void onClientWindow( - WindowFW window) - { - final long sequence = window.sequence(); - final long acknowledge = window.acknowledge(); - final int maximum = window.maximum(); - final int padding = window.padding(); - - assert acknowledge <= sequence; - assert sequence <= initialSeq; - assert acknowledge >= initialAck; - assert maximum + acknowledge >= initialMax + initialAck; - - initialAck = acknowledge; - initialMax = maximum; - initialPad = padding; - - assert initialAck <= initialSeq; - } - - private void onClientReset( - ResetFW reset) - { - final long sequence = reset.sequence(); - final long acknowledge = reset.acknowledge(); - final long traceId = reset.traceId(); - - assert acknowledge <= sequence; - assert sequence <= initialSeq; - assert acknowledge >= initialAck; - - initialAck = acknowledge; - - assert initialAck <= initialSeq; - - state = McpState.closedInitial(state); - doClientReset(traceId); - server.clients.remove(routedId, this); - } - } - - private final class McpListClient - { - private final McpListServer server; - private final long resolvedId; - private final String prefix; - private final byte[] prefixBytes; - private final DirectBuffer prefixBufferRO; - private final McpLifecycleClient lifecycle; - private final long initialId; - private final long replyId; - - private MessageConsumer sender; - private int state; - private int replySlot = NO_SLOT; - private int replySlotOffset; - - private long initialSeq; - private long initialAck; - private int initialMax; - private int initialPad; - - private long replySeq; - private long replyAck; - private int replyMax; - private int replyPad; - - private JsonParser decodableJson; - private long decodedParserProgress; // absolute streamOffset of buffer[offset] passed to decode - private int decodeDepth; // JSON nesting depth in the reply envelope - private int decodeItemDepth; // JSON nesting depth within the current item - private int decodeSkipDepth; // JSON nesting depth within a skipped value - private long decodedItemProgress = -1; // streamOffset of last byte emitted within the current item, -1 between items - private McpListClientDecoder decoder = decodeInit; - private String arrayKey; - private String idKey; - - private McpListClient( - McpListServer server, - long resolvedId, - String prefix) - { - this.server = server; - this.resolvedId = resolvedId; - this.prefix = prefix; - this.prefixBytes = prefix.getBytes(StandardCharsets.UTF_8); - this.prefixBufferRO = new UnsafeBuffer(prefixBytes); - this.lifecycle = server.lifecycle.supplyClient(resolvedId); - this.initialId = supplyInitialId.applyAsLong(resolvedId); - this.replyId = supplyReplyId.applyAsLong(initialId); - } - - private void doClientBegin( - long traceId) - { - lifecycle.doClientBegin(traceId); - - final String upstreamSessionId = lifecycle.sessionId; - final String sid = upstreamSessionId != null ? upstreamSessionId : server.lifecycle.sessionId; - final McpBeginExFW beginEx = mcpBeginExRW - .wrap(codecBuffer, 0, codecBuffer.capacity()) - .typeId(mcpTypeId) - .inject(b -> - { - switch (server.kind) - { - case KIND_TOOLS_LIST -> b.toolsList(t -> t.sessionId(sid)); - case KIND_PROMPTS_LIST -> b.promptsList(p -> p.sessionId(sid)); - case KIND_RESOURCES_LIST -> b.resourcesList(r -> r.sessionId(sid)); - default -> throw new IllegalStateException("unexpected list kind: " + server.kind); - } - }) - .build(); - - sender = newStream(this::onClientMessage, server.lifecycle.originId, resolvedId, initialId, - initialSeq, initialAck, initialMax, traceId, server.authorization, server.affinity, beginEx); - state = McpState.openingInitial(state); - } - - private void doClientEnd( - long traceId) - { - if (!McpState.initialClosed(state)) - { - doEnd(sender, server.lifecycle.originId, resolvedId, initialId, - initialSeq, initialAck, initialMax, traceId, server.authorization); - state = McpState.closedInitial(state); - } - } - - private void doClientAbort( - long traceId) - { - if (!McpState.initialClosed(state)) - { - doAbort(sender, server.lifecycle.originId, resolvedId, initialId, - initialSeq, initialAck, initialMax, traceId, server.authorization); - state = McpState.closedInitial(state); - } - } - - private void doClientReset( - long traceId) - { - if (!McpState.replyClosed(state)) - { - doReset(sender, server.lifecycle.originId, resolvedId, replyId, - replySeq, replyAck, replyMax, traceId, server.authorization, emptyRO); - state = McpState.closedReply(state); - } - } - - private void doClientWindow( - long traceId, - long budgetId, - int padding) - { - state = McpState.openedReply(state); - doWindow(sender, server.lifecycle.originId, resolvedId, replyId, - replySeq, replyAck, replyMax, traceId, server.authorization, budgetId, padding); - } - - private void flushClientWindow( - long traceId, - long budgetId, - int padding, - long minReplyNoAck, - int minReplyMax) - { - final long newReplyAck = Math.max(replyAck, replySeq - minReplyNoAck); - final int newReplyMax = Math.max(replyMax, minReplyMax); - - if (newReplyAck > replyAck || newReplyMax > replyMax || !McpState.replyOpened(state)) - { - replyAck = newReplyAck; - replyMax = newReplyMax; - doClientWindow(traceId, budgetId, padding); - } - } - - private void onClientMessage( - int msgTypeId, - DirectBuffer buffer, - int index, - int length) - { - switch (msgTypeId) - { - case BeginFW.TYPE_ID: - final BeginFW begin = beginRO.wrap(buffer, index, index + length); - onClientBegin(begin); - break; - case DataFW.TYPE_ID: - final DataFW data = dataRO.wrap(buffer, index, index + length); - onClientData(data); - break; - case EndFW.TYPE_ID: - final EndFW end = endRO.wrap(buffer, index, index + length); - onClientEnd(end); - break; - case AbortFW.TYPE_ID: - final AbortFW abort = abortRO.wrap(buffer, index, index + length); - onClientAbort(abort); - break; - case WindowFW.TYPE_ID: - final WindowFW window = windowRO.wrap(buffer, index, index + length); - onClientWindow(window); - break; - case ResetFW.TYPE_ID: - final ResetFW reset = resetRO.wrap(buffer, index, index + length); - onClientReset(reset); - break; - default: - break; - } - } - - private void onClientBegin( - BeginFW begin) - { - final long sequence = begin.sequence(); - final long acknowledge = begin.acknowledge(); - final long traceId = begin.traceId(); - - replySeq = sequence; - replyAck = acknowledge; - - state = McpState.openedInitial(state); - - flushClientWindow(traceId, 0L, 0, 0L, 0); - } - - private void onClientData( - DataFW data) - { - final long sequence = data.sequence(); - final long acknowledge = data.acknowledge(); - final long traceId = data.traceId(); - final long authorization = data.authorization(); - final long budgetId = data.budgetId(); - final int reserved = data.reserved(); - final OctetsFW payload = data.payload(); - - assert acknowledge <= sequence; - assert sequence >= replySeq; - assert acknowledge <= replyAck; - - replySeq = sequence + reserved; - - assert replyAck <= replySeq; - - DirectBuffer buffer = payload.buffer(); - int offset = payload.offset(); - int limit = payload.limit(); - - if (replySlot != NO_SLOT) - { - final MutableDirectBuffer slot = bufferPool.buffer(replySlot); - if (replySlotOffset + (limit - offset) > slot.capacity()) - { - state = McpState.closedReply(state); - server.onClientError(traceId); - return; - } - slot.putBytes(replySlotOffset, buffer, offset, limit - offset); - replySlotOffset += limit - offset; - - buffer = slot; - offset = 0; - limit = replySlotOffset; - } - - decode(traceId, authorization, budgetId, reserved, buffer, offset, limit); - } - - private void onClientEnd( - EndFW end) - { - final long sequence = end.sequence(); - final long acknowledge = end.acknowledge(); - final long traceId = end.traceId(); - - assert acknowledge <= sequence; - assert sequence >= replySeq; - assert acknowledge <= replyAck; - - replySeq = sequence; - - assert replyAck <= replySeq; - - if (!McpState.replyClosed(state)) - { - state = McpState.closedReply(state); - cleanupClientSlot(); - server.onClientClosed(traceId); - } - } - - private void onClientAbort( - AbortFW abort) - { - final long sequence = abort.sequence(); - final long acknowledge = abort.acknowledge(); - final long traceId = abort.traceId(); - - assert acknowledge <= sequence; - assert sequence >= replySeq; - assert acknowledge <= replyAck; - - replySeq = sequence; - - assert replyAck <= replySeq; - - if (!McpState.replyClosed(state)) - { - state = McpState.closedReply(state); - cleanupClientSlot(); - server.onClientError(traceId); - } - } - - private void onClientWindow( - WindowFW window) - { - final long sequence = window.sequence(); - final long acknowledge = window.acknowledge(); - final long traceId = window.traceId(); - final long budgetId = window.budgetId(); - final int maximum = window.maximum(); - final int padding = window.padding(); - - assert acknowledge <= sequence; - assert sequence <= initialSeq; - assert acknowledge >= initialAck; - assert maximum + acknowledge >= initialMax + initialAck; - - initialAck = acknowledge; - initialMax = maximum; - initialPad = padding; - - assert initialAck <= initialSeq; - - server.flushServerWindow(traceId, budgetId, padding, initialSeq - initialAck, initialMax); - } - - private void onClientReset( - ResetFW reset) - { - final long sequence = reset.sequence(); - final long acknowledge = reset.acknowledge(); - final long traceId = reset.traceId(); - - assert acknowledge <= sequence; - assert sequence <= initialSeq; - assert acknowledge >= initialAck; - - initialAck = acknowledge; - - assert initialAck <= initialSeq; - - state = McpState.closedInitial(state); - if (!McpState.replyClosed(state)) - { - state = McpState.closedReply(state); - server.onClientError(traceId); - } - } - - private void decode( - long traceId, - long authorization, - long budgetId, - int reserved, - DirectBuffer buffer, - int offset, - int limit) - { - if (decodableJson != null) - { - final int delta = (int) (decodableJson.getLocation().getStreamOffset() - decodedParserProgress); - inputRO.wrap(buffer, offset + delta, limit - offset - delta); - } - - McpListClientDecoder previous = null; - int progress = offset; - while (progress <= limit && previous != decoder) - { - previous = decoder; - progress = decoder.decode(this, traceId, authorization, budgetId, reserved, - buffer, offset, progress, limit); - } - - final int compactBoundaryInBuf; - if (decodedItemProgress >= 0) - { - compactBoundaryInBuf = offset + (int) (decodedItemProgress - decodedParserProgress); - } - else - { - compactBoundaryInBuf = offset + (int) (decodableJson.getLocation().getStreamOffset() - decodedParserProgress); - } - - if (compactBoundaryInBuf < limit) - { - final int retained = limit - compactBoundaryInBuf; - if (replySlot == NO_SLOT) - { - replySlot = bufferPool.acquire(initialId); - if (replySlot == NO_SLOT) - { - state = McpState.closedReply(state); - server.onClientError(traceId); - return; - } - } - final MutableDirectBuffer slot = bufferPool.buffer(replySlot); - if (retained > slot.capacity()) - { - state = McpState.closedReply(state); - server.onClientError(traceId); - return; - } - slot.putBytes(0, buffer, compactBoundaryInBuf, retained); - replySlotOffset = retained; - decodedParserProgress += compactBoundaryInBuf - offset; - } - else - { - cleanupClientSlot(); - decodedParserProgress += compactBoundaryInBuf - offset; - } - } - - private void decode( - long traceId) - { - if (replySlot != NO_SLOT) - { - final MutableDirectBuffer slot = bufferPool.buffer(replySlot); - decode(traceId, server.authorization, 0L, 0, slot, 0, replySlotOffset); - } - } - - private void cleanupClientSlot() - { - if (replySlot != NO_SLOT) - { - bufferPool.release(replySlot); - replySlot = NO_SLOT; - replySlotOffset = 0; - } - } - } - - @FunctionalInterface - private interface McpListClientDecoder - { - int decode( - McpListClient client, - long traceId, - long authorization, - long budgetId, - int reserved, - DirectBuffer buffer, - int offset, - int progress, - int limit); - } - - private final McpListClientDecoder decodeInit = this::decodeInit; - private final McpListClientDecoder decodeReply = this::decodeReply; - private final McpListClientDecoder decodeItemsKey = this::decodeItemsKey; - private final McpListClientDecoder decodeSkipObject = this::decodeSkipObject; - private final McpListClientDecoder decodeItems = this::decodeItems; - private final McpListClientDecoder decodeItemStart = this::decodeItemStart; - private final McpListClientDecoder decodeItemBody = this::decodeItemBody; - private final McpListClientDecoder decodeItemId = this::decodeItemId; - private final McpListClientDecoder decodeItemFinalize = this::decodeItemFinalize; - private final McpListClientDecoder decodeIgnore = this::decodeIgnore; - - private int decodeInit( - McpListClient client, - long traceId, - long authorization, - long budgetId, - int reserved, - DirectBuffer buffer, - int offset, - int progress, - int limit) - { - final JsonParserFactory parserFactory = switch (client.server.kind) - { - case KIND_TOOLS_LIST -> toolsListItemParserFactory; - case KIND_PROMPTS_LIST -> promptsListItemParserFactory; - case KIND_RESOURCES_LIST -> resourcesListItemParserFactory; - default -> null; - }; - - if (parserFactory == null) - { - client.decoder = decodeIgnore; - return limit; - } - - inputRO.wrap(buffer, progress, limit - progress); - client.decodableJson = parserFactory.createParser(inputRO); - client.arrayKey = switch (client.server.kind) - { - case KIND_TOOLS_LIST -> "tools"; - case KIND_PROMPTS_LIST -> "prompts"; - case KIND_RESOURCES_LIST -> "resources"; - default -> null; - }; - client.idKey = client.server.kind == KIND_RESOURCES_LIST ? "uri" : "name"; - client.decoder = decodeReply; - - return progress; - } - - private int decodeReply( - McpListClient client, - long traceId, - long authorization, - long budgetId, - int reserved, - DirectBuffer buffer, - int offset, - int progress, - int limit) - { - final JsonParser parser = client.decodableJson; - - decode: - while (parser.hasNext()) - { - final JsonParser.Event event = parser.next(); - if (event == JsonParser.Event.START_OBJECT) - { - client.decodeDepth = 1; - client.decoder = decodeItemsKey; - break decode; - } - } - - return offset + (int) (parser.getLocation().getStreamOffset() - client.decodedParserProgress); - } - - private int decodeItemsKey( - McpListClient client, - long traceId, - long authorization, - long budgetId, - int reserved, - DirectBuffer buffer, - int offset, - int progress, - int limit) - { - final JsonParser parser = client.decodableJson; - - decode: - while (parser.hasNext()) - { - final JsonParser.Event event = parser.next(); - switch (event) - { - case KEY_NAME: - if (client.decodeDepth == 1) - { - final String key = parser.getString(); - if (client.arrayKey.equals(key)) - { - client.decoder = decodeItems; - } - else - { - client.decodeSkipDepth = 0; - client.decoder = decodeSkipObject; - } - break decode; - } - break; - case END_OBJECT: - client.decodeDepth--; - if (client.decodeDepth == 0) - { - client.decoder = decodeIgnore; - break decode; - } - break; - default: - break; - } - } - - return offset + (int) (parser.getLocation().getStreamOffset() - client.decodedParserProgress); - } - - private int decodeSkipObject( - McpListClient client, - long traceId, - long authorization, - long budgetId, - int reserved, - DirectBuffer buffer, - int offset, - int progress, - int limit) - { - final JsonParser parser = client.decodableJson; - - decode: - while (parser.hasNext()) - { - final JsonParser.Event event = parser.next(); - switch (event) - { - case START_OBJECT: - case START_ARRAY: - client.decodeSkipDepth++; - break; - case END_OBJECT: - case END_ARRAY: - client.decodeSkipDepth--; - if (client.decodeSkipDepth == 0) - { - client.decoder = decodeItemsKey; - break decode; - } - break; - case VALUE_STRING: - case VALUE_NUMBER: - case VALUE_TRUE: - case VALUE_FALSE: - case VALUE_NULL: - if (client.decodeSkipDepth == 0) - { - client.decoder = decodeItemsKey; - break decode; - } - break; - default: - break; - } - } - - return offset + (int) (parser.getLocation().getStreamOffset() - client.decodedParserProgress); - } - - private int decodeItems( - McpListClient client, - long traceId, - long authorization, - long budgetId, - int reserved, - DirectBuffer buffer, - int offset, - int progress, - int limit) - { - final JsonParser parser = client.decodableJson; - - decode: - while (parser.hasNext()) - { - final JsonParser.Event event = parser.next(); - if (event == JsonParser.Event.START_ARRAY) - { - client.decodeItemDepth = 0; - client.decoder = decodeItemStart; - break decode; - } - } - - return offset + (int) (parser.getLocation().getStreamOffset() - client.decodedParserProgress); - } - - private int decodeItemStart( - McpListClient client, - long traceId, - long authorization, - long budgetId, - int reserved, - DirectBuffer buffer, - int offset, - int progress, - int limit) - { - final JsonParser parser = client.decodableJson; - - decode: - while (parser.hasNext()) - { - final long decodedItemProgress = parser.getLocation().getStreamOffset(); - final JsonParser.Event event = parser.next(); - switch (event) - { - case START_OBJECT: - client.decodedItemProgress = decodedItemProgress - 1; - client.server.streamItemBegin(traceId); - client.decodeItemDepth = 1; - client.decoder = decodeItemBody; - break decode; - case END_ARRAY: - client.decodeDepth--; - client.decoder = decodeItemsKey; - break decode; - default: - break; - } - } - - return offset + (int) (parser.getLocation().getStreamOffset() - client.decodedParserProgress); - } - - private int decodeItemBody( - McpListClient client, - long traceId, - long authorization, - long budgetId, - int reserved, - DirectBuffer buffer, - int offset, - int progress, - int limit) - { - final JsonParser parser = client.decodableJson; - - decode: - while (true) - { - final long decodedItemProgress = parser.getLocation().getStreamOffset(); - if (client.decodedItemProgress < decodedItemProgress) - { - final int decodedOffset = - offset + (int) (client.decodedItemProgress - client.decodedParserProgress); - final int decodedLimit = offset + (int) (decodedItemProgress - client.decodedParserProgress); - final int chunkLen = decodedLimit - decodedOffset; - final int decodedProgress = client.server.streamItemChunk(buffer, decodedOffset, chunkLen, traceId); - client.decodedItemProgress += decodedProgress; - if (decodedProgress < chunkLen) - { - break decode; - } - } - - if (!parser.hasNext()) - { - break decode; - } - final long decodedEventProgress = parser.getLocation().getStreamOffset(); - final JsonParser.Event event = parser.next(); - switch (event) - { - case START_OBJECT: - case START_ARRAY: - client.decodeItemDepth++; - break; - case END_OBJECT: - client.decodeItemDepth--; - if (client.decodeItemDepth == 0) - { - final int decodedLimit = offset + (int) (decodedEventProgress - client.decodedParserProgress); - final int decodedOffset = - offset + (int) (client.decodedItemProgress - client.decodedParserProgress); - final int chunkLen = decodedLimit - decodedOffset; - final int decodedProgress = client.server.streamItemChunk(buffer, decodedOffset, chunkLen, traceId); - client.decodedItemProgress += decodedProgress; - if (decodedProgress < chunkLen) - { - client.decoder = decodeItemFinalize; - break decode; - } - client.server.streamItemEnd(traceId); - client.decodedItemProgress = -1; - client.decoder = decodeItemStart; - break decode; - } - break; - case END_ARRAY: - client.decodeItemDepth--; - break; - case KEY_NAME: - if (client.decodeItemDepth == 1 && - client.prefixBytes.length > 0 && - client.idKey.equals(parser.getString())) - { - client.decoder = decodeItemId; - break decode; - } - break; - default: - break; - } - } - - return offset + (int) (parser.getLocation().getStreamOffset() - client.decodedParserProgress); - } - - private int decodeItemId( - McpListClient client, - long traceId, - long authorization, - long budgetId, - int reserved, - DirectBuffer buffer, - int offset, - int progress, - int limit) - { - final JsonParser parser = client.decodableJson; - - final long decodedKeyProgress = parser.getLocation().getStreamOffset(); - if (client.decodedItemProgress < decodedKeyProgress) - { - final int decodedOffset = - offset + (int) (client.decodedItemProgress - client.decodedParserProgress); - final int decodedLimit = offset + (int) (decodedKeyProgress - client.decodedParserProgress); - final int chunkLen = decodedLimit - decodedOffset; - final int decodedProgress = client.server.streamItemChunk(buffer, decodedOffset, chunkLen, traceId); - client.decodedItemProgress += decodedProgress; - if (decodedProgress < chunkLen) - { - return offset + (int) (client.decodedItemProgress - client.decodedParserProgress); - } - } - - if (parser.hasNext()) - { - final long decodedValueProgress = parser.getLocation().getStreamOffset(); - final JsonParser.Event event = parser.next(); - if (event == JsonParser.Event.VALUE_STRING) - { - final int decodedKeyOffset = offset + (int) (decodedKeyProgress - client.decodedParserProgress); - final int decodedValueOffset = offset + (int) (decodedValueProgress - client.decodedParserProgress); - final int decodedOpenQuote = indexOfByte(buffer, decodedKeyOffset, decodedValueOffset, (byte) '"'); - final int decodedContent = (decodedOpenQuote != -1 ? decodedOpenQuote : decodedValueOffset) + 1; - final int decodedOffset = - offset + (int) (client.decodedItemProgress - client.decodedParserProgress); - client.server.streamItemChunk(buffer, decodedOffset, decodedContent - decodedOffset, traceId); - client.server.streamItemChunk(client.prefixBufferRO, 0, client.prefixBytes.length, traceId); - client.decodedItemProgress = - client.decodedParserProgress + (long) (decodedContent - offset); - } - client.decoder = decodeItemBody; - } - - return offset + (int) (parser.getLocation().getStreamOffset() - client.decodedParserProgress); - } - - private int decodeItemFinalize( - McpListClient client, - long traceId, - long authorization, - long budgetId, - int reserved, - DirectBuffer buffer, - int offset, - int progress, - int limit) - { - final JsonParser parser = client.decodableJson; - final long decodedItemProgress = parser.getLocation().getStreamOffset(); - - if (client.decodedItemProgress < decodedItemProgress) - { - final int decodedOffset = - offset + (int) (client.decodedItemProgress - client.decodedParserProgress); - final int decodedLimit = offset + (int) (decodedItemProgress - client.decodedParserProgress); - final int chunkLen = decodedLimit - decodedOffset; - final int decodedProgress = client.server.streamItemChunk(buffer, decodedOffset, chunkLen, traceId); - client.decodedItemProgress += decodedProgress; - if (decodedProgress < chunkLen) - { - return offset + (int) (client.decodedItemProgress - client.decodedParserProgress); - } - } - - client.server.streamItemEnd(traceId); - client.decodedItemProgress = -1; - client.decoder = decodeItemStart; - - return offset + (int) (decodedItemProgress - client.decodedParserProgress); - } - - private int decodeIgnore( - McpListClient client, - long traceId, - long authorization, - long budgetId, - int reserved, - DirectBuffer buffer, - int offset, - int progress, - int limit) - { - return limit; - } - - private final class McpListServer - { - private final McpLifecycleServer lifecycle; - private final int kind; - private final long initialId; - private final long replyId; - private final long affinity; - private final long authorization; - private final Deque remaining; - - private int state; - private int itemsEmitted; - private McpListClient client; - - private long initialSeq; - private long initialAck; - private int initialMax; - private int initialPad; - - private long replySeq; - private long replyAck; - private int replyMax; - private int replyPad; - - private McpListServer( - McpLifecycleServer lifecycle, - int kind, - long initialId, - long affinity, - long authorization, - List prefixes) - { - this.lifecycle = lifecycle; - this.kind = kind; - this.initialId = initialId; - this.replyId = supplyReplyId.applyAsLong(initialId); - this.affinity = affinity; - this.authorization = authorization; - this.remaining = new ArrayDeque<>(prefixes); - } - - private void onServerMessage( - int msgTypeId, - DirectBuffer buffer, - int index, - int length) - { - switch (msgTypeId) - { - case BeginFW.TYPE_ID: - final BeginFW begin = beginRO.wrap(buffer, index, index + length); - onServerBegin(begin); - break; - case EndFW.TYPE_ID: - final EndFW end = endRO.wrap(buffer, index, index + length); - onServerEnd(end); - break; - case AbortFW.TYPE_ID: - final AbortFW abort = abortRO.wrap(buffer, index, index + length); - onServerAbort(abort); - break; - case WindowFW.TYPE_ID: - final WindowFW window = windowRO.wrap(buffer, index, index + length); - onServerWindow(window); - break; - case ResetFW.TYPE_ID: - final ResetFW reset = resetRO.wrap(buffer, index, index + length); - onServerReset(reset); - break; - default: - break; - } - } - - private void onServerBegin( - BeginFW begin) - { - final long sequence = begin.sequence(); - final long acknowledge = begin.acknowledge(); - final long traceId = begin.traceId(); - - initialSeq = sequence; - initialAck = acknowledge; - - state = McpState.openingInitial(state); - - flushServerWindow(traceId, 0L, 0, 0L, 0); - - doServerBegin(traceId); - doEncodeBeginItems(traceId); - onNextClient(traceId); - } - - private void onServerEnd( - EndFW end) - { - final long sequence = end.sequence(); - final long acknowledge = end.acknowledge(); - final long traceId = end.traceId(); - - assert acknowledge <= sequence; - assert sequence >= initialSeq; - assert acknowledge <= initialAck; - - initialSeq = sequence; - - assert initialAck <= initialSeq; - - state = McpState.closedInitial(state); - - if (client != null) - { - client.doClientEnd(traceId); - } - } - - private void onServerAbort( - AbortFW abort) - { - final long sequence = abort.sequence(); - final long acknowledge = abort.acknowledge(); - final long traceId = abort.traceId(); - - assert acknowledge <= sequence; - assert sequence >= initialSeq; - assert acknowledge <= initialAck; - - initialSeq = sequence; - - assert initialAck <= initialSeq; - - state = McpState.closedInitial(state); - - if (client != null) - { - client.doClientAbort(traceId); - } - remaining.clear(); - } - - private void onServerWindow( - WindowFW window) - { - final long sequence = window.sequence(); - final long acknowledge = window.acknowledge(); - final long traceId = window.traceId(); - final long budgetId = window.budgetId(); - final int maximum = window.maximum(); - final int padding = window.padding(); - - assert acknowledge <= sequence; - assert sequence <= replySeq; - assert acknowledge >= replyAck; - assert maximum + acknowledge >= replyMax + replyAck; - - replyAck = acknowledge; - replyMax = maximum; - replyPad = padding; - - assert replyAck <= replySeq; - - if (client != null) - { - client.decode(traceId); - client.flushClientWindow(traceId, budgetId, padding, replySeq - replyAck, replyMax); - } - } - - private void onServerReset( - ResetFW reset) - { - final long sequence = reset.sequence(); - final long acknowledge = reset.acknowledge(); - final long traceId = reset.traceId(); - - assert acknowledge <= sequence; - assert sequence <= replySeq; - assert acknowledge >= replyAck; - - replyAck = acknowledge; - - assert replyAck <= replySeq; - - state = McpState.closedReply(state); - - if (client != null) - { - client.doClientReset(traceId); - } - remaining.clear(); - } - - private void onClientClosed( - long traceId) - { - client = null; - onNextClient(traceId); - } - - private void onClientError( - long traceId) - { - client = null; - remaining.clear(); - doServerAbort(traceId); - } - - private void onNextClient( - long traceId) - { - final McpRoutePrefix route = remaining.poll(); - if (route == null) - { - doEncodeEndItems(traceId); - return; - } - client = new McpListClient(this, route.resolvedId(), route.prefix()); - client.doClientBegin(traceId); - if (McpState.initialClosed(state)) - { - client.doClientEnd(traceId); - } - } - - private void streamItemBegin( - long traceId) - { - if (itemsEmitted > 0) - { - doServerData(traceId, 0L, 0x03, listReplySeparatorRO.capacity(), - listReplySeparatorRO, 0, listReplySeparatorRO.capacity()); - } - itemsEmitted++; - } - - private int streamItemChunk( - DirectBuffer buffer, - int offset, - int length, - long traceId) - { - final int replyWin = replyMax - (int) (replySeq - replyAck) - replyPad; - final int emit = Math.min(Math.max(replyWin, 0), length); - if (emit > 0) - { - doServerData(traceId, 0L, 0x03, emit, buffer, offset, emit); - } - return emit; - } - - private void streamItemEnd( - long traceId) - { - } - - private void doEncodeBeginItems( - long traceId) - { - final DirectBuffer prelude = switch (kind) - { - case KIND_TOOLS_LIST -> listReplyToolsOpenRO; - case KIND_PROMPTS_LIST -> listReplyPromptsOpenRO; - case KIND_RESOURCES_LIST -> listReplyResourcesOpenRO; - default -> throw new IllegalStateException("unexpected list kind: " + kind); - }; - doServerData(traceId, 0L, 0x03, prelude.capacity(), prelude, 0, prelude.capacity()); - } - - private void doEncodeEndItems( - long traceId) - { - doServerData(traceId, 0L, 0x03, listReplyCloseRO.capacity(), - listReplyCloseRO, 0, listReplyCloseRO.capacity()); - doServerEnd(traceId); - } - - private void doServerBegin( - long traceId) - { - final String sid = lifecycle.sessionId; - final McpBeginExFW beginEx = mcpBeginExRW - .wrap(codecBuffer, 0, codecBuffer.capacity()) - .typeId(mcpTypeId) - .inject(b -> - { - switch (kind) - { - case KIND_TOOLS_LIST -> b.toolsList(t -> t.sessionId(sid)); - case KIND_PROMPTS_LIST -> b.promptsList(p -> p.sessionId(sid)); - case KIND_RESOURCES_LIST -> b.resourcesList(r -> r.sessionId(sid)); - default -> throw new IllegalStateException("unexpected list kind: " + kind); - } - }) - .build(); - - doBegin(lifecycle.sender, lifecycle.originId, lifecycle.routedId, replyId, replySeq, replyAck, replyMax, - traceId, authorization, affinity, beginEx); - state = McpState.openedReply(state); - } - - private void doServerData( - long traceId, - long budgetId, - int flags, - int reserved, - DirectBuffer payload, - int offset, - int length) - { - doData(lifecycle.sender, lifecycle.originId, lifecycle.routedId, replyId, replySeq, replyAck, replyMax, - traceId, authorization, flags, budgetId, reserved, payload, offset, length); - replySeq += reserved; - } - - private void doServerEnd( - long traceId) - { - if (!McpState.replyClosed(state)) - { - doEnd(lifecycle.sender, lifecycle.originId, lifecycle.routedId, replyId, replySeq, replyAck, replyMax, - traceId, authorization); - state = McpState.closedReply(state); - } - } - - private void doServerAbort( - long traceId) - { - if (!McpState.replyClosed(state)) - { - doAbort(lifecycle.sender, lifecycle.originId, lifecycle.routedId, replyId, replySeq, replyAck, replyMax, - traceId, authorization); - state = McpState.closedReply(state); - } - } - - private void doServerWindow( - long traceId, - long budgetId, - int padding) - { - state = McpState.openedInitial(state); - doWindow(lifecycle.sender, lifecycle.originId, lifecycle.routedId, initialId, initialSeq, initialAck, initialMax, - traceId, authorization, budgetId, padding); - } - - private void flushServerWindow( - long traceId, - long budgetId, - int padding, - long minInitialNoAck, - int minInitialMax) - { - final long newInitialAck = Math.max(initialAck, initialSeq - minInitialNoAck); - final int newInitialMax = Math.max(initialMax, minInitialMax); - - if (newInitialAck > initialAck || newInitialMax > initialMax || !McpState.initialOpened(state)) - { - initialAck = newInitialAck; - initialMax = newInitialMax; - doServerWindow(traceId, budgetId, padding); - } - } - } - - private static boolean isListKind( - int kind) - { - return kind == KIND_TOOLS_LIST || kind == KIND_PROMPTS_LIST || kind == KIND_RESOURCES_LIST; - } - - private static int indexOfByte( - DirectBuffer buffer, - int offset, - int limit, - byte value) - { - for (int cursor = offset; cursor < limit; cursor++) - { - if (buffer.getByte(cursor) == value) - { - return cursor; - } - } - - return -1; - } - - private static String sessionId( - McpBeginExFW beginEx) - { - return switch (beginEx.kind()) - { - case KIND_LIFECYCLE -> beginEx.lifecycle().sessionId().asString(); - case KIND_TOOLS_LIST -> beginEx.toolsList().sessionId().asString(); - case KIND_TOOLS_CALL -> beginEx.toolsCall().sessionId().asString(); - case KIND_PROMPTS_LIST -> beginEx.promptsList().sessionId().asString(); - case KIND_PROMPTS_GET -> beginEx.promptsGet().sessionId().asString(); - case KIND_RESOURCES_LIST -> beginEx.resourcesList().sessionId().asString(); - case KIND_RESOURCES_READ -> beginEx.resourcesRead().sessionId().asString(); - default -> null; - }; - } - - private MessageConsumer newStream( - MessageConsumer sender, - long originId, - long routedId, - long streamId, - long sequence, - long acknowledge, - int maximum, - long traceId, - long authorization, - long affinity, - Flyweight extension) - { - final BeginFW begin = beginRW.wrap(writeBuffer, 0, writeBuffer.capacity()) - .originId(originId) - .routedId(routedId) - .streamId(streamId) - .sequence(sequence) - .acknowledge(acknowledge) - .maximum(maximum) - .traceId(traceId) - .authorization(authorization) - .affinity(affinity) - .extension(extension.buffer(), extension.offset(), extension.sizeof()) - .build(); - - final MessageConsumer receiver = - streamFactory.newStream(begin.typeId(), begin.buffer(), begin.offset(), begin.sizeof(), sender); - assert receiver != null; - - receiver.accept(begin.typeId(), begin.buffer(), begin.offset(), begin.sizeof()); - - return receiver; - } - - private void doBegin( - MessageConsumer receiver, - long originId, - long routedId, - long streamId, - long sequence, - long acknowledge, - int maximum, - long traceId, - long authorization, - long affinity, - Flyweight extension) - { - final BeginFW begin = beginRW.wrap(writeBuffer, 0, writeBuffer.capacity()) - .originId(originId) - .routedId(routedId) - .streamId(streamId) - .sequence(sequence) - .acknowledge(acknowledge) - .maximum(maximum) - .traceId(traceId) - .authorization(authorization) - .affinity(affinity) - .extension(extension.buffer(), extension.offset(), extension.sizeof()) - .build(); - - receiver.accept(begin.typeId(), begin.buffer(), begin.offset(), begin.sizeof()); - } - - private void doData( - MessageConsumer receiver, - long originId, - long routedId, - long streamId, - long sequence, - long acknowledge, - int maximum, - long traceId, - long authorization, - int flags, - long budgetId, - int reserved, - DirectBuffer payload, - int offset, - int length) - { - final DataFW data = dataRW.wrap(writeBuffer, 0, writeBuffer.capacity()) - .originId(originId) - .routedId(routedId) - .streamId(streamId) - .sequence(sequence) - .acknowledge(acknowledge) - .maximum(maximum) - .traceId(traceId) - .authorization(authorization) - .flags(flags) - .budgetId(budgetId) - .reserved(reserved) - .payload(payload, offset, length) - .build(); - - receiver.accept(data.typeId(), data.buffer(), data.offset(), data.sizeof()); - } - - private void doEnd( - MessageConsumer receiver, - long originId, - long routedId, - long streamId, - long sequence, - long acknowledge, - int maximum, - long traceId, - long authorization) - { - final EndFW end = endRW.wrap(writeBuffer, 0, writeBuffer.capacity()) - .originId(originId) - .routedId(routedId) - .streamId(streamId) - .sequence(sequence) - .acknowledge(acknowledge) - .maximum(maximum) - .traceId(traceId) - .authorization(authorization) - .build(); - - receiver.accept(end.typeId(), end.buffer(), end.offset(), end.sizeof()); - } - - private void doAbort( - MessageConsumer receiver, - long originId, - long routedId, - long streamId, - long sequence, - long acknowledge, - int maximum, - long traceId, - long authorization) - { - final AbortFW abort = abortRW.wrap(writeBuffer, 0, writeBuffer.capacity()) - .originId(originId) - .routedId(routedId) - .streamId(streamId) - .sequence(sequence) - .acknowledge(acknowledge) - .maximum(maximum) - .traceId(traceId) - .authorization(authorization) - .build(); - - receiver.accept(abort.typeId(), abort.buffer(), abort.offset(), abort.sizeof()); - } - - private void doFlush( - MessageConsumer receiver, - long originId, - long routedId, - long streamId, - long sequence, - long acknowledge, - int maximum, - long traceId, - long authorization, - long budgetId, - int reserved) - { - doFlush(receiver, originId, routedId, streamId, sequence, acknowledge, maximum, - traceId, authorization, budgetId, reserved, emptyRO); - } - - private void doFlush( - MessageConsumer receiver, - long originId, - long routedId, - long streamId, - long sequence, - long acknowledge, - int maximum, - long traceId, - long authorization, - long budgetId, - int reserved, - OctetsFW extension) - { - final FlushFW flush = flushRW.wrap(writeBuffer, 0, writeBuffer.capacity()) - .originId(originId) - .routedId(routedId) - .streamId(streamId) - .sequence(sequence) - .acknowledge(acknowledge) - .maximum(maximum) - .traceId(traceId) - .authorization(authorization) - .budgetId(budgetId) - .reserved(reserved) - .extension(extension.buffer(), extension.offset(), extension.sizeof()) - .build(); - - receiver.accept(flush.typeId(), flush.buffer(), flush.offset(), flush.sizeof()); - } - - private void doChallenge( - MessageConsumer receiver, - long originId, - long routedId, - long streamId, - long sequence, - long acknowledge, - int maximum, - long traceId, - long authorization, - Flyweight extension) - { - final ChallengeFW challenge = challengeRW.wrap(writeBuffer, 0, writeBuffer.capacity()) - .originId(originId) - .routedId(routedId) - .streamId(streamId) - .sequence(sequence) - .acknowledge(acknowledge) - .maximum(maximum) - .traceId(traceId) - .authorization(authorization) - .extension(extension.buffer(), extension.offset(), extension.sizeof()) - .build(); - - receiver.accept(challenge.typeId(), challenge.buffer(), challenge.offset(), challenge.sizeof()); - } - - private void doReset( - MessageConsumer receiver, - long originId, - long routedId, - long streamId, - long sequence, - long acknowledge, - int maximum, - long traceId, - long authorization, - Flyweight extension) - { - final ResetFW reset = resetRW.wrap(writeBuffer, 0, writeBuffer.capacity()) - .originId(originId) - .routedId(routedId) - .streamId(streamId) - .sequence(sequence) - .acknowledge(acknowledge) - .maximum(maximum) - .traceId(traceId) - .authorization(authorization) - .extension(extension.buffer(), extension.offset(), extension.sizeof()) - .build(); - - receiver.accept(reset.typeId(), reset.buffer(), reset.offset(), reset.sizeof()); - } - - private void doWindow( - MessageConsumer receiver, - long originId, - long routedId, - long streamId, - long sequence, - long acknowledge, - int maximum, - long traceId, - long authorization, - long budgetId, - int padding) - { - final WindowFW window = windowRW.wrap(writeBuffer, 0, writeBuffer.capacity()) - .originId(originId) - .routedId(routedId) - .streamId(streamId) - .sequence(sequence) - .acknowledge(acknowledge) - .maximum(maximum) - .traceId(traceId) - .authorization(authorization) - .budgetId(budgetId) - .padding(padding) - .build(); - - receiver.accept(window.typeId(), window.buffer(), window.offset(), window.sizeof()); - } } diff --git a/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/McpProxyItemFactory.java b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/McpProxyItemFactory.java new file mode 100644 index 0000000000..eb0c969319 --- /dev/null +++ b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/McpProxyItemFactory.java @@ -0,0 +1,1112 @@ +/* + * Copyright 2021-2024 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. + */ +package io.aklivity.zilla.runtime.binding.mcp.internal.stream; + +import java.util.function.LongFunction; +import java.util.function.LongUnaryOperator; + +import org.agrona.DirectBuffer; +import org.agrona.MutableDirectBuffer; +import org.agrona.concurrent.UnsafeBuffer; + +import io.aklivity.zilla.runtime.binding.mcp.internal.McpConfiguration; +import io.aklivity.zilla.runtime.binding.mcp.internal.config.McpBindingConfig; +import io.aklivity.zilla.runtime.binding.mcp.internal.config.McpRouteConfig; +import io.aklivity.zilla.runtime.binding.mcp.internal.stream.McpProxyLifecycleFactory.McpLifecycleClient; +import io.aklivity.zilla.runtime.binding.mcp.internal.stream.McpProxyLifecycleFactory.McpLifecycleServer; +import io.aklivity.zilla.runtime.binding.mcp.internal.types.Flyweight; +import io.aklivity.zilla.runtime.binding.mcp.internal.types.OctetsFW; +import io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.AbortFW; +import io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.BeginFW; +import io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.ChallengeFW; +import io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.DataFW; +import io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.EndFW; +import io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.FlushFW; +import io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.McpBeginExFW; +import io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.ResetFW; +import io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.WindowFW; +import io.aklivity.zilla.runtime.engine.EngineContext; +import io.aklivity.zilla.runtime.engine.binding.BindingHandler; +import io.aklivity.zilla.runtime.engine.binding.function.MessageConsumer; + +abstract class McpProxyItemFactory implements BindingHandler +{ + private static final String MCP_TYPE_NAME = "mcp"; + + private final BeginFW beginRO = new BeginFW(); + private final DataFW dataRO = new DataFW(); + private final EndFW endRO = new EndFW(); + private final AbortFW abortRO = new AbortFW(); + private final FlushFW flushRO = new FlushFW(); + private final WindowFW windowRO = new WindowFW(); + private final ResetFW resetRO = new ResetFW(); + private final ChallengeFW challengeRO = new ChallengeFW(); + private final McpBeginExFW mcpBeginExRO = new McpBeginExFW(); + private final OctetsFW emptyRO = new OctetsFW().wrap(new UnsafeBuffer(), 0, 0); + + private final BeginFW.Builder beginRW = new BeginFW.Builder(); + private final DataFW.Builder dataRW = new DataFW.Builder(); + private final EndFW.Builder endRW = new EndFW.Builder(); + private final AbortFW.Builder abortRW = new AbortFW.Builder(); + private final FlushFW.Builder flushRW = new FlushFW.Builder(); + private final WindowFW.Builder windowRW = new WindowFW.Builder(); + private final ResetFW.Builder resetRW = new ResetFW.Builder(); + private final ChallengeFW.Builder challengeRW = new ChallengeFW.Builder(); + private final McpBeginExFW.Builder mcpBeginExRW = new McpBeginExFW.Builder(); + + private final MutableDirectBuffer writeBuffer; + private final MutableDirectBuffer codecBuffer; + private final BindingHandler streamFactory; + private final LongUnaryOperator supplyInitialId; + private final LongUnaryOperator supplyReplyId; + private final int mcpTypeId; + private final LongFunction supplyBinding; + private final int kind; + + McpProxyItemFactory( + McpConfiguration config, + EngineContext context, + LongFunction supplyBinding, + int kind) + { + this.writeBuffer = context.writeBuffer(); + this.codecBuffer = new UnsafeBuffer(new byte[context.writeBuffer().capacity()]); + this.streamFactory = context.streamFactory(); + this.supplyInitialId = context::supplyInitialId; + this.supplyReplyId = context::supplyReplyId; + this.mcpTypeId = context.supplyTypeId(MCP_TYPE_NAME); + this.supplyBinding = supplyBinding; + this.kind = kind; + } + + @Override + public final MessageConsumer newStream( + int msgTypeId, + DirectBuffer buffer, + int index, + int length, + MessageConsumer sender) + { + final BeginFW begin = beginRO.wrap(buffer, index, index + length); + final long originId = begin.originId(); + final long routedId = begin.routedId(); + final long initialId = begin.streamId(); + final long affinity = begin.affinity(); + final long authorization = begin.authorization(); + final OctetsFW extension = begin.extension(); + + MessageConsumer newStream = null; + + final McpBindingConfig binding = supplyBinding.apply(routedId); + final McpBeginExFW beginEx = extension.get(mcpBeginExRO::tryWrap); + + if (binding != null && beginEx != null && beginEx.kind() == kind) + { + final String sessionId = sessionId(beginEx); + if (binding.sessions.get(sessionId) instanceof McpLifecycleServer lifecycle) + { + final McpRouteConfig route = binding.resolve(beginEx, authorization); + if (route != null) + { + final String identifier = route.strip(beginEx); + final String prefix = route.prefix(beginEx); + + newStream = new McpServer( + lifecycle, + sender, + originId, + routedId, + initialId, + route.id, + affinity, + authorization, + identifier, + prefix)::onServerMessage; + } + } + } + + return newStream; + } + + protected abstract void injectInitialBeginEx( + McpBeginExFW.Builder builder, + String sessionId, + String identifier); + + protected abstract void injectReplyBeginEx( + McpBeginExFW.Builder builder, + String sessionId, + McpBeginExFW upstream); + + protected abstract String sessionId( + McpBeginExFW beginEx); + + private final class McpServer + { + private final McpLifecycleServer lifecycle; + private final MessageConsumer sender; + private final long originId; + private final long routedId; + private final long initialId; + private final long replyId; + private final long affinity; + private final long authorization; + private final String identifier; + private final String prefix; + private final McpClient client; + + private int state; + + private long initialSeq; + private long initialAck; + private int initialMax; + private int initialPad; + + private long replySeq; + private long replyAck; + private int replyMax; + private int replyPad; + + private McpServer( + McpLifecycleServer lifecycle, + MessageConsumer sender, + long originId, + long routedId, + long initialId, + long resolvedId, + long affinity, + long authorization, + String identifier, + String prefix) + { + this.lifecycle = lifecycle; + this.sender = sender; + this.originId = originId; + this.routedId = routedId; + this.initialId = initialId; + this.replyId = supplyReplyId.applyAsLong(initialId); + this.affinity = affinity; + this.authorization = authorization; + this.identifier = identifier; + this.prefix = prefix; + this.client = new McpClient(this, resolvedId); + } + + private String sessionId() + { + return lifecycle.sessionId; + } + + private void onServerMessage( + int msgTypeId, + DirectBuffer buffer, + int index, + int length) + { + switch (msgTypeId) + { + case BeginFW.TYPE_ID: + final BeginFW begin = beginRO.wrap(buffer, index, index + length); + onServerBegin(begin); + break; + case DataFW.TYPE_ID: + final DataFW data = dataRO.wrap(buffer, index, index + length); + onServerData(data); + break; + case EndFW.TYPE_ID: + final EndFW end = endRO.wrap(buffer, index, index + length); + onServerEnd(end); + break; + case AbortFW.TYPE_ID: + final AbortFW abort = abortRO.wrap(buffer, index, index + length); + onServerAbort(abort); + break; + case FlushFW.TYPE_ID: + final FlushFW flush = flushRO.wrap(buffer, index, index + length); + onServerFlush(flush); + break; + case WindowFW.TYPE_ID: + final WindowFW window = windowRO.wrap(buffer, index, index + length); + onServerWindow(window); + break; + case ResetFW.TYPE_ID: + final ResetFW reset = resetRO.wrap(buffer, index, index + length); + onServerReset(reset); + break; + case ChallengeFW.TYPE_ID: + final ChallengeFW challenge = challengeRO.wrap(buffer, index, index + length); + onServerChallenge(challenge); + break; + default: + break; + } + } + + private void onServerChallenge( + ChallengeFW challenge) + { + client.doClientChallenge(challenge.traceId(), challenge.authorization(), challenge.extension()); + } + + private void onServerBegin( + BeginFW begin) + { + final long sequence = begin.sequence(); + final long acknowledge = begin.acknowledge(); + final long traceId = begin.traceId(); + + initialSeq = sequence; + initialAck = acknowledge; + + state = McpState.openingInitial(state); + + client.doClientBegin(traceId); + + flushServerWindow(traceId, 0L, 0, 0L, 0); + } + + private void onServerData( + DataFW data) + { + final long sequence = data.sequence(); + final long acknowledge = data.acknowledge(); + final long traceId = data.traceId(); + final long budgetId = data.budgetId(); + final int flags = data.flags(); + final int reserved = data.reserved(); + final OctetsFW payload = data.payload(); + + assert acknowledge <= sequence; + assert sequence >= initialSeq; + assert acknowledge <= initialAck; + + initialSeq = sequence + reserved; + + assert initialAck <= initialSeq; + + client.doClientData(traceId, budgetId, flags, reserved, + payload.buffer(), payload.offset(), payload.sizeof()); + } + + private void onServerEnd( + EndFW end) + { + final long sequence = end.sequence(); + final long acknowledge = end.acknowledge(); + final long traceId = end.traceId(); + + assert acknowledge <= sequence; + assert sequence >= initialSeq; + assert acknowledge <= initialAck; + + initialSeq = sequence; + + assert initialAck <= initialSeq; + + state = McpState.closedInitial(state); + + client.doClientEnd(traceId); + } + + private void onServerAbort( + AbortFW abort) + { + final long sequence = abort.sequence(); + final long acknowledge = abort.acknowledge(); + final long traceId = abort.traceId(); + + assert acknowledge <= sequence; + assert sequence >= initialSeq; + assert acknowledge <= initialAck; + + initialSeq = sequence; + + assert initialAck <= initialSeq; + + state = McpState.closedInitial(state); + + client.doClientAbort(traceId); + } + + private void onServerFlush( + FlushFW flush) + { + client.doClientFlush(flush.traceId(), flush.authorization(), + flush.budgetId(), flush.reserved(), flush.extension()); + } + + private void onServerWindow( + WindowFW window) + { + final long sequence = window.sequence(); + final long acknowledge = window.acknowledge(); + final int maximum = window.maximum(); + final long traceId = window.traceId(); + final long budgetId = window.budgetId(); + final int padding = window.padding(); + + assert acknowledge <= sequence; + assert sequence <= replySeq; + assert acknowledge >= replyAck; + assert maximum + acknowledge >= replyMax + replyAck; + + replyAck = acknowledge; + replyMax = maximum; + replyPad = padding; + + assert replyAck <= replySeq; + + client.flushClientWindow(traceId, budgetId, padding, replySeq - replyAck, replyMax); + } + + private void onServerReset( + ResetFW reset) + { + final long sequence = reset.sequence(); + final long acknowledge = reset.acknowledge(); + final long traceId = reset.traceId(); + + assert acknowledge <= sequence; + assert sequence <= replySeq; + assert acknowledge >= replyAck; + + replyAck = acknowledge; + + assert replyAck <= replySeq; + + state = McpState.closedReply(state); + + client.doClientReset(traceId); + } + + private void doServerBegin( + long traceId, + Flyweight extension) + { + doBegin(sender, originId, routedId, replyId, replySeq, replyAck, replyMax, traceId, authorization, + affinity, extension); + state = McpState.openedReply(state); + } + + private void doServerData( + long traceId, + long budgetId, + int flags, + int reserved, + DirectBuffer payload, + int offset, + int length) + { + doData(sender, originId, routedId, replyId, replySeq, replyAck, replyMax, traceId, authorization, + flags, budgetId, reserved, payload, offset, length); + replySeq += reserved; + } + + private void doServerEnd( + long traceId) + { + if (!McpState.replyClosed(state)) + { + doEnd(sender, originId, routedId, replyId, replySeq, replyAck, replyMax, traceId, authorization); + state = McpState.closedReply(state); + } + } + + private void doServerAbort( + long traceId) + { + if (!McpState.replyClosed(state)) + { + doAbort(sender, originId, routedId, replyId, replySeq, replyAck, replyMax, traceId, authorization); + state = McpState.closedReply(state); + } + } + + private void doServerFlush( + long traceId, + long authorization, + long budgetId, + int reserved, + OctetsFW extension) + { + doFlush(sender, originId, routedId, replyId, replySeq, replyAck, replyMax, + traceId, authorization, budgetId, reserved, extension); + } + + private void doServerChallenge( + long traceId, + long authorization, + OctetsFW extension) + { + doChallenge(sender, originId, routedId, initialId, initialSeq, initialAck, initialMax, + traceId, authorization, extension); + } + + private void doServerWindow( + long traceId, + long budgetId, + int padding) + { + state = McpState.openedInitial(state); + doWindow(sender, originId, routedId, initialId, initialSeq, initialAck, initialMax, traceId, authorization, + budgetId, padding); + } + + private void flushServerWindow( + long traceId, + long budgetId, + int padding, + long minInitialNoAck, + int minInitialMax) + { + final long newInitialAck = Math.max(initialAck, initialSeq - minInitialNoAck); + final int newInitialMax = Math.max(initialMax, minInitialMax); + + if (newInitialAck > initialAck || newInitialMax > initialMax || !McpState.initialOpened(state)) + { + initialAck = newInitialAck; + initialMax = newInitialMax; + doServerWindow(traceId, budgetId, padding); + } + } + + private void doServerReset( + long traceId) + { + if (!McpState.initialClosed(state)) + { + doReset(sender, originId, routedId, initialId, initialSeq, initialAck, initialMax, traceId, authorization, + emptyRO); + state = McpState.closedInitial(state); + } + } + } + + private final class McpClient + { + private final McpServer server; + private final long resolvedId; + private final McpLifecycleClient lifecycle; + + private final long initialId; + private final long replyId; + + private MessageConsumer sender; + private int state; + + private long initialSeq; + private long initialAck; + private int initialMax; + private int initialPad; + + private long replySeq; + private long replyAck; + private int replyMax; + private int replyPad; + + private McpClient( + McpServer server, + long resolvedId) + { + this.server = server; + this.resolvedId = resolvedId; + this.lifecycle = server.lifecycle.supplyClient(resolvedId); + this.initialId = supplyInitialId.applyAsLong(resolvedId); + this.replyId = supplyReplyId.applyAsLong(initialId); + } + + private void doClientBegin( + long traceId) + { + lifecycle.doClientBegin(traceId); + + final String identifier = server.identifier; + final String upstreamSessionId = lifecycle.sessionId; + final String outboundSessionId = upstreamSessionId != null + ? upstreamSessionId + : server.sessionId(); + final McpBeginExFW beginEx = mcpBeginExRW + .wrap(codecBuffer, 0, codecBuffer.capacity()) + .typeId(mcpTypeId) + .inject(b -> injectInitialBeginEx(b, outboundSessionId, identifier)) + .build(); + + sender = newStream(this::onClientMessage, server.lifecycle.originId, resolvedId, initialId, + initialSeq, initialAck, initialMax, traceId, server.authorization, server.affinity, beginEx); + state = McpState.openingInitial(state); + } + + private void doClientData( + long traceId, + long budgetId, + int flags, + int reserved, + DirectBuffer payload, + int offset, + int length) + { + if (!McpState.closed(state)) + { + doData(sender, server.lifecycle.originId, resolvedId, initialId, + initialSeq, initialAck, initialMax, traceId, server.authorization, + flags, budgetId, reserved, payload, offset, length); + initialSeq += reserved; + } + } + + private void doClientFlush( + long traceId, + long authorization, + long budgetId, + int reserved, + OctetsFW extension) + { + doFlush(sender, server.lifecycle.originId, resolvedId, initialId, + initialSeq, initialAck, initialMax, + traceId, authorization, budgetId, reserved, extension); + } + + private void doClientEnd( + long traceId) + { + if (!McpState.initialClosed(state)) + { + doEnd(sender, server.lifecycle.originId, resolvedId, initialId, + initialSeq, initialAck, initialMax, traceId, server.authorization); + state = McpState.closedInitial(state); + } + } + + private void doClientAbort( + long traceId) + { + if (!McpState.initialClosed(state)) + { + doAbort(sender, server.lifecycle.originId, resolvedId, initialId, + initialSeq, initialAck, initialMax, traceId, server.authorization); + state = McpState.closedInitial(state); + } + } + + private void doClientWindow( + long traceId, + long budgetId, + int padding) + { + state = McpState.openedReply(state); + doWindow(sender, server.lifecycle.originId, resolvedId, replyId, + replySeq, replyAck, replyMax, traceId, server.authorization, budgetId, padding); + } + + private void flushClientWindow( + long traceId, + long budgetId, + int padding, + long minReplyNoAck, + int minReplyMax) + { + final long newReplyAck = Math.max(replyAck, replySeq - minReplyNoAck); + final int newReplyMax = Math.max(replyMax, minReplyMax); + + if (newReplyAck > replyAck || newReplyMax > replyMax || !McpState.replyOpened(state)) + { + replyAck = newReplyAck; + replyMax = newReplyMax; + doClientWindow(traceId, budgetId, padding); + } + } + + private void doClientReset( + long traceId) + { + if (!McpState.replyClosed(state)) + { + doReset(sender, server.lifecycle.originId, resolvedId, replyId, + replySeq, replyAck, replyMax, traceId, server.authorization, emptyRO); + state = McpState.closedReply(state); + } + } + + private void doClientChallenge( + long traceId, + long authorization, + Flyweight extension) + { + doChallenge(sender, server.lifecycle.originId, resolvedId, replyId, + replySeq, replyAck, replyMax, traceId, authorization, extension); + } + + private void onClientMessage( + int msgTypeId, + DirectBuffer buffer, + int index, + int length) + { + switch (msgTypeId) + { + case BeginFW.TYPE_ID: + final BeginFW begin = beginRO.wrap(buffer, index, index + length); + onClientBegin(begin); + break; + case DataFW.TYPE_ID: + final DataFW data = dataRO.wrap(buffer, index, index + length); + onClientData(data); + break; + case EndFW.TYPE_ID: + final EndFW end = endRO.wrap(buffer, index, index + length); + onClientEnd(end); + break; + case AbortFW.TYPE_ID: + final AbortFW abort = abortRO.wrap(buffer, index, index + length); + onClientAbort(abort); + break; + case FlushFW.TYPE_ID: + final FlushFW flush = flushRO.wrap(buffer, index, index + length); + onClientFlush(flush); + break; + case WindowFW.TYPE_ID: + final WindowFW window = windowRO.wrap(buffer, index, index + length); + onClientWindow(window); + break; + case ResetFW.TYPE_ID: + final ResetFW reset = resetRO.wrap(buffer, index, index + length); + onClientReset(reset); + break; + case ChallengeFW.TYPE_ID: + final ChallengeFW challenge = challengeRO.wrap(buffer, index, index + length); + onClientChallenge(challenge); + break; + default: + break; + } + } + + private void onClientFlush( + FlushFW flush) + { + server.doServerFlush(flush.traceId(), flush.authorization(), + flush.budgetId(), flush.reserved(), flush.extension()); + } + + private void onClientChallenge( + ChallengeFW challenge) + { + server.doServerChallenge(challenge.traceId(), challenge.authorization(), challenge.extension()); + } + + private void onClientBegin( + BeginFW begin) + { + final long sequence = begin.sequence(); + final long acknowledge = begin.acknowledge(); + final long traceId = begin.traceId(); + final OctetsFW extension = begin.extension(); + + replySeq = sequence; + replyAck = acknowledge; + + state = McpState.openedInitial(state); + + final McpBeginExFW beginEx = extension.get(mcpBeginExRO::tryWrap); + final Flyweight replyExtension = beginEx != null + ? rewriteReplyBeginEx(beginEx) + : emptyRO; + + server.doServerBegin(traceId, replyExtension); + + flushClientWindow(traceId, 0L, 0, 0L, 0); + } + + private Flyweight rewriteReplyBeginEx( + McpBeginExFW beginEx) + { + final String sid = server.sessionId(); + return mcpBeginExRW + .wrap(codecBuffer, 0, codecBuffer.capacity()) + .typeId(mcpTypeId) + .inject(b -> injectReplyBeginEx(b, sid, beginEx)) + .build(); + } + + private void onClientData( + DataFW data) + { + final long sequence = data.sequence(); + final long acknowledge = data.acknowledge(); + final long traceId = data.traceId(); + final long budgetId = data.budgetId(); + final int flags = data.flags(); + final int reserved = data.reserved(); + final OctetsFW payload = data.payload(); + + assert acknowledge <= sequence; + assert sequence >= replySeq; + assert acknowledge <= replyAck; + + replySeq = sequence + reserved; + + assert replyAck <= replySeq; + + server.doServerData(traceId, budgetId, flags, reserved, + payload.buffer(), payload.offset(), payload.sizeof()); + } + + private void onClientEnd( + EndFW end) + { + final long sequence = end.sequence(); + final long acknowledge = end.acknowledge(); + final long traceId = end.traceId(); + + assert acknowledge <= sequence; + assert sequence >= replySeq; + assert acknowledge <= replyAck; + + replySeq = sequence; + + assert replyAck <= replySeq; + + state = McpState.closedReply(state); + server.doServerEnd(traceId); + } + + private void onClientAbort( + AbortFW abort) + { + final long sequence = abort.sequence(); + final long acknowledge = abort.acknowledge(); + final long traceId = abort.traceId(); + + assert acknowledge <= sequence; + assert sequence >= replySeq; + assert acknowledge <= replyAck; + + replySeq = sequence; + + assert replyAck <= replySeq; + + state = McpState.closedReply(state); + server.doServerAbort(traceId); + } + + private void onClientWindow( + WindowFW window) + { + final long sequence = window.sequence(); + final long acknowledge = window.acknowledge(); + final int maximum = window.maximum(); + final long traceId = window.traceId(); + final long budgetId = window.budgetId(); + final int padding = window.padding(); + + assert acknowledge <= sequence; + assert sequence <= initialSeq; + assert acknowledge >= initialAck; + assert maximum + acknowledge >= initialMax + initialAck; + + initialAck = acknowledge; + initialMax = maximum; + initialPad = padding; + + assert initialAck <= initialSeq; + + server.flushServerWindow(traceId, budgetId, padding, initialSeq - initialAck, initialMax); + } + + private void onClientReset( + ResetFW reset) + { + final long sequence = reset.sequence(); + final long acknowledge = reset.acknowledge(); + final long traceId = reset.traceId(); + + assert acknowledge <= sequence; + assert sequence <= initialSeq; + assert acknowledge >= initialAck; + + initialAck = acknowledge; + + assert initialAck <= initialSeq; + + state = McpState.closedInitial(state); + + server.doServerReset(traceId); + } + } + + private MessageConsumer newStream( + MessageConsumer sender, + long originId, + long routedId, + long streamId, + long sequence, + long acknowledge, + int maximum, + long traceId, + long authorization, + long affinity, + Flyweight extension) + { + final BeginFW begin = beginRW.wrap(writeBuffer, 0, writeBuffer.capacity()) + .originId(originId) + .routedId(routedId) + .streamId(streamId) + .sequence(sequence) + .acknowledge(acknowledge) + .maximum(maximum) + .traceId(traceId) + .authorization(authorization) + .affinity(affinity) + .extension(extension.buffer(), extension.offset(), extension.sizeof()) + .build(); + + final MessageConsumer receiver = + streamFactory.newStream(begin.typeId(), begin.buffer(), begin.offset(), begin.sizeof(), sender); + assert receiver != null; + + receiver.accept(begin.typeId(), begin.buffer(), begin.offset(), begin.sizeof()); + + return receiver; + } + + private void doBegin( + MessageConsumer receiver, + long originId, + long routedId, + long streamId, + long sequence, + long acknowledge, + int maximum, + long traceId, + long authorization, + long affinity, + Flyweight extension) + { + final BeginFW begin = beginRW.wrap(writeBuffer, 0, writeBuffer.capacity()) + .originId(originId) + .routedId(routedId) + .streamId(streamId) + .sequence(sequence) + .acknowledge(acknowledge) + .maximum(maximum) + .traceId(traceId) + .authorization(authorization) + .affinity(affinity) + .extension(extension.buffer(), extension.offset(), extension.sizeof()) + .build(); + + receiver.accept(begin.typeId(), begin.buffer(), begin.offset(), begin.sizeof()); + } + + private void doData( + MessageConsumer receiver, + long originId, + long routedId, + long streamId, + long sequence, + long acknowledge, + int maximum, + long traceId, + long authorization, + int flags, + long budgetId, + int reserved, + DirectBuffer payload, + int offset, + int length) + { + final DataFW data = dataRW.wrap(writeBuffer, 0, writeBuffer.capacity()) + .originId(originId) + .routedId(routedId) + .streamId(streamId) + .sequence(sequence) + .acknowledge(acknowledge) + .maximum(maximum) + .traceId(traceId) + .authorization(authorization) + .flags(flags) + .budgetId(budgetId) + .reserved(reserved) + .payload(payload, offset, length) + .build(); + + receiver.accept(data.typeId(), data.buffer(), data.offset(), data.sizeof()); + } + + private void doEnd( + MessageConsumer receiver, + long originId, + long routedId, + long streamId, + long sequence, + long acknowledge, + int maximum, + long traceId, + long authorization) + { + final EndFW end = endRW.wrap(writeBuffer, 0, writeBuffer.capacity()) + .originId(originId) + .routedId(routedId) + .streamId(streamId) + .sequence(sequence) + .acknowledge(acknowledge) + .maximum(maximum) + .traceId(traceId) + .authorization(authorization) + .build(); + + receiver.accept(end.typeId(), end.buffer(), end.offset(), end.sizeof()); + } + + private void doAbort( + MessageConsumer receiver, + long originId, + long routedId, + long streamId, + long sequence, + long acknowledge, + int maximum, + long traceId, + long authorization) + { + final AbortFW abort = abortRW.wrap(writeBuffer, 0, writeBuffer.capacity()) + .originId(originId) + .routedId(routedId) + .streamId(streamId) + .sequence(sequence) + .acknowledge(acknowledge) + .maximum(maximum) + .traceId(traceId) + .authorization(authorization) + .build(); + + receiver.accept(abort.typeId(), abort.buffer(), abort.offset(), abort.sizeof()); + } + + private void doFlush( + MessageConsumer receiver, + long originId, + long routedId, + long streamId, + long sequence, + long acknowledge, + int maximum, + long traceId, + long authorization, + long budgetId, + int reserved, + OctetsFW extension) + { + final FlushFW flush = flushRW.wrap(writeBuffer, 0, writeBuffer.capacity()) + .originId(originId) + .routedId(routedId) + .streamId(streamId) + .sequence(sequence) + .acknowledge(acknowledge) + .maximum(maximum) + .traceId(traceId) + .authorization(authorization) + .budgetId(budgetId) + .reserved(reserved) + .extension(extension.buffer(), extension.offset(), extension.sizeof()) + .build(); + + receiver.accept(flush.typeId(), flush.buffer(), flush.offset(), flush.sizeof()); + } + + private void doChallenge( + MessageConsumer receiver, + long originId, + long routedId, + long streamId, + long sequence, + long acknowledge, + int maximum, + long traceId, + long authorization, + Flyweight extension) + { + final ChallengeFW challenge = challengeRW.wrap(writeBuffer, 0, writeBuffer.capacity()) + .originId(originId) + .routedId(routedId) + .streamId(streamId) + .sequence(sequence) + .acknowledge(acknowledge) + .maximum(maximum) + .traceId(traceId) + .authorization(authorization) + .extension(extension.buffer(), extension.offset(), extension.sizeof()) + .build(); + + receiver.accept(challenge.typeId(), challenge.buffer(), challenge.offset(), challenge.sizeof()); + } + + private void doReset( + MessageConsumer receiver, + long originId, + long routedId, + long streamId, + long sequence, + long acknowledge, + int maximum, + long traceId, + long authorization, + Flyweight extension) + { + final ResetFW reset = resetRW.wrap(writeBuffer, 0, writeBuffer.capacity()) + .originId(originId) + .routedId(routedId) + .streamId(streamId) + .sequence(sequence) + .acknowledge(acknowledge) + .maximum(maximum) + .traceId(traceId) + .authorization(authorization) + .extension(extension.buffer(), extension.offset(), extension.sizeof()) + .build(); + + receiver.accept(reset.typeId(), reset.buffer(), reset.offset(), reset.sizeof()); + } + + private void doWindow( + MessageConsumer receiver, + long originId, + long routedId, + long streamId, + long sequence, + long acknowledge, + int maximum, + long traceId, + long authorization, + long budgetId, + int padding) + { + final WindowFW window = windowRW.wrap(writeBuffer, 0, writeBuffer.capacity()) + .originId(originId) + .routedId(routedId) + .streamId(streamId) + .sequence(sequence) + .acknowledge(acknowledge) + .maximum(maximum) + .traceId(traceId) + .authorization(authorization) + .budgetId(budgetId) + .padding(padding) + .build(); + + receiver.accept(window.typeId(), window.buffer(), window.offset(), window.sizeof()); + } +} diff --git a/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/McpProxyLifecycleFactory.java b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/McpProxyLifecycleFactory.java new file mode 100644 index 0000000000..9790ce403d --- /dev/null +++ b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/McpProxyLifecycleFactory.java @@ -0,0 +1,1009 @@ +/* + * Copyright 2021-2024 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. + */ +package io.aklivity.zilla.runtime.binding.mcp.internal.stream; + +import static io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.McpBeginExFW.KIND_LIFECYCLE; + +import java.util.function.LongFunction; +import java.util.function.LongUnaryOperator; + +import org.agrona.DirectBuffer; +import org.agrona.MutableDirectBuffer; +import org.agrona.collections.Long2ObjectHashMap; +import org.agrona.concurrent.UnsafeBuffer; + +import io.aklivity.zilla.runtime.binding.mcp.internal.McpConfiguration; +import io.aklivity.zilla.runtime.binding.mcp.internal.config.McpBindingConfig; +import io.aklivity.zilla.runtime.binding.mcp.internal.config.McpProxySession; +import io.aklivity.zilla.runtime.binding.mcp.internal.config.McpRouteConfig; +import io.aklivity.zilla.runtime.binding.mcp.internal.types.Flyweight; +import io.aklivity.zilla.runtime.binding.mcp.internal.types.OctetsFW; +import io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.AbortFW; +import io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.BeginFW; +import io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.ChallengeFW; +import io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.DataFW; +import io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.EndFW; +import io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.FlushFW; +import io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.McpBeginExFW; +import io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.McpChallengeExFW; +import io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.ResetFW; +import io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.SignalFW; +import io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.WindowFW; +import io.aklivity.zilla.runtime.engine.EngineContext; +import io.aklivity.zilla.runtime.engine.binding.BindingHandler; +import io.aklivity.zilla.runtime.engine.binding.function.MessageConsumer; +import io.aklivity.zilla.runtime.engine.concurrent.Signaler; + +final class McpProxyLifecycleFactory implements BindingHandler +{ + static final int SIGNAL_HYDRATE_COMPLETE = 1; + + private static final String MCP_TYPE_NAME = "mcp"; + + private final BeginFW beginRO = new BeginFW(); + private final DataFW dataRO = new DataFW(); + private final EndFW endRO = new EndFW(); + private final AbortFW abortRO = new AbortFW(); + private final FlushFW flushRO = new FlushFW(); + private final WindowFW windowRO = new WindowFW(); + private final ResetFW resetRO = new ResetFW(); + private final ChallengeFW challengeRO = new ChallengeFW(); + private final SignalFW signalRO = new SignalFW(); + private final McpBeginExFW mcpBeginExRO = new McpBeginExFW(); + private final OctetsFW emptyRO = new OctetsFW().wrap(new UnsafeBuffer(), 0, 0); + + private final BeginFW.Builder beginRW = new BeginFW.Builder(); + private final DataFW.Builder dataRW = new DataFW.Builder(); + private final EndFW.Builder endRW = new EndFW.Builder(); + private final AbortFW.Builder abortRW = new AbortFW.Builder(); + private final FlushFW.Builder flushRW = new FlushFW.Builder(); + private final WindowFW.Builder windowRW = new WindowFW.Builder(); + private final ResetFW.Builder resetRW = new ResetFW.Builder(); + private final ChallengeFW.Builder challengeRW = new ChallengeFW.Builder(); + private final McpBeginExFW.Builder mcpBeginExRW = new McpBeginExFW.Builder(); + private final McpChallengeExFW.Builder mcpChallengeExRW = new McpChallengeExFW.Builder(); + + private final MutableDirectBuffer writeBuffer; + private final MutableDirectBuffer codecBuffer; + private final BindingHandler streamFactory; + private final Signaler signaler; + private final LongUnaryOperator supplyInitialId; + private final LongUnaryOperator supplyReplyId; + private final int mcpTypeId; + private final LongFunction supplyBinding; + + McpProxyLifecycleFactory( + McpConfiguration config, + EngineContext context, + LongFunction supplyBinding) + { + this.writeBuffer = context.writeBuffer(); + this.codecBuffer = new UnsafeBuffer(new byte[context.writeBuffer().capacity()]); + this.streamFactory = context.streamFactory(); + this.signaler = context.signaler(); + this.supplyInitialId = context::supplyInitialId; + this.supplyReplyId = context::supplyReplyId; + this.mcpTypeId = context.supplyTypeId(MCP_TYPE_NAME); + this.supplyBinding = supplyBinding; + } + + @Override + public MessageConsumer newStream( + int msgTypeId, + DirectBuffer buffer, + int index, + int length, + MessageConsumer sender) + { + final BeginFW begin = beginRO.wrap(buffer, index, index + length); + final long originId = begin.originId(); + final long routedId = begin.routedId(); + final long initialId = begin.streamId(); + final long affinity = begin.affinity(); + final long authorization = begin.authorization(); + final OctetsFW extension = begin.extension(); + + MessageConsumer newStream = null; + + final McpBindingConfig binding = supplyBinding.apply(routedId); + final McpBeginExFW beginEx = extension.get(mcpBeginExRO::tryWrap); + + if (binding != null && beginEx != null && beginEx.kind() == KIND_LIFECYCLE) + { + final String sessionId = beginEx.lifecycle().sessionId().asString(); + final McpRouteConfig route = binding.resolve(beginEx, authorization); + if (route != null) + { + final int clientCapabilities = beginEx.lifecycle().capabilities(); + final McpLifecycleServer lifecycle = new McpLifecycleServer( + binding, sender, originId, routedId, initialId, affinity, authorization, + clientCapabilities, sessionId); + binding.sessions.put(sessionId, lifecycle); + newStream = lifecycle::onServerMessage; + } + } + + return newStream; + } + + final class McpLifecycleServer implements McpProxySession + { + private final McpBindingConfig binding; + final MessageConsumer sender; + final long originId; + final long routedId; + private final long initialId; + private final long replyId; + private final long affinity; + private final long authorization; + private final int clientCapabilities; + final String sessionId; + private final Long2ObjectHashMap clients; + + private int state; + private boolean resumePending; + + private long initialSeq; + private long initialAck; + private int initialMax; + private int initialPad; + + private long replySeq; + private long replyAck; + private int replyMax; + private int replyPad; + + private McpLifecycleServer( + McpBindingConfig binding, + MessageConsumer sender, + long originId, + long routedId, + long initialId, + long affinity, + long authorization, + int clientCapabilities, + String sessionId) + { + this.binding = binding; + this.sender = sender; + this.originId = originId; + this.routedId = routedId; + this.initialId = initialId; + this.replyId = supplyReplyId.applyAsLong(initialId); + this.affinity = affinity; + this.authorization = authorization; + this.clientCapabilities = clientCapabilities; + this.sessionId = sessionId; + this.clients = new Long2ObjectHashMap<>(); + } + + McpLifecycleClient supplyClient( + long routedId) + { + return clients.computeIfAbsent(routedId, id -> new McpLifecycleClient(this, id)); + } + + private void onServerMessage( + int msgTypeId, + DirectBuffer buffer, + int index, + int length) + { + switch (msgTypeId) + { + case BeginFW.TYPE_ID: + final BeginFW begin = beginRO.wrap(buffer, index, index + length); + onServerBegin(begin); + break; + case DataFW.TYPE_ID: + // no-op: proxy terminates lifecycle locally, no DATA is forwarded + break; + case EndFW.TYPE_ID: + final EndFW end = endRO.wrap(buffer, index, index + length); + onServerEnd(end); + break; + case AbortFW.TYPE_ID: + final AbortFW abort = abortRO.wrap(buffer, index, index + length); + onServerAbort(abort); + break; + case WindowFW.TYPE_ID: + final WindowFW window = windowRO.wrap(buffer, index, index + length); + onServerWindow(window); + break; + case ResetFW.TYPE_ID: + final ResetFW reset = resetRO.wrap(buffer, index, index + length); + onServerReset(reset); + break; + case ChallengeFW.TYPE_ID: + final ChallengeFW challenge = challengeRO.wrap(buffer, index, index + length); + onServerChallenge(challenge); + break; + case SignalFW.TYPE_ID: + final SignalFW signal = signalRO.wrap(buffer, index, index + length); + onServerSignal(signal); + break; + default: + break; + } + } + + private void onServerChallenge( + ChallengeFW challenge) + { + resumePending = true; + + final long traceId = challenge.traceId(); + final long authorization = challenge.authorization(); + for (McpLifecycleClient client : clients.values()) + { + client.doClientResume(traceId, authorization); + } + } + + private void onServerBegin( + BeginFW begin) + { + final long sequence = begin.sequence(); + final long acknowledge = begin.acknowledge(); + final long traceId = begin.traceId(); + + initialSeq = sequence; + initialAck = acknowledge; + + state = McpState.openingInitial(state); + + doServerWindow(traceId, 0L, 0); + + if (binding.cache != null && originId != routedId) + { + binding.cache.register(() -> signaler.signalNow( + originId, routedId, replyId, traceId, SIGNAL_HYDRATE_COMPLETE, 0)); + } + else + { + doServerBeginDeferred(traceId); + } + } + + private void onServerSignal( + SignalFW signal) + { + if (signal.signalId() == SIGNAL_HYDRATE_COMPLETE) + { + doServerBeginDeferred(signal.traceId()); + } + } + + private void doServerBeginDeferred( + long traceId) + { + if (!McpState.replyOpened(state) && !McpState.replyClosed(state)) + { + final int serverCapabilities = binding.serverCapabilities(authorization); + final String sid = sessionId; + final McpBeginExFW beginEx = mcpBeginExRW + .wrap(codecBuffer, 0, codecBuffer.capacity()) + .typeId(mcpTypeId) + .lifecycle(l -> l.sessionId(sid).capabilities(serverCapabilities)) + .build(); + + doServerBegin(traceId, beginEx); + } + } + + private void onServerEnd( + EndFW end) + { + final long sequence = end.sequence(); + final long acknowledge = end.acknowledge(); + final long traceId = end.traceId(); + + assert acknowledge <= sequence; + assert sequence >= initialSeq; + assert acknowledge <= initialAck; + + initialSeq = sequence; + + assert initialAck <= initialSeq; + + state = McpState.closedInitial(state); + + binding.sessions.remove(sessionId); + + for (McpLifecycleClient client : clients.values()) + { + client.doClientEnd(traceId); + } + + doServerEnd(traceId); + } + + private void onServerAbort( + AbortFW abort) + { + final long sequence = abort.sequence(); + final long acknowledge = abort.acknowledge(); + final long traceId = abort.traceId(); + + assert acknowledge <= sequence; + assert sequence >= initialSeq; + assert acknowledge <= initialAck; + + initialSeq = sequence; + + assert initialAck <= initialSeq; + + state = McpState.closedInitial(state); + + binding.sessions.remove(sessionId); + + for (McpLifecycleClient client : clients.values()) + { + client.doClientAbort(traceId); + } + + doServerAbort(traceId); + } + + private void onServerWindow( + WindowFW window) + { + final long sequence = window.sequence(); + final long acknowledge = window.acknowledge(); + final int maximum = window.maximum(); + final int padding = window.padding(); + + assert acknowledge <= sequence; + assert sequence <= replySeq; + assert acknowledge >= replyAck; + assert maximum + acknowledge >= replyMax + replyAck; + + replyAck = acknowledge; + replyMax = maximum; + replyPad = padding; + + assert replyAck <= replySeq; + } + + private void onServerReset( + ResetFW reset) + { + final long sequence = reset.sequence(); + final long acknowledge = reset.acknowledge(); + final long traceId = reset.traceId(); + + assert acknowledge <= sequence; + assert sequence <= replySeq; + assert acknowledge >= replyAck; + + replyAck = acknowledge; + + assert replyAck <= replySeq; + + state = McpState.closedReply(state); + + binding.sessions.remove(sessionId); + + for (McpLifecycleClient client : clients.values()) + { + client.doClientReset(traceId); + } + } + + private void doServerBegin( + long traceId, + Flyweight extension) + { + doBegin(sender, originId, routedId, replyId, replySeq, replyAck, replyMax, traceId, authorization, + affinity, extension); + state = McpState.openedReply(state); + } + + private void doServerEnd( + long traceId) + { + if (!McpState.replyClosed(state)) + { + doEnd(sender, originId, routedId, replyId, replySeq, replyAck, replyMax, traceId, authorization); + state = McpState.closedReply(state); + } + } + + private void doServerAbort( + long traceId) + { + if (!McpState.replyClosed(state)) + { + doAbort(sender, originId, routedId, replyId, replySeq, replyAck, replyMax, traceId, authorization); + state = McpState.closedReply(state); + } + } + + private void doServerReset( + long traceId) + { + if (!McpState.initialClosed(state)) + { + doReset(sender, originId, routedId, initialId, initialSeq, initialAck, initialMax, traceId, + authorization, emptyRO); + state = McpState.closedInitial(state); + } + } + + private void doServerFlush( + long traceId, + long authorization, + long budgetId, + int reserved, + OctetsFW extension) + { + doFlush(sender, originId, routedId, replyId, replySeq, replyAck, replyMax, + traceId, authorization, budgetId, reserved, extension); + } + + private void doServerWindow( + long traceId, + long budgetId, + int padding) + { + doWindow(sender, originId, routedId, initialId, initialSeq, initialAck, initialMax, traceId, authorization, + budgetId, padding); + } + + } + + final class McpLifecycleClient + { + private final McpLifecycleServer server; + private final long routedId; + private final long initialId; + private final long replyId; + + private MessageConsumer sender; + private int state; + String sessionId; // upstream-provided session id, set on BEGIN reply + + private long initialSeq; + private long initialAck; + private int initialMax; + private int initialPad; + + private long replySeq; + private long replyAck; + private int replyMax; + private int replyPad; + + private McpLifecycleClient( + McpLifecycleServer server, + long routedId) + { + this.server = server; + this.routedId = routedId; + this.initialId = supplyInitialId.applyAsLong(routedId); + this.replyId = supplyReplyId.applyAsLong(initialId); + } + + void doClientBegin( + long traceId) + { + if (!McpState.initialOpening(state)) + { + final long originId = server.routedId; + final String sid = server.sessionId; + final int clientCapabilities = server.clientCapabilities; + final McpBeginExFW beginEx = mcpBeginExRW + .wrap(codecBuffer, 0, codecBuffer.capacity()) + .typeId(mcpTypeId) + .lifecycle(l -> l.sessionId(sid).capabilities(clientCapabilities)) + .build(); + + sender = newStream(this::onClientMessage, originId, routedId, initialId, + initialSeq, initialAck, initialMax, traceId, server.authorization, server.affinity, beginEx); + state = McpState.openingInitial(state); + } + } + + private void doClientEnd( + long traceId) + { + if (!McpState.initialClosed(state)) + { + final long originId = server.routedId; + doEnd(sender, originId, routedId, initialId, initialSeq, initialAck, initialMax, traceId, + server.authorization); + state = McpState.closedInitial(state); + } + } + + private void doClientAbort( + long traceId) + { + if (!McpState.initialClosed(state)) + { + final long originId = server.routedId; + doAbort(sender, originId, routedId, initialId, initialSeq, initialAck, initialMax, traceId, + server.authorization); + state = McpState.closedInitial(state); + } + } + + private void doClientReset( + long traceId) + { + if (!McpState.replyClosed(state)) + { + final long originId = server.routedId; + doReset(sender, originId, routedId, replyId, replySeq, replyAck, replyMax, traceId, + server.authorization, emptyRO); + state = McpState.closedReply(state); + } + } + + private void doClientChallenge( + long traceId, + long authorization, + Flyweight extension) + { + final long originId = server.routedId; + doChallenge(sender, originId, routedId, replyId, replySeq, replyAck, replyMax, + traceId, authorization, extension); + } + + private void doClientResume( + long traceId, + long authorization) + { + final McpChallengeExFW resumeEx = mcpChallengeExRW + .wrap(codecBuffer, 0, codecBuffer.capacity()) + .typeId(mcpTypeId) + .resume(b -> {}) + .build(); + doClientChallenge(traceId, authorization, resumeEx); + } + + private void doClientWindow( + long traceId, + long budgetId, + int padding) + { + final long originId = server.routedId; + doWindow(sender, originId, routedId, replyId, replySeq, replyAck, replyMax, traceId, server.authorization, + budgetId, padding); + } + + private void onClientMessage( + int msgTypeId, + DirectBuffer buffer, + int index, + int length) + { + switch (msgTypeId) + { + case BeginFW.TYPE_ID: + final BeginFW begin = beginRO.wrap(buffer, index, index + length); + onClientBegin(begin); + break; + case DataFW.TYPE_ID: + // lifecycle does not carry DATA in this proxy model + break; + case EndFW.TYPE_ID: + final EndFW end = endRO.wrap(buffer, index, index + length); + onClientEnd(end); + break; + case AbortFW.TYPE_ID: + final AbortFW abort = abortRO.wrap(buffer, index, index + length); + onClientAbort(abort); + break; + case FlushFW.TYPE_ID: + final FlushFW flush = flushRO.wrap(buffer, index, index + length); + onClientFlush(flush); + break; + case WindowFW.TYPE_ID: + final WindowFW window = windowRO.wrap(buffer, index, index + length); + onClientWindow(window); + break; + case ResetFW.TYPE_ID: + final ResetFW reset = resetRO.wrap(buffer, index, index + length); + onClientReset(reset); + break; + default: + break; + } + } + + private void onClientFlush( + FlushFW flush) + { + server.doServerFlush(flush.traceId(), flush.authorization(), + flush.budgetId(), flush.reserved(), flush.extension()); + } + + private void onClientBegin( + BeginFW begin) + { + final long sequence = begin.sequence(); + final long acknowledge = begin.acknowledge(); + final long traceId = begin.traceId(); + final long authorization = begin.authorization(); + final OctetsFW extension = begin.extension(); + + replySeq = sequence; + replyAck = acknowledge; + + state = McpState.openedInitial(state); + + final McpBeginExFW beginEx = extension.get(mcpBeginExRO::tryWrap); + if (beginEx != null && beginEx.kind() == KIND_LIFECYCLE) + { + sessionId = beginEx.lifecycle().sessionId().asString(); + } + + doClientWindow(traceId, 0L, 0); + + state = McpState.openedReply(state); + + if (server.resumePending) + { + doClientResume(traceId, authorization); + } + } + + private void onClientEnd( + EndFW end) + { + final long sequence = end.sequence(); + final long acknowledge = end.acknowledge(); + final long traceId = end.traceId(); + + assert acknowledge <= sequence; + assert sequence >= replySeq; + assert acknowledge <= replyAck; + + replySeq = sequence; + + assert replyAck <= replySeq; + + state = McpState.closedReply(state); + doClientEnd(traceId); + server.clients.remove(routedId, this); + server.doServerEnd(traceId); + } + + private void onClientAbort( + AbortFW abort) + { + final long sequence = abort.sequence(); + final long acknowledge = abort.acknowledge(); + final long traceId = abort.traceId(); + + assert acknowledge <= sequence; + assert sequence >= replySeq; + assert acknowledge <= replyAck; + + replySeq = sequence; + + assert replyAck <= replySeq; + + state = McpState.closedReply(state); + doClientAbort(traceId); + server.clients.remove(routedId, this); + server.doServerAbort(traceId); + } + + private void onClientWindow( + WindowFW window) + { + final long sequence = window.sequence(); + final long acknowledge = window.acknowledge(); + final int maximum = window.maximum(); + final int padding = window.padding(); + + assert acknowledge <= sequence; + assert sequence <= initialSeq; + assert acknowledge >= initialAck; + assert maximum + acknowledge >= initialMax + initialAck; + + initialAck = acknowledge; + initialMax = maximum; + initialPad = padding; + + assert initialAck <= initialSeq; + } + + private void onClientReset( + ResetFW reset) + { + final long sequence = reset.sequence(); + final long acknowledge = reset.acknowledge(); + final long traceId = reset.traceId(); + + assert acknowledge <= sequence; + assert sequence <= initialSeq; + assert acknowledge >= initialAck; + + initialAck = acknowledge; + + assert initialAck <= initialSeq; + + state = McpState.closedInitial(state); + doClientReset(traceId); + server.clients.remove(routedId, this); + server.doServerReset(traceId); + } + } + + private MessageConsumer newStream( + MessageConsumer sender, + long originId, + long routedId, + long streamId, + long sequence, + long acknowledge, + int maximum, + long traceId, + long authorization, + long affinity, + Flyweight extension) + { + final BeginFW begin = beginRW.wrap(writeBuffer, 0, writeBuffer.capacity()) + .originId(originId) + .routedId(routedId) + .streamId(streamId) + .sequence(sequence) + .acknowledge(acknowledge) + .maximum(maximum) + .traceId(traceId) + .authorization(authorization) + .affinity(affinity) + .extension(extension.buffer(), extension.offset(), extension.sizeof()) + .build(); + + final MessageConsumer receiver = + streamFactory.newStream(begin.typeId(), begin.buffer(), begin.offset(), begin.sizeof(), sender); + assert receiver != null; + + receiver.accept(begin.typeId(), begin.buffer(), begin.offset(), begin.sizeof()); + + return receiver; + } + + private void doBegin( + MessageConsumer receiver, + long originId, + long routedId, + long streamId, + long sequence, + long acknowledge, + int maximum, + long traceId, + long authorization, + long affinity, + Flyweight extension) + { + final BeginFW begin = beginRW.wrap(writeBuffer, 0, writeBuffer.capacity()) + .originId(originId) + .routedId(routedId) + .streamId(streamId) + .sequence(sequence) + .acknowledge(acknowledge) + .maximum(maximum) + .traceId(traceId) + .authorization(authorization) + .affinity(affinity) + .extension(extension.buffer(), extension.offset(), extension.sizeof()) + .build(); + + receiver.accept(begin.typeId(), begin.buffer(), begin.offset(), begin.sizeof()); + } + + private void doData( + MessageConsumer receiver, + long originId, + long routedId, + long streamId, + long sequence, + long acknowledge, + int maximum, + long traceId, + long authorization, + int flags, + long budgetId, + int reserved, + DirectBuffer payload, + int offset, + int length) + { + final DataFW data = dataRW.wrap(writeBuffer, 0, writeBuffer.capacity()) + .originId(originId) + .routedId(routedId) + .streamId(streamId) + .sequence(sequence) + .acknowledge(acknowledge) + .maximum(maximum) + .traceId(traceId) + .authorization(authorization) + .flags(flags) + .budgetId(budgetId) + .reserved(reserved) + .payload(payload, offset, length) + .build(); + + receiver.accept(data.typeId(), data.buffer(), data.offset(), data.sizeof()); + } + + private void doEnd( + MessageConsumer receiver, + long originId, + long routedId, + long streamId, + long sequence, + long acknowledge, + int maximum, + long traceId, + long authorization) + { + final EndFW end = endRW.wrap(writeBuffer, 0, writeBuffer.capacity()) + .originId(originId) + .routedId(routedId) + .streamId(streamId) + .sequence(sequence) + .acknowledge(acknowledge) + .maximum(maximum) + .traceId(traceId) + .authorization(authorization) + .build(); + + receiver.accept(end.typeId(), end.buffer(), end.offset(), end.sizeof()); + } + + private void doAbort( + MessageConsumer receiver, + long originId, + long routedId, + long streamId, + long sequence, + long acknowledge, + int maximum, + long traceId, + long authorization) + { + final AbortFW abort = abortRW.wrap(writeBuffer, 0, writeBuffer.capacity()) + .originId(originId) + .routedId(routedId) + .streamId(streamId) + .sequence(sequence) + .acknowledge(acknowledge) + .maximum(maximum) + .traceId(traceId) + .authorization(authorization) + .build(); + + receiver.accept(abort.typeId(), abort.buffer(), abort.offset(), abort.sizeof()); + } + + private void doFlush( + MessageConsumer receiver, + long originId, + long routedId, + long streamId, + long sequence, + long acknowledge, + int maximum, + long traceId, + long authorization, + long budgetId, + int reserved, + OctetsFW extension) + { + final FlushFW flush = flushRW.wrap(writeBuffer, 0, writeBuffer.capacity()) + .originId(originId) + .routedId(routedId) + .streamId(streamId) + .sequence(sequence) + .acknowledge(acknowledge) + .maximum(maximum) + .traceId(traceId) + .authorization(authorization) + .budgetId(budgetId) + .reserved(reserved) + .extension(extension.buffer(), extension.offset(), extension.sizeof()) + .build(); + + receiver.accept(flush.typeId(), flush.buffer(), flush.offset(), flush.sizeof()); + } + + private void doChallenge( + MessageConsumer receiver, + long originId, + long routedId, + long streamId, + long sequence, + long acknowledge, + int maximum, + long traceId, + long authorization, + Flyweight extension) + { + final ChallengeFW challenge = challengeRW.wrap(writeBuffer, 0, writeBuffer.capacity()) + .originId(originId) + .routedId(routedId) + .streamId(streamId) + .sequence(sequence) + .acknowledge(acknowledge) + .maximum(maximum) + .traceId(traceId) + .authorization(authorization) + .extension(extension.buffer(), extension.offset(), extension.sizeof()) + .build(); + + receiver.accept(challenge.typeId(), challenge.buffer(), challenge.offset(), challenge.sizeof()); + } + + private void doReset( + MessageConsumer receiver, + long originId, + long routedId, + long streamId, + long sequence, + long acknowledge, + int maximum, + long traceId, + long authorization, + Flyweight extension) + { + final ResetFW reset = resetRW.wrap(writeBuffer, 0, writeBuffer.capacity()) + .originId(originId) + .routedId(routedId) + .streamId(streamId) + .sequence(sequence) + .acknowledge(acknowledge) + .maximum(maximum) + .traceId(traceId) + .authorization(authorization) + .extension(extension.buffer(), extension.offset(), extension.sizeof()) + .build(); + + receiver.accept(reset.typeId(), reset.buffer(), reset.offset(), reset.sizeof()); + } + + private void doWindow( + MessageConsumer receiver, + long originId, + long routedId, + long streamId, + long sequence, + long acknowledge, + int maximum, + long traceId, + long authorization, + long budgetId, + int padding) + { + final WindowFW window = windowRW.wrap(writeBuffer, 0, writeBuffer.capacity()) + .originId(originId) + .routedId(routedId) + .streamId(streamId) + .sequence(sequence) + .acknowledge(acknowledge) + .maximum(maximum) + .traceId(traceId) + .authorization(authorization) + .budgetId(budgetId) + .padding(padding) + .build(); + + receiver.accept(window.typeId(), window.buffer(), window.offset(), window.sizeof()); + } +} diff --git a/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/McpProxyListFactory.java b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/McpProxyListFactory.java new file mode 100644 index 0000000000..4c4792d3b3 --- /dev/null +++ b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/McpProxyListFactory.java @@ -0,0 +1,1811 @@ +/* + * Copyright 2021-2024 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. + */ +package io.aklivity.zilla.runtime.binding.mcp.internal.stream; + +import static io.aklivity.zilla.runtime.engine.buffer.BufferPool.NO_SLOT; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.List; +import java.util.Map; +import java.util.function.LongFunction; +import java.util.function.LongSupplier; +import java.util.function.LongUnaryOperator; + +import jakarta.json.stream.JsonParser; +import jakarta.json.stream.JsonParserFactory; + +import org.agrona.DirectBuffer; +import org.agrona.MutableDirectBuffer; +import org.agrona.concurrent.UnsafeBuffer; + +import io.aklivity.zilla.runtime.binding.mcp.internal.McpConfiguration; +import io.aklivity.zilla.runtime.binding.mcp.internal.config.McpBindingConfig; +import io.aklivity.zilla.runtime.binding.mcp.internal.config.McpRoutePrefix; +import io.aklivity.zilla.runtime.binding.mcp.internal.stream.McpProxyLifecycleFactory.McpLifecycleClient; +import io.aklivity.zilla.runtime.binding.mcp.internal.stream.McpProxyLifecycleFactory.McpLifecycleServer; +import io.aklivity.zilla.runtime.binding.mcp.internal.stream.cache.McpProxyCache; +import io.aklivity.zilla.runtime.binding.mcp.internal.types.Flyweight; +import io.aklivity.zilla.runtime.binding.mcp.internal.types.OctetsFW; +import io.aklivity.zilla.runtime.binding.mcp.internal.types.String8FW; +import io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.AbortFW; +import io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.BeginFW; +import io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.DataFW; +import io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.EndFW; +import io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.McpBeginExFW; +import io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.ResetFW; +import io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.WindowFW; +import io.aklivity.zilla.runtime.common.json.DirectBufferInputStreamEx; +import io.aklivity.zilla.runtime.common.json.StreamingJson; +import io.aklivity.zilla.runtime.engine.EngineContext; +import io.aklivity.zilla.runtime.engine.binding.BindingHandler; +import io.aklivity.zilla.runtime.engine.binding.function.MessageConsumer; +import io.aklivity.zilla.runtime.engine.buffer.BufferPool; + +abstract class McpProxyListFactory implements BindingHandler +{ + private static final String MCP_TYPE_NAME = "mcp"; + + private final BeginFW beginRO = new BeginFW(); + private final DataFW dataRO = new DataFW(); + private final EndFW endRO = new EndFW(); + private final AbortFW abortRO = new AbortFW(); + private final WindowFW windowRO = new WindowFW(); + private final ResetFW resetRO = new ResetFW(); + private final McpBeginExFW mcpBeginExRO = new McpBeginExFW(); + private final OctetsFW emptyRO = new OctetsFW().wrap(new UnsafeBuffer(), 0, 0); + private final DirectBufferInputStreamEx inputRO = new DirectBufferInputStreamEx(); + private final DirectBuffer listReplyCloseRO = + new UnsafeBuffer("]}".getBytes(StandardCharsets.UTF_8)); + private final DirectBuffer listReplySeparatorRO = + new UnsafeBuffer(",".getBytes(StandardCharsets.UTF_8)); + + private final BeginFW.Builder beginRW = new BeginFW.Builder(); + private final DataFW.Builder dataRW = new DataFW.Builder(); + private final EndFW.Builder endRW = new EndFW.Builder(); + private final AbortFW.Builder abortRW = new AbortFW.Builder(); + private final WindowFW.Builder windowRW = new WindowFW.Builder(); + private final ResetFW.Builder resetRW = new ResetFW.Builder(); + private final McpBeginExFW.Builder mcpBeginExRW = new McpBeginExFW.Builder(); + + private final MutableDirectBuffer writeBuffer; + private final MutableDirectBuffer codecBuffer; + private final BindingHandler streamFactory; + private final BufferPool bufferPool; + private final LongUnaryOperator supplyInitialId; + private final LongUnaryOperator supplyReplyId; + private final LongSupplier supplyTraceId; + private final int mcpTypeId; + private final LongFunction supplyBinding; + private final int kind; + private final JsonParserFactory listItemParserFactory; + + private final McpListClientDecoder decodeInit = this::decodeInit; + private final McpListClientDecoder decodeReply = this::decodeReply; + private final McpListClientDecoder decodeItemsKey = this::decodeItemsKey; + private final McpListClientDecoder decodeSkipObject = this::decodeSkipObject; + private final McpListClientDecoder decodeItems = this::decodeItems; + private final McpListClientDecoder decodeItemStart = this::decodeItemStart; + private final McpListClientDecoder decodeItemBody = this::decodeItemBody; + private final McpListClientDecoder decodeItemId = this::decodeItemId; + private final McpListClientDecoder decodeItemFinalize = this::decodeItemFinalize; + private final McpListClientDecoder decodeIgnore = this::decodeIgnore; + + McpProxyListFactory( + McpConfiguration config, + EngineContext context, + LongFunction supplyBinding, + int kind, + List pathIncludes) + { + this.writeBuffer = context.writeBuffer(); + this.codecBuffer = new UnsafeBuffer(new byte[context.writeBuffer().capacity()]); + this.streamFactory = context.streamFactory(); + this.bufferPool = context.bufferPool(); + this.supplyInitialId = context::supplyInitialId; + this.supplyReplyId = context::supplyReplyId; + this.supplyTraceId = context::supplyTraceId; + this.mcpTypeId = context.supplyTypeId(MCP_TYPE_NAME); + this.supplyBinding = supplyBinding; + this.kind = kind; + this.listItemParserFactory = StreamingJson.createParserFactory( + Map.of(StreamingJson.PATH_INCLUDES, pathIncludes)); + } + + @Override + public final MessageConsumer newStream( + int msgTypeId, + DirectBuffer buffer, + int index, + int length, + MessageConsumer sender) + { + final BeginFW begin = beginRO.wrap(buffer, index, index + length); + final long originId = begin.originId(); + final long routedId = begin.routedId(); + final long initialId = begin.streamId(); + final long affinity = begin.affinity(); + final long authorization = begin.authorization(); + final OctetsFW extension = begin.extension(); + + MessageConsumer newStream = null; + + final McpBindingConfig binding = supplyBinding.apply(routedId); + final McpBeginExFW beginEx = extension.get(mcpBeginExRO::tryWrap); + + if (binding != null && beginEx != null && beginEx.kind() == kind) + { + final String sessionId = sessionId(beginEx); + if (binding.sessions.get(sessionId) instanceof McpLifecycleServer lifecycle) + { + final McpProxyCache.McpListCache cache = cacheOf(binding); + if (cache != null && originId != routedId) + { + newStream = new McpCacheListServer( + lifecycle, + initialId, + affinity, + authorization, + cache)::onServerMessage; + } + else + { + final List prefixes = binding.resolveAll(beginEx, authorization) + .stream() + .map(r -> new McpRoutePrefix(r.id, new String8FW(r.prefix(kind)))) + .toList(); + newStream = new McpListServer( + lifecycle, + initialId, + affinity, + authorization, + prefixes)::onServerMessage; + } + } + } + + return newStream; + } + + protected abstract McpProxyCache.McpListCache cacheOf( + McpBindingConfig binding); + + protected abstract void injectInitialBeginEx( + McpBeginExFW.Builder builder, + String sessionId); + + protected abstract void injectReplyBeginEx( + McpBeginExFW.Builder builder, + String sessionId); + + protected abstract DirectBuffer listReplyOpenPrelude(); + + protected abstract String arrayKey(); + + protected abstract String idKey(); + + protected abstract String sessionId( + McpBeginExFW beginEx); + + private final class McpListClient + { + private final McpListServer server; + private final long resolvedId; + private final String8FW prefix; + private final McpLifecycleClient lifecycle; + private final long initialId; + private final long replyId; + + private MessageConsumer sender; + private int state; + private int replySlot = NO_SLOT; + private int replySlotOffset; + + private long initialSeq; + private long initialAck; + private int initialMax; + private int initialPad; + + private long replySeq; + private long replyAck; + private int replyMax; + private int replyPad; + + private JsonParser decodableJson; + private long decodedParserProgress; // absolute streamOffset of buffer[offset] passed to decode + private int decodeDepth; // JSON nesting depth in the reply envelope + private int decodeItemDepth; // JSON nesting depth within the current item + private int decodeSkipDepth; // JSON nesting depth within a skipped value + private long decodedItemProgress = -1; // streamOffset of last byte emitted within the current item, -1 between items + private McpListClientDecoder decoder = decodeInit; + private String arrayKey; + private String idKey; + + private McpListClient( + McpListServer server, + long resolvedId, + String8FW prefix) + { + this.server = server; + this.resolvedId = resolvedId; + this.prefix = prefix; + this.lifecycle = server.lifecycle.supplyClient(resolvedId); + this.initialId = supplyInitialId.applyAsLong(resolvedId); + this.replyId = supplyReplyId.applyAsLong(initialId); + } + + private void doClientBegin( + long traceId) + { + lifecycle.doClientBegin(traceId); + + final String upstreamSessionId = lifecycle.sessionId; + final String sid = upstreamSessionId != null ? upstreamSessionId : server.lifecycle.sessionId; + final McpBeginExFW beginEx = mcpBeginExRW + .wrap(codecBuffer, 0, codecBuffer.capacity()) + .typeId(mcpTypeId) + .inject(b -> injectInitialBeginEx(b, sid)) + .build(); + + sender = newStream(this::onClientMessage, server.lifecycle.originId, resolvedId, initialId, + initialSeq, initialAck, initialMax, traceId, server.authorization, server.affinity, beginEx); + state = McpState.openingInitial(state); + } + + private void doClientEnd( + long traceId) + { + if (!McpState.initialClosed(state)) + { + doEnd(sender, server.lifecycle.originId, resolvedId, initialId, + initialSeq, initialAck, initialMax, traceId, server.authorization); + state = McpState.closedInitial(state); + } + } + + private void doClientAbort( + long traceId) + { + if (!McpState.initialClosed(state)) + { + doAbort(sender, server.lifecycle.originId, resolvedId, initialId, + initialSeq, initialAck, initialMax, traceId, server.authorization); + state = McpState.closedInitial(state); + } + } + + private void doClientReset( + long traceId) + { + if (!McpState.replyClosed(state)) + { + doReset(sender, server.lifecycle.originId, resolvedId, replyId, + replySeq, replyAck, replyMax, traceId, server.authorization, emptyRO); + state = McpState.closedReply(state); + } + } + + private void doClientWindow( + long traceId, + long budgetId, + int padding) + { + state = McpState.openedReply(state); + doWindow(sender, server.lifecycle.originId, resolvedId, replyId, + replySeq, replyAck, replyMax, traceId, server.authorization, budgetId, padding); + } + + private void flushClientWindow( + long traceId, + long budgetId, + int padding, + long minReplyNoAck, + int minReplyMax) + { + final long newReplyAck = Math.max(replyAck, replySeq - minReplyNoAck); + final int newReplyMax = Math.max(replyMax, minReplyMax); + + if (newReplyAck > replyAck || newReplyMax > replyMax || !McpState.replyOpened(state)) + { + replyAck = newReplyAck; + replyMax = newReplyMax; + doClientWindow(traceId, budgetId, padding); + } + } + + private void onClientMessage( + int msgTypeId, + DirectBuffer buffer, + int index, + int length) + { + switch (msgTypeId) + { + case BeginFW.TYPE_ID: + final BeginFW begin = beginRO.wrap(buffer, index, index + length); + onClientBegin(begin); + break; + case DataFW.TYPE_ID: + final DataFW data = dataRO.wrap(buffer, index, index + length); + onClientData(data); + break; + case EndFW.TYPE_ID: + final EndFW end = endRO.wrap(buffer, index, index + length); + onClientEnd(end); + break; + case AbortFW.TYPE_ID: + final AbortFW abort = abortRO.wrap(buffer, index, index + length); + onClientAbort(abort); + break; + case WindowFW.TYPE_ID: + final WindowFW window = windowRO.wrap(buffer, index, index + length); + onClientWindow(window); + break; + case ResetFW.TYPE_ID: + final ResetFW reset = resetRO.wrap(buffer, index, index + length); + onClientReset(reset); + break; + default: + break; + } + } + + private void onClientBegin( + BeginFW begin) + { + final long sequence = begin.sequence(); + final long acknowledge = begin.acknowledge(); + final long traceId = begin.traceId(); + + replySeq = sequence; + replyAck = acknowledge; + + state = McpState.openedInitial(state); + + flushClientWindow(traceId, 0L, 0, 0L, 0); + } + + private void onClientData( + DataFW data) + { + final long sequence = data.sequence(); + final long acknowledge = data.acknowledge(); + final long traceId = data.traceId(); + final long authorization = data.authorization(); + final long budgetId = data.budgetId(); + final int reserved = data.reserved(); + final OctetsFW payload = data.payload(); + + assert acknowledge <= sequence; + assert sequence >= replySeq; + assert acknowledge <= replyAck; + + replySeq = sequence + reserved; + + assert replyAck <= replySeq; + + DirectBuffer buffer = payload.buffer(); + int offset = payload.offset(); + int limit = payload.limit(); + + if (replySlot != NO_SLOT) + { + final MutableDirectBuffer slot = bufferPool.buffer(replySlot); + if (replySlotOffset + (limit - offset) > slot.capacity()) + { + state = McpState.closedReply(state); + server.onClientError(traceId); + return; + } + slot.putBytes(replySlotOffset, buffer, offset, limit - offset); + replySlotOffset += limit - offset; + + buffer = slot; + offset = 0; + limit = replySlotOffset; + } + + decode(traceId, authorization, budgetId, reserved, buffer, offset, limit); + } + + private void onClientEnd( + EndFW end) + { + final long sequence = end.sequence(); + final long acknowledge = end.acknowledge(); + final long traceId = end.traceId(); + + assert acknowledge <= sequence; + assert sequence >= replySeq; + assert acknowledge <= replyAck; + + replySeq = sequence; + + assert replyAck <= replySeq; + + if (!McpState.replyClosed(state)) + { + state = McpState.closedReply(state); + cleanupClientSlot(); + server.onClientClosed(traceId); + } + } + + private void onClientAbort( + AbortFW abort) + { + final long sequence = abort.sequence(); + final long acknowledge = abort.acknowledge(); + final long traceId = abort.traceId(); + + assert acknowledge <= sequence; + assert sequence >= replySeq; + assert acknowledge <= replyAck; + + replySeq = sequence; + + assert replyAck <= replySeq; + + if (!McpState.replyClosed(state)) + { + state = McpState.closedReply(state); + cleanupClientSlot(); + server.onClientError(traceId); + } + } + + private void onClientWindow( + WindowFW window) + { + final long sequence = window.sequence(); + final long acknowledge = window.acknowledge(); + final long traceId = window.traceId(); + final long budgetId = window.budgetId(); + final int maximum = window.maximum(); + final int padding = window.padding(); + + assert acknowledge <= sequence; + assert sequence <= initialSeq; + assert acknowledge >= initialAck; + assert maximum + acknowledge >= initialMax + initialAck; + + initialAck = acknowledge; + initialMax = maximum; + initialPad = padding; + + assert initialAck <= initialSeq; + + server.flushServerWindow(traceId, budgetId, padding, initialSeq - initialAck, initialMax); + } + + private void onClientReset( + ResetFW reset) + { + final long sequence = reset.sequence(); + final long acknowledge = reset.acknowledge(); + final long traceId = reset.traceId(); + + assert acknowledge <= sequence; + assert sequence <= initialSeq; + assert acknowledge >= initialAck; + + initialAck = acknowledge; + + assert initialAck <= initialSeq; + + state = McpState.closedInitial(state); + if (!McpState.replyClosed(state)) + { + state = McpState.closedReply(state); + server.onClientError(traceId); + } + } + + private void decode( + long traceId, + long authorization, + long budgetId, + int reserved, + DirectBuffer buffer, + int offset, + int limit) + { + if (decodableJson != null) + { + final int delta = (int) (decodableJson.getLocation().getStreamOffset() - decodedParserProgress); + inputRO.wrap(buffer, offset + delta, limit - offset - delta); + } + + McpListClientDecoder previous = null; + int progress = offset; + while (progress <= limit && previous != decoder) + { + previous = decoder; + progress = decoder.decode(this, traceId, authorization, budgetId, reserved, + buffer, offset, progress, limit); + } + + final int compactBoundaryInBuf; + if (decodedItemProgress >= 0) + { + compactBoundaryInBuf = offset + (int) (decodedItemProgress - decodedParserProgress); + } + else + { + compactBoundaryInBuf = offset + (int) (decodableJson.getLocation().getStreamOffset() - decodedParserProgress); + } + + if (compactBoundaryInBuf < limit) + { + final int retained = limit - compactBoundaryInBuf; + if (replySlot == NO_SLOT) + { + replySlot = bufferPool.acquire(initialId); + if (replySlot == NO_SLOT) + { + state = McpState.closedReply(state); + server.onClientError(traceId); + return; + } + } + final MutableDirectBuffer slot = bufferPool.buffer(replySlot); + if (retained > slot.capacity()) + { + state = McpState.closedReply(state); + server.onClientError(traceId); + return; + } + slot.putBytes(0, buffer, compactBoundaryInBuf, retained); + replySlotOffset = retained; + decodedParserProgress += compactBoundaryInBuf - offset; + } + else + { + cleanupClientSlot(); + decodedParserProgress += compactBoundaryInBuf - offset; + } + } + + private void decode( + long traceId) + { + if (replySlot != NO_SLOT) + { + final MutableDirectBuffer slot = bufferPool.buffer(replySlot); + decode(traceId, server.authorization, 0L, 0, slot, 0, replySlotOffset); + } + } + + private void cleanupClientSlot() + { + if (replySlot != NO_SLOT) + { + bufferPool.release(replySlot); + replySlot = NO_SLOT; + replySlotOffset = 0; + } + } + } + + @FunctionalInterface + private interface McpListClientDecoder + { + int decode( + McpListClient client, + long traceId, + long authorization, + long budgetId, + int reserved, + DirectBuffer buffer, + int offset, + int progress, + int limit); + } + + private int decodeInit( + McpListClient client, + long traceId, + long authorization, + long budgetId, + int reserved, + DirectBuffer buffer, + int offset, + int progress, + int limit) + { + if (listItemParserFactory == null) + { + client.decoder = decodeIgnore; + return limit; + } + + inputRO.wrap(buffer, progress, limit - progress); + client.decodableJson = listItemParserFactory.createParser(inputRO); + client.arrayKey = arrayKey(); + client.idKey = idKey(); + client.decoder = decodeReply; + + return progress; + } + + private int decodeReply( + McpListClient client, + long traceId, + long authorization, + long budgetId, + int reserved, + DirectBuffer buffer, + int offset, + int progress, + int limit) + { + final JsonParser parser = client.decodableJson; + + decode: + while (parser.hasNext()) + { + final JsonParser.Event event = parser.next(); + if (event == JsonParser.Event.START_OBJECT) + { + client.decodeDepth = 1; + client.decoder = decodeItemsKey; + break decode; + } + } + + return offset + (int) (parser.getLocation().getStreamOffset() - client.decodedParserProgress); + } + + private int decodeItemsKey( + McpListClient client, + long traceId, + long authorization, + long budgetId, + int reserved, + DirectBuffer buffer, + int offset, + int progress, + int limit) + { + final JsonParser parser = client.decodableJson; + + decode: + while (parser.hasNext()) + { + final JsonParser.Event event = parser.next(); + switch (event) + { + case KEY_NAME: + if (client.decodeDepth == 1) + { + final String key = parser.getString(); + if (client.arrayKey.equals(key)) + { + client.decoder = decodeItems; + } + else + { + client.decodeSkipDepth = 0; + client.decoder = decodeSkipObject; + } + break decode; + } + break; + case END_OBJECT: + client.decodeDepth--; + if (client.decodeDepth == 0) + { + client.decoder = decodeIgnore; + break decode; + } + break; + default: + break; + } + } + + return offset + (int) (parser.getLocation().getStreamOffset() - client.decodedParserProgress); + } + + private int decodeSkipObject( + McpListClient client, + long traceId, + long authorization, + long budgetId, + int reserved, + DirectBuffer buffer, + int offset, + int progress, + int limit) + { + final JsonParser parser = client.decodableJson; + + decode: + while (parser.hasNext()) + { + final JsonParser.Event event = parser.next(); + switch (event) + { + case START_OBJECT: + case START_ARRAY: + client.decodeSkipDepth++; + break; + case END_OBJECT: + case END_ARRAY: + client.decodeSkipDepth--; + if (client.decodeSkipDepth == 0) + { + client.decoder = decodeItemsKey; + break decode; + } + break; + case VALUE_STRING: + case VALUE_NUMBER: + case VALUE_TRUE: + case VALUE_FALSE: + case VALUE_NULL: + if (client.decodeSkipDepth == 0) + { + client.decoder = decodeItemsKey; + break decode; + } + break; + default: + break; + } + } + + return offset + (int) (parser.getLocation().getStreamOffset() - client.decodedParserProgress); + } + + private int decodeItems( + McpListClient client, + long traceId, + long authorization, + long budgetId, + int reserved, + DirectBuffer buffer, + int offset, + int progress, + int limit) + { + final JsonParser parser = client.decodableJson; + + decode: + while (parser.hasNext()) + { + final JsonParser.Event event = parser.next(); + if (event == JsonParser.Event.START_ARRAY) + { + client.decodeItemDepth = 0; + client.decoder = decodeItemStart; + break decode; + } + } + + return offset + (int) (parser.getLocation().getStreamOffset() - client.decodedParserProgress); + } + + private int decodeItemStart( + McpListClient client, + long traceId, + long authorization, + long budgetId, + int reserved, + DirectBuffer buffer, + int offset, + int progress, + int limit) + { + final JsonParser parser = client.decodableJson; + + decode: + while (parser.hasNext()) + { + final long decodedItemProgress = parser.getLocation().getStreamOffset(); + final JsonParser.Event event = parser.next(); + switch (event) + { + case START_OBJECT: + client.decodedItemProgress = decodedItemProgress - 1; + client.server.streamItemBegin(traceId); + client.decodeItemDepth = 1; + client.decoder = decodeItemBody; + break decode; + case END_ARRAY: + client.decodeDepth--; + client.decoder = decodeItemsKey; + break decode; + default: + break; + } + } + + return offset + (int) (parser.getLocation().getStreamOffset() - client.decodedParserProgress); + } + + private int decodeItemBody( + McpListClient client, + long traceId, + long authorization, + long budgetId, + int reserved, + DirectBuffer buffer, + int offset, + int progress, + int limit) + { + final JsonParser parser = client.decodableJson; + + decode: + while (true) + { + final long decodedItemProgress = parser.getLocation().getStreamOffset(); + if (client.decodedItemProgress < decodedItemProgress) + { + final int decodedOffset = + offset + (int) (client.decodedItemProgress - client.decodedParserProgress); + final int decodedLimit = offset + (int) (decodedItemProgress - client.decodedParserProgress); + final int chunkLen = decodedLimit - decodedOffset; + final int decodedProgress = client.server.streamItemChunk(buffer, decodedOffset, chunkLen, traceId); + client.decodedItemProgress += decodedProgress; + if (decodedProgress < chunkLen) + { + break decode; + } + } + + if (!parser.hasNext()) + { + break decode; + } + final long decodedEventProgress = parser.getLocation().getStreamOffset(); + final JsonParser.Event event = parser.next(); + switch (event) + { + case START_OBJECT: + case START_ARRAY: + client.decodeItemDepth++; + break; + case END_OBJECT: + client.decodeItemDepth--; + if (client.decodeItemDepth == 0) + { + final int decodedLimit = offset + (int) (decodedEventProgress - client.decodedParserProgress); + final int decodedOffset = + offset + (int) (client.decodedItemProgress - client.decodedParserProgress); + final int chunkLen = decodedLimit - decodedOffset; + final int decodedProgress = client.server.streamItemChunk(buffer, decodedOffset, chunkLen, traceId); + client.decodedItemProgress += decodedProgress; + if (decodedProgress < chunkLen) + { + client.decoder = decodeItemFinalize; + break decode; + } + client.server.streamItemEnd(traceId); + client.decodedItemProgress = -1; + client.decoder = decodeItemStart; + break decode; + } + break; + case END_ARRAY: + client.decodeItemDepth--; + break; + case KEY_NAME: + if (client.decodeItemDepth == 1 && + client.prefix.length() > 0 && + client.idKey.equals(parser.getString())) + { + client.decoder = decodeItemId; + break decode; + } + break; + default: + break; + } + } + + return offset + (int) (parser.getLocation().getStreamOffset() - client.decodedParserProgress); + } + + private int decodeItemId( + McpListClient client, + long traceId, + long authorization, + long budgetId, + int reserved, + DirectBuffer buffer, + int offset, + int progress, + int limit) + { + final JsonParser parser = client.decodableJson; + + final long decodedKeyProgress = parser.getLocation().getStreamOffset(); + if (client.decodedItemProgress < decodedKeyProgress) + { + final int decodedOffset = + offset + (int) (client.decodedItemProgress - client.decodedParserProgress); + final int decodedLimit = offset + (int) (decodedKeyProgress - client.decodedParserProgress); + final int chunkLen = decodedLimit - decodedOffset; + final int decodedProgress = client.server.streamItemChunk(buffer, decodedOffset, chunkLen, traceId); + client.decodedItemProgress += decodedProgress; + if (decodedProgress < chunkLen) + { + return offset + (int) (client.decodedItemProgress - client.decodedParserProgress); + } + } + + if (parser.hasNext()) + { + final long decodedValueProgress = parser.getLocation().getStreamOffset(); + final JsonParser.Event event = parser.next(); + if (event == JsonParser.Event.VALUE_STRING) + { + final int decodedKeyOffset = offset + (int) (decodedKeyProgress - client.decodedParserProgress); + final int decodedValueOffset = offset + (int) (decodedValueProgress - client.decodedParserProgress); + final int decodedOpenQuote = indexOfByte(buffer, decodedKeyOffset, decodedValueOffset, (byte) '"'); + final int decodedContent = (decodedOpenQuote != -1 ? decodedOpenQuote : decodedValueOffset) + 1; + final int decodedOffset = + offset + (int) (client.decodedItemProgress - client.decodedParserProgress); + client.server.streamItemChunk(buffer, decodedOffset, decodedContent - decodedOffset, traceId); + client.server.streamItemChunk(client.prefix.value(), 0, client.prefix.length(), traceId); + client.decodedItemProgress = + client.decodedParserProgress + (long) (decodedContent - offset); + } + client.decoder = decodeItemBody; + } + + return offset + (int) (parser.getLocation().getStreamOffset() - client.decodedParserProgress); + } + + private int decodeItemFinalize( + McpListClient client, + long traceId, + long authorization, + long budgetId, + int reserved, + DirectBuffer buffer, + int offset, + int progress, + int limit) + { + final JsonParser parser = client.decodableJson; + final long decodedItemProgress = parser.getLocation().getStreamOffset(); + + if (client.decodedItemProgress < decodedItemProgress) + { + final int decodedOffset = + offset + (int) (client.decodedItemProgress - client.decodedParserProgress); + final int decodedLimit = offset + (int) (decodedItemProgress - client.decodedParserProgress); + final int chunkLen = decodedLimit - decodedOffset; + final int decodedProgress = client.server.streamItemChunk(buffer, decodedOffset, chunkLen, traceId); + client.decodedItemProgress += decodedProgress; + if (decodedProgress < chunkLen) + { + return offset + (int) (client.decodedItemProgress - client.decodedParserProgress); + } + } + + client.server.streamItemEnd(traceId); + client.decodedItemProgress = -1; + client.decoder = decodeItemStart; + + return offset + (int) (decodedItemProgress - client.decodedParserProgress); + } + + private int decodeIgnore( + McpListClient client, + long traceId, + long authorization, + long budgetId, + int reserved, + DirectBuffer buffer, + int offset, + int progress, + int limit) + { + return limit; + } + + private final class McpListServer + { + private final McpLifecycleServer lifecycle; + private final long initialId; + private final long replyId; + private final long affinity; + private final long authorization; + private final Deque remaining; + + private int state; + private int itemsEmitted; + private McpListClient client; + + private long initialSeq; + private long initialAck; + private int initialMax; + private int initialPad; + + private long replySeq; + private long replyAck; + private int replyMax; + private int replyPad; + + private McpListServer( + McpLifecycleServer lifecycle, + long initialId, + long affinity, + long authorization, + List prefixes) + { + this.lifecycle = lifecycle; + this.initialId = initialId; + this.replyId = supplyReplyId.applyAsLong(initialId); + this.affinity = affinity; + this.authorization = authorization; + this.remaining = new ArrayDeque<>(prefixes); + } + + private void onServerMessage( + int msgTypeId, + DirectBuffer buffer, + int index, + int length) + { + switch (msgTypeId) + { + case BeginFW.TYPE_ID: + final BeginFW begin = beginRO.wrap(buffer, index, index + length); + onServerBegin(begin); + break; + case EndFW.TYPE_ID: + final EndFW end = endRO.wrap(buffer, index, index + length); + onServerEnd(end); + break; + case AbortFW.TYPE_ID: + final AbortFW abort = abortRO.wrap(buffer, index, index + length); + onServerAbort(abort); + break; + case WindowFW.TYPE_ID: + final WindowFW window = windowRO.wrap(buffer, index, index + length); + onServerWindow(window); + break; + case ResetFW.TYPE_ID: + final ResetFW reset = resetRO.wrap(buffer, index, index + length); + onServerReset(reset); + break; + default: + break; + } + } + + private void onServerBegin( + BeginFW begin) + { + final long sequence = begin.sequence(); + final long acknowledge = begin.acknowledge(); + final long traceId = begin.traceId(); + + initialSeq = sequence; + initialAck = acknowledge; + + state = McpState.openingInitial(state); + + flushServerWindow(traceId, 0L, 0, 0L, 0); + + doServerBegin(traceId); + doEncodeBeginItems(traceId); + onNextClient(traceId); + } + + private void onServerEnd( + EndFW end) + { + final long sequence = end.sequence(); + final long acknowledge = end.acknowledge(); + final long traceId = end.traceId(); + + assert acknowledge <= sequence; + assert sequence >= initialSeq; + assert acknowledge <= initialAck; + + initialSeq = sequence; + + assert initialAck <= initialSeq; + + state = McpState.closedInitial(state); + + if (client != null) + { + client.doClientEnd(traceId); + } + } + + private void onServerAbort( + AbortFW abort) + { + final long sequence = abort.sequence(); + final long acknowledge = abort.acknowledge(); + final long traceId = abort.traceId(); + + assert acknowledge <= sequence; + assert sequence >= initialSeq; + assert acknowledge <= initialAck; + + initialSeq = sequence; + + assert initialAck <= initialSeq; + + state = McpState.closedInitial(state); + + if (client != null) + { + client.doClientAbort(traceId); + } + remaining.clear(); + } + + private void onServerWindow( + WindowFW window) + { + final long sequence = window.sequence(); + final long acknowledge = window.acknowledge(); + final long traceId = window.traceId(); + final long budgetId = window.budgetId(); + final int maximum = window.maximum(); + final int padding = window.padding(); + + assert acknowledge <= sequence; + assert sequence <= replySeq; + assert acknowledge >= replyAck; + assert maximum + acknowledge >= replyMax + replyAck; + + replyAck = acknowledge; + replyMax = maximum; + replyPad = padding; + + assert replyAck <= replySeq; + + if (client != null) + { + client.decode(traceId); + client.flushClientWindow(traceId, budgetId, padding, replySeq - replyAck, replyMax); + } + } + + private void onServerReset( + ResetFW reset) + { + final long sequence = reset.sequence(); + final long acknowledge = reset.acknowledge(); + final long traceId = reset.traceId(); + + assert acknowledge <= sequence; + assert sequence <= replySeq; + assert acknowledge >= replyAck; + + replyAck = acknowledge; + + assert replyAck <= replySeq; + + state = McpState.closedReply(state); + + if (client != null) + { + client.doClientReset(traceId); + } + remaining.clear(); + } + + private void onClientClosed( + long traceId) + { + client = null; + onNextClient(traceId); + } + + private void onClientError( + long traceId) + { + client = null; + remaining.clear(); + doServerAbort(traceId); + } + + private void onNextClient( + long traceId) + { + final McpRoutePrefix route = remaining.poll(); + if (route == null) + { + doEncodeEndItems(traceId); + return; + } + client = new McpListClient(this, route.resolvedId(), route.prefix()); + client.doClientBegin(traceId); + if (McpState.initialClosed(state)) + { + client.doClientEnd(traceId); + } + } + + private void streamItemBegin( + long traceId) + { + if (itemsEmitted > 0) + { + doServerData(traceId, 0L, 0x03, listReplySeparatorRO.capacity(), + listReplySeparatorRO, 0, listReplySeparatorRO.capacity()); + } + itemsEmitted++; + } + + private int streamItemChunk( + DirectBuffer buffer, + int offset, + int length, + long traceId) + { + final int replyWin = replyMax - (int) (replySeq - replyAck) - replyPad; + final int emit = Math.min(Math.max(replyWin, 0), length); + if (emit > 0) + { + doServerData(traceId, 0L, 0x03, emit, buffer, offset, emit); + } + return emit; + } + + private void streamItemEnd( + long traceId) + { + } + + private void doEncodeBeginItems( + long traceId) + { + final DirectBuffer prelude = listReplyOpenPrelude(); + doServerData(traceId, 0L, 0x03, prelude.capacity(), prelude, 0, prelude.capacity()); + } + + private void doEncodeEndItems( + long traceId) + { + doServerData(traceId, 0L, 0x03, listReplyCloseRO.capacity(), + listReplyCloseRO, 0, listReplyCloseRO.capacity()); + doServerEnd(traceId); + } + + private void doServerBegin( + long traceId) + { + final String sid = lifecycle.sessionId; + final McpBeginExFW beginEx = mcpBeginExRW + .wrap(codecBuffer, 0, codecBuffer.capacity()) + .typeId(mcpTypeId) + .inject(b -> injectReplyBeginEx(b, sid)) + .build(); + + doBegin(lifecycle.sender, lifecycle.originId, lifecycle.routedId, replyId, replySeq, replyAck, replyMax, + traceId, authorization, affinity, beginEx); + state = McpState.openedReply(state); + } + + private void doServerData( + long traceId, + long budgetId, + int flags, + int reserved, + DirectBuffer payload, + int offset, + int length) + { + doData(lifecycle.sender, lifecycle.originId, lifecycle.routedId, replyId, replySeq, replyAck, replyMax, + traceId, authorization, flags, budgetId, reserved, payload, offset, length); + replySeq += reserved; + } + + private void doServerEnd( + long traceId) + { + if (!McpState.replyClosed(state)) + { + doEnd(lifecycle.sender, lifecycle.originId, lifecycle.routedId, replyId, replySeq, replyAck, replyMax, + traceId, authorization); + state = McpState.closedReply(state); + } + } + + private void doServerAbort( + long traceId) + { + if (!McpState.replyClosed(state)) + { + doAbort(lifecycle.sender, lifecycle.originId, lifecycle.routedId, replyId, replySeq, replyAck, replyMax, + traceId, authorization); + state = McpState.closedReply(state); + } + } + + private void doServerWindow( + long traceId, + long budgetId, + int padding) + { + state = McpState.openedInitial(state); + doWindow(lifecycle.sender, lifecycle.originId, lifecycle.routedId, initialId, initialSeq, initialAck, initialMax, + traceId, authorization, budgetId, padding); + } + + private void flushServerWindow( + long traceId, + long budgetId, + int padding, + long minInitialNoAck, + int minInitialMax) + { + final long newInitialAck = Math.max(initialAck, initialSeq - minInitialNoAck); + final int newInitialMax = Math.max(initialMax, minInitialMax); + + if (newInitialAck > initialAck || newInitialMax > initialMax || !McpState.initialOpened(state)) + { + initialAck = newInitialAck; + initialMax = newInitialMax; + doServerWindow(traceId, budgetId, padding); + } + } + } + + private final class McpCacheListServer + { + private final McpLifecycleServer lifecycle; + private final long initialId; + private final long replyId; + private final long affinity; + private final long authorization; + private final McpProxyCache.McpListCache cache; + + private int state; + private boolean fetched; + private DirectBuffer cachedBuf; + private int cachedLen; + private int emitOffset; + + private long initialSeq; + private long initialAck; + private int initialMax; + + private long replySeq; + private long replyAck; + private int replyMax; + private int replyPad; + + private McpCacheListServer( + McpLifecycleServer lifecycle, + long initialId, + long affinity, + long authorization, + McpProxyCache.McpListCache cache) + { + this.lifecycle = lifecycle; + this.initialId = initialId; + this.replyId = supplyReplyId.applyAsLong(initialId); + this.affinity = affinity; + this.authorization = authorization; + this.cache = cache; + } + + private void onServerMessage( + int msgTypeId, + DirectBuffer buffer, + int index, + int length) + { + switch (msgTypeId) + { + case BeginFW.TYPE_ID: + onServerBegin(beginRO.wrap(buffer, index, index + length)); + break; + case EndFW.TYPE_ID: + onServerEnd(endRO.wrap(buffer, index, index + length)); + break; + case AbortFW.TYPE_ID: + onServerAbort(abortRO.wrap(buffer, index, index + length)); + break; + case WindowFW.TYPE_ID: + onServerWindow(windowRO.wrap(buffer, index, index + length)); + break; + case ResetFW.TYPE_ID: + onServerReset(resetRO.wrap(buffer, index, index + length)); + break; + default: + break; + } + } + + private void onServerBegin( + BeginFW begin) + { + final long traceId = begin.traceId(); + + initialSeq = begin.sequence(); + initialAck = begin.acknowledge(); + state = McpState.openingInitial(state); + + doServerBegin(traceId); + doServerWindow(traceId, 0L, 0); + cache.get(this::onStoreResult); + } + + private void onStoreResult( + String key, + String value) + { + fetched = true; + if (value != null) + { + final byte[] bytes = value.getBytes(StandardCharsets.UTF_8); + cachedBuf = new UnsafeBuffer(bytes); + cachedLen = bytes.length; + } + emitIfReady(supplyTraceId.getAsLong()); + } + + private void onServerEnd( + EndFW end) + { + initialSeq = end.sequence(); + state = McpState.closedInitial(state); + emitIfReady(end.traceId()); + } + + private void onServerAbort( + AbortFW abort) + { + initialSeq = abort.sequence(); + state = McpState.closedInitial(state); + doServerAbort(abort.traceId()); + } + + private void onServerWindow( + WindowFW window) + { + replyAck = window.acknowledge(); + replyMax = window.maximum(); + replyPad = window.padding(); + state = McpState.openedReply(state); + emitIfReady(window.traceId()); + } + + private void onServerReset( + ResetFW reset) + { + replyAck = reset.acknowledge(); + state = McpState.closedReply(state); + } + + private void emitIfReady( + long traceId) + { + if (!fetched || McpState.replyClosed(state)) + { + return; + } + + if (cachedBuf == null) + { + doServerAbort(traceId); + return; + } + + while (emitOffset < cachedLen) + { + final int replyWin = replyMax - (int) (replySeq - replyAck) - replyPad; + if (replyWin <= 0) + { + return; + } + final int chunkLen = Math.min(replyWin, cachedLen - emitOffset); + doServerData(traceId, 0L, 0x03, chunkLen, cachedBuf, emitOffset, chunkLen); + emitOffset += chunkLen; + } + + doServerEnd(traceId); + } + + private void doServerBegin( + long traceId) + { + final String sid = lifecycle.sessionId; + final McpBeginExFW beginEx = mcpBeginExRW + .wrap(codecBuffer, 0, codecBuffer.capacity()) + .typeId(mcpTypeId) + .inject(b -> injectReplyBeginEx(b, sid)) + .build(); + + doBegin(lifecycle.sender, lifecycle.originId, lifecycle.routedId, replyId, replySeq, replyAck, replyMax, + traceId, authorization, affinity, beginEx); + state = McpState.openingReply(state); + } + + private void doServerData( + long traceId, + long budgetId, + int flags, + int reserved, + DirectBuffer payload, + int offset, + int length) + { + doData(lifecycle.sender, lifecycle.originId, lifecycle.routedId, replyId, replySeq, replyAck, replyMax, + traceId, authorization, flags, budgetId, reserved, payload, offset, length); + replySeq += reserved; + } + + private void doServerEnd( + long traceId) + { + if (!McpState.replyClosed(state)) + { + doEnd(lifecycle.sender, lifecycle.originId, lifecycle.routedId, replyId, replySeq, replyAck, replyMax, + traceId, authorization); + state = McpState.closedReply(state); + } + } + + private void doServerAbort( + long traceId) + { + if (!McpState.replyClosed(state)) + { + doAbort(lifecycle.sender, lifecycle.originId, lifecycle.routedId, replyId, replySeq, replyAck, replyMax, + traceId, authorization); + state = McpState.closedReply(state); + } + } + + private void doServerWindow( + long traceId, + long budgetId, + int padding) + { + state = McpState.openedInitial(state); + doWindow(lifecycle.sender, lifecycle.originId, lifecycle.routedId, initialId, initialSeq, initialAck, initialMax, + traceId, authorization, budgetId, padding); + } + } + + private static int indexOfByte( + DirectBuffer buffer, + int offset, + int limit, + byte value) + { + for (int cursor = offset; cursor < limit; cursor++) + { + if (buffer.getByte(cursor) == value) + { + return cursor; + } + } + + return -1; + } + + private MessageConsumer newStream( + MessageConsumer sender, + long originId, + long routedId, + long streamId, + long sequence, + long acknowledge, + int maximum, + long traceId, + long authorization, + long affinity, + Flyweight extension) + { + final BeginFW begin = beginRW.wrap(writeBuffer, 0, writeBuffer.capacity()) + .originId(originId) + .routedId(routedId) + .streamId(streamId) + .sequence(sequence) + .acknowledge(acknowledge) + .maximum(maximum) + .traceId(traceId) + .authorization(authorization) + .affinity(affinity) + .extension(extension.buffer(), extension.offset(), extension.sizeof()) + .build(); + + final MessageConsumer receiver = + streamFactory.newStream(begin.typeId(), begin.buffer(), begin.offset(), begin.sizeof(), sender); + assert receiver != null; + + receiver.accept(begin.typeId(), begin.buffer(), begin.offset(), begin.sizeof()); + + return receiver; + } + + private void doBegin( + MessageConsumer receiver, + long originId, + long routedId, + long streamId, + long sequence, + long acknowledge, + int maximum, + long traceId, + long authorization, + long affinity, + Flyweight extension) + { + final BeginFW begin = beginRW.wrap(writeBuffer, 0, writeBuffer.capacity()) + .originId(originId) + .routedId(routedId) + .streamId(streamId) + .sequence(sequence) + .acknowledge(acknowledge) + .maximum(maximum) + .traceId(traceId) + .authorization(authorization) + .affinity(affinity) + .extension(extension.buffer(), extension.offset(), extension.sizeof()) + .build(); + + receiver.accept(begin.typeId(), begin.buffer(), begin.offset(), begin.sizeof()); + } + + private void doData( + MessageConsumer receiver, + long originId, + long routedId, + long streamId, + long sequence, + long acknowledge, + int maximum, + long traceId, + long authorization, + int flags, + long budgetId, + int reserved, + DirectBuffer payload, + int offset, + int length) + { + final DataFW data = dataRW.wrap(writeBuffer, 0, writeBuffer.capacity()) + .originId(originId) + .routedId(routedId) + .streamId(streamId) + .sequence(sequence) + .acknowledge(acknowledge) + .maximum(maximum) + .traceId(traceId) + .authorization(authorization) + .flags(flags) + .budgetId(budgetId) + .reserved(reserved) + .payload(payload, offset, length) + .build(); + + receiver.accept(data.typeId(), data.buffer(), data.offset(), data.sizeof()); + } + + private void doEnd( + MessageConsumer receiver, + long originId, + long routedId, + long streamId, + long sequence, + long acknowledge, + int maximum, + long traceId, + long authorization) + { + final EndFW end = endRW.wrap(writeBuffer, 0, writeBuffer.capacity()) + .originId(originId) + .routedId(routedId) + .streamId(streamId) + .sequence(sequence) + .acknowledge(acknowledge) + .maximum(maximum) + .traceId(traceId) + .authorization(authorization) + .build(); + + receiver.accept(end.typeId(), end.buffer(), end.offset(), end.sizeof()); + } + + private void doAbort( + MessageConsumer receiver, + long originId, + long routedId, + long streamId, + long sequence, + long acknowledge, + int maximum, + long traceId, + long authorization) + { + final AbortFW abort = abortRW.wrap(writeBuffer, 0, writeBuffer.capacity()) + .originId(originId) + .routedId(routedId) + .streamId(streamId) + .sequence(sequence) + .acknowledge(acknowledge) + .maximum(maximum) + .traceId(traceId) + .authorization(authorization) + .build(); + + receiver.accept(abort.typeId(), abort.buffer(), abort.offset(), abort.sizeof()); + } + + private void doReset( + MessageConsumer receiver, + long originId, + long routedId, + long streamId, + long sequence, + long acknowledge, + int maximum, + long traceId, + long authorization, + Flyweight extension) + { + final ResetFW reset = resetRW.wrap(writeBuffer, 0, writeBuffer.capacity()) + .originId(originId) + .routedId(routedId) + .streamId(streamId) + .sequence(sequence) + .acknowledge(acknowledge) + .maximum(maximum) + .traceId(traceId) + .authorization(authorization) + .extension(extension.buffer(), extension.offset(), extension.sizeof()) + .build(); + + receiver.accept(reset.typeId(), reset.buffer(), reset.offset(), reset.sizeof()); + } + + private void doWindow( + MessageConsumer receiver, + long originId, + long routedId, + long streamId, + long sequence, + long acknowledge, + int maximum, + long traceId, + long authorization, + long budgetId, + int padding) + { + final WindowFW window = windowRW.wrap(writeBuffer, 0, writeBuffer.capacity()) + .originId(originId) + .routedId(routedId) + .streamId(streamId) + .sequence(sequence) + .acknowledge(acknowledge) + .maximum(maximum) + .traceId(traceId) + .authorization(authorization) + .budgetId(budgetId) + .padding(padding) + .build(); + + receiver.accept(window.typeId(), window.buffer(), window.offset(), window.sizeof()); + } +} diff --git a/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/McpProxyPromptsGetFactory.java b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/McpProxyPromptsGetFactory.java new file mode 100644 index 0000000000..88255f1aff --- /dev/null +++ b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/McpProxyPromptsGetFactory.java @@ -0,0 +1,58 @@ +/* + * Copyright 2021-2024 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. + */ +package io.aklivity.zilla.runtime.binding.mcp.internal.stream; + +import java.util.function.LongFunction; + +import io.aklivity.zilla.runtime.binding.mcp.internal.McpConfiguration; +import io.aklivity.zilla.runtime.binding.mcp.internal.config.McpBindingConfig; +import io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.McpBeginExFW; +import io.aklivity.zilla.runtime.engine.EngineContext; + +final class McpProxyPromptsGetFactory extends McpProxyItemFactory +{ + McpProxyPromptsGetFactory( + McpConfiguration config, + EngineContext context, + LongFunction supplyBinding) + { + super(config, context, supplyBinding, McpBeginExFW.KIND_PROMPTS_GET); + } + + @Override + protected void injectInitialBeginEx( + McpBeginExFW.Builder builder, + String sessionId, + String identifier) + { + builder.promptsGet(p -> p.sessionId(sessionId).name(identifier)); + } + + @Override + protected void injectReplyBeginEx( + McpBeginExFW.Builder builder, + String sessionId, + McpBeginExFW upstream) + { + builder.promptsGet(p -> p.sessionId(sessionId).name(upstream.promptsGet().name().asString())); + } + + @Override + protected String sessionId( + McpBeginExFW beginEx) + { + return beginEx.promptsGet().sessionId().asString(); + } +} diff --git a/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/McpProxyPromptsListFactory.java b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/McpProxyPromptsListFactory.java new file mode 100644 index 0000000000..da0c321778 --- /dev/null +++ b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/McpProxyPromptsListFactory.java @@ -0,0 +1,94 @@ +/* + * Copyright 2021-2024 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. + */ +package io.aklivity.zilla.runtime.binding.mcp.internal.stream; + +import static io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.McpBeginExFW.KIND_PROMPTS_LIST; + +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.function.LongFunction; + +import org.agrona.DirectBuffer; +import org.agrona.concurrent.UnsafeBuffer; + +import io.aklivity.zilla.runtime.binding.mcp.internal.McpConfiguration; +import io.aklivity.zilla.runtime.binding.mcp.internal.config.McpBindingConfig; +import io.aklivity.zilla.runtime.binding.mcp.internal.stream.cache.McpProxyCache; +import io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.McpBeginExFW; +import io.aklivity.zilla.runtime.engine.EngineContext; + +final class McpProxyPromptsListFactory extends McpProxyListFactory +{ + private static final List PROMPTS_LIST_ITEM_JSON_PATH_INCLUDES = List.of("/prompts/-/name"); + + private final DirectBuffer prelude = + new UnsafeBuffer("{\"prompts\":[".getBytes(StandardCharsets.UTF_8)); + + McpProxyPromptsListFactory( + McpConfiguration config, + EngineContext context, + LongFunction supplyBinding) + { + super(config, context, supplyBinding, McpBeginExFW.KIND_PROMPTS_LIST, PROMPTS_LIST_ITEM_JSON_PATH_INCLUDES); + } + + @Override + protected McpProxyCache.McpListCache cacheOf( + McpBindingConfig binding) + { + return binding.cache != null ? binding.cache.cacheOf(KIND_PROMPTS_LIST) : null; + } + + @Override + protected void injectInitialBeginEx( + McpBeginExFW.Builder builder, + String sessionId) + { + builder.promptsList(p -> p.sessionId(sessionId)); + } + + @Override + protected void injectReplyBeginEx( + McpBeginExFW.Builder builder, + String sessionId) + { + builder.promptsList(p -> p.sessionId(sessionId)); + } + + @Override + protected DirectBuffer listReplyOpenPrelude() + { + return prelude; + } + + @Override + protected String arrayKey() + { + return "prompts"; + } + + @Override + protected String idKey() + { + return "name"; + } + + @Override + protected String sessionId( + McpBeginExFW beginEx) + { + return beginEx.promptsList().sessionId().asString(); + } +} diff --git a/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/McpProxyResourcesListFactory.java b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/McpProxyResourcesListFactory.java new file mode 100644 index 0000000000..aa99e76404 --- /dev/null +++ b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/McpProxyResourcesListFactory.java @@ -0,0 +1,94 @@ +/* + * Copyright 2021-2024 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. + */ +package io.aklivity.zilla.runtime.binding.mcp.internal.stream; + +import static io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.McpBeginExFW.KIND_RESOURCES_LIST; + +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.function.LongFunction; + +import org.agrona.DirectBuffer; +import org.agrona.concurrent.UnsafeBuffer; + +import io.aklivity.zilla.runtime.binding.mcp.internal.McpConfiguration; +import io.aklivity.zilla.runtime.binding.mcp.internal.config.McpBindingConfig; +import io.aklivity.zilla.runtime.binding.mcp.internal.stream.cache.McpProxyCache; +import io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.McpBeginExFW; +import io.aklivity.zilla.runtime.engine.EngineContext; + +final class McpProxyResourcesListFactory extends McpProxyListFactory +{ + private static final List RESOURCES_LIST_ITEM_JSON_PATH_INCLUDES = List.of("/resources/-/uri"); + + private final DirectBuffer prelude = + new UnsafeBuffer("{\"resources\":[".getBytes(StandardCharsets.UTF_8)); + + McpProxyResourcesListFactory( + McpConfiguration config, + EngineContext context, + LongFunction supplyBinding) + { + super(config, context, supplyBinding, McpBeginExFW.KIND_RESOURCES_LIST, RESOURCES_LIST_ITEM_JSON_PATH_INCLUDES); + } + + @Override + protected McpProxyCache.McpListCache cacheOf( + McpBindingConfig binding) + { + return binding.cache != null ? binding.cache.cacheOf(KIND_RESOURCES_LIST) : null; + } + + @Override + protected void injectInitialBeginEx( + McpBeginExFW.Builder builder, + String sessionId) + { + builder.resourcesList(r -> r.sessionId(sessionId)); + } + + @Override + protected void injectReplyBeginEx( + McpBeginExFW.Builder builder, + String sessionId) + { + builder.resourcesList(r -> r.sessionId(sessionId)); + } + + @Override + protected DirectBuffer listReplyOpenPrelude() + { + return prelude; + } + + @Override + protected String arrayKey() + { + return "resources"; + } + + @Override + protected String idKey() + { + return "uri"; + } + + @Override + protected String sessionId( + McpBeginExFW beginEx) + { + return beginEx.resourcesList().sessionId().asString(); + } +} diff --git a/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/McpProxyResourcesReadFactory.java b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/McpProxyResourcesReadFactory.java new file mode 100644 index 0000000000..7bae5826c7 --- /dev/null +++ b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/McpProxyResourcesReadFactory.java @@ -0,0 +1,58 @@ +/* + * Copyright 2021-2024 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. + */ +package io.aklivity.zilla.runtime.binding.mcp.internal.stream; + +import java.util.function.LongFunction; + +import io.aklivity.zilla.runtime.binding.mcp.internal.McpConfiguration; +import io.aklivity.zilla.runtime.binding.mcp.internal.config.McpBindingConfig; +import io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.McpBeginExFW; +import io.aklivity.zilla.runtime.engine.EngineContext; + +final class McpProxyResourcesReadFactory extends McpProxyItemFactory +{ + McpProxyResourcesReadFactory( + McpConfiguration config, + EngineContext context, + LongFunction supplyBinding) + { + super(config, context, supplyBinding, McpBeginExFW.KIND_RESOURCES_READ); + } + + @Override + protected void injectInitialBeginEx( + McpBeginExFW.Builder builder, + String sessionId, + String identifier) + { + builder.resourcesRead(r -> r.sessionId(sessionId).uri(identifier)); + } + + @Override + protected void injectReplyBeginEx( + McpBeginExFW.Builder builder, + String sessionId, + McpBeginExFW upstream) + { + builder.resourcesRead(r -> r.sessionId(sessionId).uri(upstream.resourcesRead().uri().asString())); + } + + @Override + protected String sessionId( + McpBeginExFW beginEx) + { + return beginEx.resourcesRead().sessionId().asString(); + } +} diff --git a/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/McpProxyToolsCallFactory.java b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/McpProxyToolsCallFactory.java new file mode 100644 index 0000000000..fa159d0331 --- /dev/null +++ b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/McpProxyToolsCallFactory.java @@ -0,0 +1,58 @@ +/* + * Copyright 2021-2024 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. + */ +package io.aklivity.zilla.runtime.binding.mcp.internal.stream; + +import java.util.function.LongFunction; + +import io.aklivity.zilla.runtime.binding.mcp.internal.McpConfiguration; +import io.aklivity.zilla.runtime.binding.mcp.internal.config.McpBindingConfig; +import io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.McpBeginExFW; +import io.aklivity.zilla.runtime.engine.EngineContext; + +final class McpProxyToolsCallFactory extends McpProxyItemFactory +{ + McpProxyToolsCallFactory( + McpConfiguration config, + EngineContext context, + LongFunction supplyBinding) + { + super(config, context, supplyBinding, McpBeginExFW.KIND_TOOLS_CALL); + } + + @Override + protected void injectInitialBeginEx( + McpBeginExFW.Builder builder, + String sessionId, + String identifier) + { + builder.toolsCall(t -> t.sessionId(sessionId).name(identifier)); + } + + @Override + protected void injectReplyBeginEx( + McpBeginExFW.Builder builder, + String sessionId, + McpBeginExFW upstream) + { + builder.toolsCall(t -> t.sessionId(sessionId).name(upstream.toolsCall().name().asString())); + } + + @Override + protected String sessionId( + McpBeginExFW beginEx) + { + return beginEx.toolsCall().sessionId().asString(); + } +} diff --git a/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/McpProxyToolsListFactory.java b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/McpProxyToolsListFactory.java new file mode 100644 index 0000000000..1e76dc888a --- /dev/null +++ b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/McpProxyToolsListFactory.java @@ -0,0 +1,94 @@ +/* + * Copyright 2021-2024 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. + */ +package io.aklivity.zilla.runtime.binding.mcp.internal.stream; + +import static io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.McpBeginExFW.KIND_TOOLS_LIST; + +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.function.LongFunction; + +import org.agrona.DirectBuffer; +import org.agrona.concurrent.UnsafeBuffer; + +import io.aklivity.zilla.runtime.binding.mcp.internal.McpConfiguration; +import io.aklivity.zilla.runtime.binding.mcp.internal.config.McpBindingConfig; +import io.aklivity.zilla.runtime.binding.mcp.internal.stream.cache.McpProxyCache; +import io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.McpBeginExFW; +import io.aklivity.zilla.runtime.engine.EngineContext; + +final class McpProxyToolsListFactory extends McpProxyListFactory +{ + private static final List TOOLS_LIST_ITEM_JSON_PATH_INCLUDES = List.of("/tools/-/name"); + + private final DirectBuffer prelude = + new UnsafeBuffer("{\"tools\":[".getBytes(StandardCharsets.UTF_8)); + + McpProxyToolsListFactory( + McpConfiguration config, + EngineContext context, + LongFunction supplyBinding) + { + super(config, context, supplyBinding, McpBeginExFW.KIND_TOOLS_LIST, TOOLS_LIST_ITEM_JSON_PATH_INCLUDES); + } + + @Override + protected McpProxyCache.McpListCache cacheOf( + McpBindingConfig binding) + { + return binding.cache != null ? binding.cache.cacheOf(KIND_TOOLS_LIST) : null; + } + + @Override + protected void injectInitialBeginEx( + McpBeginExFW.Builder builder, + String sessionId) + { + builder.toolsList(t -> t.sessionId(sessionId)); + } + + @Override + protected void injectReplyBeginEx( + McpBeginExFW.Builder builder, + String sessionId) + { + builder.toolsList(t -> t.sessionId(sessionId)); + } + + @Override + protected DirectBuffer listReplyOpenPrelude() + { + return prelude; + } + + @Override + protected String arrayKey() + { + return "tools"; + } + + @Override + protected String idKey() + { + return "name"; + } + + @Override + protected String sessionId( + McpBeginExFW beginEx) + { + return beginEx.toolsList().sessionId().asString(); + } +} 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 ec3fa223cc..dab2b72856 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 @@ -192,6 +192,7 @@ public final class McpServerFactory implements McpStreamFactory private final long altSvcMaxAgeSeconds; private final long inactivityTimeoutMillis; private final long sseKeepaliveIntervalMillis; + private final EngineContext context; private final Signaler signaler; private final MutableDirectBuffer writeBuffer; private final MutableDirectBuffer codecBuffer; @@ -236,6 +237,7 @@ public final class McpServerFactory implements McpStreamFactory private final McpServerDecoder decodeJsonRpcProgressToken = this::decodeJsonRpcProgressToken; private final McpServerDecoder decodeIgnore = this::decodeIgnore; + private final McpConfiguration config; private final Long2ObjectHashMap bindings; private final Map sessions; private final int localIndex; @@ -244,6 +246,7 @@ public McpServerFactory( McpConfiguration config, EngineContext context) { + this.config = config; this.supplySessionId = config.sessionIdSupplier(); this.supplyElicitationId = config.elicitationIdSupplier(); this.serverName = config.serverName(); @@ -252,6 +255,7 @@ public McpServerFactory( this.altSvcMaxAgeSeconds = config.altSvcMaxAge().toSeconds(); this.inactivityTimeoutMillis = config.inactivityTimeout().toMillis(); this.sseKeepaliveIntervalMillis = config.sseKeepaliveInterval().toMillis(); + this.context = context; this.signaler = context.signaler(); this.writeBuffer = context.writeBuffer(); this.codecBuffer = new UnsafeBuffer(new byte[context.writeBuffer().capacity()]); @@ -290,7 +294,7 @@ public int routedTypeId() public void attach( BindingConfig binding) { - McpBindingConfig newBinding = new McpBindingConfig(binding); + McpBindingConfig newBinding = new McpBindingConfig(binding, config, context); bindings.put(binding.id, newBinding); } diff --git a/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/McpState.java b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/McpState.java index 67d50188fe..2dd1cdad3c 100644 --- a/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/McpState.java +++ b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/McpState.java @@ -25,103 +25,103 @@ public final class McpState private static final int REPLY_CLOSING = 0x04; private static final int REPLY_CLOSED = 0x08; - static int openingInitial( + public static int openingInitial( int state) { return state | INITIAL_OPENING; } - static int openedInitial( + public static int openedInitial( int state) { return state | INITIAL_OPENED; } - static boolean initialOpening( + public static boolean initialOpening( int state) { return (state & INITIAL_OPENING) != 0; } - static boolean initialOpened( + public static boolean initialOpened( int state) { return (state & INITIAL_OPENED) != 0; } - static int closingInitial( + public static int closingInitial( int state) { return state | INITIAL_CLOSING; } - static int closedInitial( + public static int closedInitial( int state) { return state | INITIAL_CLOSED; } - static boolean initialClosing( + public static boolean initialClosing( int state) { return (state & INITIAL_CLOSING) != 0; } - static boolean initialClosed( + public static boolean initialClosed( int state) { return (state & INITIAL_CLOSED) != 0; } - static int openingReply( + public static int openingReply( int state) { return state | REPLY_OPENING; } - static int openedReply( + public static int openedReply( int state) { return state | REPLY_OPENED; } - static boolean replyOpening( + public static boolean replyOpening( int state) { return (state & REPLY_OPENING) != 0; } - static boolean replyOpened( + public static boolean replyOpened( int state) { return (state & REPLY_OPENED) != 0; } - static int closingReply( + public static int closingReply( int state) { return state | REPLY_CLOSING; } - static int closedReply( + public static int closedReply( int state) { return state | REPLY_CLOSED; } - static boolean replyClosing( + public static boolean replyClosing( int state) { return (state & REPLY_CLOSING) != 0; } - static boolean replyClosed( + public static boolean replyClosed( int state) { return (state & REPLY_CLOSED) != 0; } - static boolean closed( + public static boolean closed( int state) { return initialClosed(state) && replyClosed(state); diff --git a/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/cache/McpProxyCache.java b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/cache/McpProxyCache.java new file mode 100644 index 0000000000..dd47bebe1a --- /dev/null +++ b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/cache/McpProxyCache.java @@ -0,0 +1,236 @@ +/* + * Copyright 2021-2024 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. + */ +package io.aklivity.zilla.runtime.binding.mcp.internal.stream.cache; + +import static io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.McpBeginExFW.KIND_PROMPTS_LIST; +import static io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.McpBeginExFW.KIND_RESOURCES_LIST; +import static io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.McpBeginExFW.KIND_TOOLS_LIST; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.IntPredicate; + +import org.agrona.collections.Int2ObjectHashMap; + +import io.aklivity.zilla.runtime.binding.mcp.config.McpCacheConfig; +import io.aklivity.zilla.runtime.binding.mcp.internal.McpConfiguration; +import io.aklivity.zilla.runtime.engine.EngineContext; +import io.aklivity.zilla.runtime.engine.config.BindingConfig; +import io.aklivity.zilla.runtime.engine.guard.GuardHandler; +import io.aklivity.zilla.runtime.engine.store.StoreHandler; + +public final class McpProxyCache +{ + private static final String STORE_KEY_TOOLS = "tools"; + private static final String STORE_KEY_RESOURCES = "resources"; + private static final String STORE_KEY_PROMPTS = "prompts"; + private static final String STORE_LOCK_SUFFIX = ".lock"; + private static final String STORE_LOCK_VALUE = "1"; + private static final String STORE_LOCK_KEY_TOOLS = STORE_KEY_TOOLS + STORE_LOCK_SUFFIX; + private static final String STORE_LOCK_KEY_RESOURCES = STORE_KEY_RESOURCES + STORE_LOCK_SUFFIX; + private static final String STORE_LOCK_KEY_PROMPTS = STORE_KEY_PROMPTS + STORE_LOCK_SUFFIX; + private static final String STORE_LOCK_KEY_LIFECYCLE = "lifecycle.lock"; + private static final long STORE_TTL_FOREVER = Long.MAX_VALUE; + + public final long bindingId; + public final GuardHandler guard; + public final String credentials; + public final Duration leaseTtl; + public final Duration leaseRetry; + public final Duration cacheTtl; + + public String sessionId; + public long authorization; + + private final StoreHandler store; + private final Int2ObjectHashMap caches; + private final List awaiters; + + boolean populated; + + Runnable onReady; + + public McpProxyCache( + BindingConfig binding, + McpConfiguration config, + EngineContext context, + McpCacheConfig cache) + { + this.bindingId = binding.id; + this.store = context.supplyStore(binding.resolveId.applyAsLong(cache.store)); + this.guard = Optional.ofNullable(cache.authorization) + .map(a -> a.name) + .map(binding.resolveId::applyAsLong) + .map(context::supplyGuard) + .orElse(null); + this.credentials = Optional.ofNullable(cache.authorization) + .map(a -> a.credentials) + .orElse(null); + this.leaseTtl = config.leaseTtl(); + this.leaseRetry = config.leaseRetry(); + this.cacheTtl = cache.ttl; + this.awaiters = new ArrayList<>(); + this.caches = new Int2ObjectHashMap<>(); + + final IntPredicate filter = config.hydrateFilter(); + if (filter.test(KIND_TOOLS_LIST)) + { + caches.put(KIND_TOOLS_LIST, new McpListCache(KIND_TOOLS_LIST, STORE_KEY_TOOLS, STORE_LOCK_KEY_TOOLS)); + } + if (filter.test(KIND_RESOURCES_LIST)) + { + caches.put(KIND_RESOURCES_LIST, + new McpListCache(KIND_RESOURCES_LIST, STORE_KEY_RESOURCES, STORE_LOCK_KEY_RESOURCES)); + } + if (filter.test(KIND_PROMPTS_LIST)) + { + caches.put(KIND_PROMPTS_LIST, new McpListCache(KIND_PROMPTS_LIST, STORE_KEY_PROMPTS, STORE_LOCK_KEY_PROMPTS)); + } + } + + public McpListCache cacheOf( + int kind) + { + return caches.get(kind); + } + + public Int2ObjectHashMap caches() + { + return caches; + } + + public void register( + Runnable awaiter) + { + if (populated) + { + awaiter.run(); + } + else + { + awaiters.add(awaiter); + } + } + + void acquireLifecycle( + Consumer completion) + { + store.putIfAbsent(STORE_LOCK_KEY_LIFECYCLE, STORE_LOCK_VALUE, leaseTtl.toMillis(), + prior -> completion.accept(prior == null)); + } + + void releaseLifecycle( + Consumer completion) + { + store.delete(STORE_LOCK_KEY_LIFECYCLE, completion); + } + + void onPurged( + int kind) + { + final McpListCache cache = caches.get(kind); + if (cache != null) + { + cache.populated = false; + } + populated = false; + } + + private void checkReady() + { + for (McpListCache cache : caches.values()) + { + if (!cache.populated) + { + return; + } + } + populated = true; + if (onReady != null) + { + onReady.run(); + } + for (Runnable awaiter : awaiters) + { + awaiter.run(); + } + awaiters.clear(); + } + + public final class McpListCache + { + public final int kind; + + private final String storeKey; + private final String storeLockKey; + + boolean populated; + + private McpListCache( + int kind, + String storeKey, + String storeLockKey) + { + this.kind = kind; + this.storeKey = storeKey; + this.storeLockKey = storeLockKey; + } + + public void get( + BiConsumer completion) + { + store.get(storeKey, completion.andThen(this::checkGet)); + } + + public void put( + String value, + Consumer completion) + { + store.put(storeKey, value, STORE_TTL_FOREVER, completion.andThen(this::checkPut)); + } + + public void acquire( + Consumer completion) + { + store.putIfAbsent(storeLockKey, STORE_LOCK_VALUE, leaseTtl.toMillis(), + prior -> completion.accept(prior == null)); + } + + public void release( + Consumer completion) + { + store.delete(storeLockKey, completion); + } + + private void checkGet( + String key, + String value) + { + populated = value != null; + checkReady(); + } + + private void checkPut( + String key) + { + populated = true; + checkReady(); + } + } +} diff --git a/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/cache/McpProxyCacheHandler.java b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/cache/McpProxyCacheHandler.java new file mode 100644 index 0000000000..d3989d10e0 --- /dev/null +++ b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/cache/McpProxyCacheHandler.java @@ -0,0 +1,25 @@ +/* + * Copyright 2021-2024 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. + */ +package io.aklivity.zilla.runtime.binding.mcp.internal.stream.cache; + +interface McpProxyCacheHandler +{ + void start(); + + void stop(); + + void hydrate( + int kind); +} diff --git a/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/cache/McpProxyCacheHydrater.java b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/cache/McpProxyCacheHydrater.java new file mode 100644 index 0000000000..9289b381be --- /dev/null +++ b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/cache/McpProxyCacheHydrater.java @@ -0,0 +1,846 @@ +/* + * Copyright 2021-2024 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. + */ +package io.aklivity.zilla.runtime.binding.mcp.internal.stream.cache; + +import static io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.McpBeginExFW.KIND_PROMPTS_LIST; +import static io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.McpBeginExFW.KIND_RESOURCES_LIST; +import static io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.McpBeginExFW.KIND_TOOLS_LIST; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.LongSupplier; +import java.util.function.LongUnaryOperator; +import java.util.function.Supplier; + +import org.agrona.DirectBuffer; +import org.agrona.ExpandableArrayBuffer; +import org.agrona.MutableDirectBuffer; +import org.agrona.collections.Int2ObjectHashMap; +import org.agrona.concurrent.UnsafeBuffer; + +import io.aklivity.zilla.runtime.binding.mcp.internal.McpConfiguration; +import io.aklivity.zilla.runtime.binding.mcp.internal.stream.McpState; +import io.aklivity.zilla.runtime.binding.mcp.internal.types.Flyweight; +import io.aklivity.zilla.runtime.binding.mcp.internal.types.OctetsFW; +import io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.AbortFW; +import io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.BeginFW; +import io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.DataFW; +import io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.EndFW; +import io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.McpBeginExFW; +import io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.ResetFW; +import io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.WindowFW; +import io.aklivity.zilla.runtime.engine.EngineContext; +import io.aklivity.zilla.runtime.engine.binding.BindingHandler; +import io.aklivity.zilla.runtime.engine.binding.function.MessageConsumer; +import io.aklivity.zilla.runtime.engine.buffer.BufferPool; + +final class McpProxyCacheHydrater +{ + private final MutableDirectBuffer writeBuffer; + private final MutableDirectBuffer codecBuffer; + private final BindingHandler streamFactory; + private final BufferPool bufferPool; + private final LongUnaryOperator supplyInitialId; + private final LongUnaryOperator supplyReplyId; + private final LongSupplier supplyTraceId; + private final int mcpTypeId; + private final Supplier supplySessionId; + + private final BeginFW beginRO = new BeginFW(); + private final EndFW endRO = new EndFW(); + private final DataFW dataRO = new DataFW(); + private final AbortFW abortRO = new AbortFW(); + private final ResetFW resetRO = new ResetFW(); + private final WindowFW windowRO = new WindowFW(); + private final BeginFW.Builder beginRW = new BeginFW.Builder(); + private final EndFW.Builder endRW = new EndFW.Builder(); + private final AbortFW.Builder abortRW = new AbortFW.Builder(); + private final ResetFW.Builder resetRW = new ResetFW.Builder(); + private final WindowFW.Builder windowRW = new WindowFW.Builder(); + private final McpBeginExFW.Builder mcpBeginExRW = new McpBeginExFW.Builder(); + + private final Int2ObjectHashMap hydraters; + + McpProxyCacheHydrater( + McpConfiguration config, + EngineContext context) + { + this.writeBuffer = context.writeBuffer(); + this.codecBuffer = new UnsafeBuffer(new byte[context.writeBuffer().capacity()]); + this.streamFactory = context.streamFactory(); + this.bufferPool = context.bufferPool(); + this.supplyInitialId = context::supplyInitialId; + this.supplyReplyId = context::supplyReplyId; + this.supplyTraceId = context::supplyTraceId; + this.mcpTypeId = context.supplyTypeId("mcp"); + this.supplySessionId = config.sessionIdSupplier(); + + this.hydraters = new Int2ObjectHashMap<>(); + hydraters.put(KIND_TOOLS_LIST, new McpToolsListHydrater()); + hydraters.put(KIND_RESOURCES_LIST, new McpResourcesListHydrater()); + hydraters.put(KIND_PROMPTS_LIST, new McpPromptsListHydrater()); + } + + McpProxyCacheHandler attach( + McpProxyCache cache, + McpProxyCacheListener listener) + { + return new HandlerImpl(cache, listener); + } + + private final class HandlerImpl implements McpProxyCacheHandler + { + private final McpProxyCache cache; + private final McpProxyCacheListener listener; + + private McpHydrateLifecycleStream lifecycle; + private boolean stopped; + private boolean closedNotified; + + HandlerImpl( + McpProxyCache cache, + McpProxyCacheListener listener) + { + this.cache = cache; + this.listener = listener; + } + + @Override + public void start() + { + if (stopped) + { + return; + } + cache.acquireLifecycle(this::onAcquireLifecycleComplete); + } + + @Override + public void stop() + { + stopped = true; + if (lifecycle != null) + { + lifecycle.doLifecycleEnd(supplyTraceId.getAsLong()); + lifecycle = null; + } + cache.releaseLifecycle(k -> {}); + } + + @Override + public void hydrate( + int kind) + { + if (stopped || lifecycle == null) + { + return; + } + final McpListHydrater hydrater = hydraters.get(kind); + if (hydrater != null) + { + hydrater.hydrate(this); + } + } + + private void onAcquireLifecycleComplete( + boolean acquired) + { + if (stopped) + { + return; + } + if (acquired) + { + final long traceId = supplyTraceId.getAsLong(); + cache.sessionId = supplySessionId.get(); + cache.authorization = cache.guard != null + ? cache.guard.reauthorize(traceId, cache.bindingId, 0L, cache.credentials) + : 0L; + lifecycle = new McpHydrateLifecycleStream(this); + lifecycle.doLifecycleBegin(traceId); + } + else + { + notifyClosed(); + } + } + + private void onLifecycleOpened( + long traceId) + { + if (!stopped) + { + listener.onOpened(); + } + } + + private void onLifecycleClosed() + { + lifecycle = null; + cache.releaseLifecycle(k -> {}); + notifyClosed(); + } + + private void notifyClosed() + { + if (!stopped && !closedNotified) + { + closedNotified = true; + listener.onClosed(); + } + } + } + + final class McpHydrateLifecycleStream + { + private final HandlerImpl handler; + private final long originId; + private final long routedId; + private final long initialId; + private final long replyId; + private final List streams; + + private int state; + private long initialSeq; + private long initialAck; + private int initialMax; + private long replySeq; + private long replyAck; + private int replyMax; + private MessageConsumer receiver; + + McpHydrateLifecycleStream( + HandlerImpl handler) + { + this.handler = handler; + this.originId = handler.cache.bindingId; + this.routedId = handler.cache.bindingId; + this.initialId = supplyInitialId.applyAsLong(routedId); + this.replyId = supplyReplyId.applyAsLong(initialId); + this.replyMax = bufferPool.slotCapacity(); + this.streams = new ArrayList<>(); + } + + void register( + McpListHydrater.McpListHydrateStream stream) + { + streams.add(stream); + } + + void unregister( + McpListHydrater.McpListHydrateStream stream) + { + streams.remove(stream); + } + + private void cleanupStreams( + long traceId) + { + if (streams.isEmpty()) + { + return; + } + final List copy = new ArrayList<>(streams); + streams.clear(); + for (McpListHydrater.McpListHydrateStream stream : copy) + { + stream.doListHydrateEnd(traceId); + } + } + + private void onLifecycleMessage( + int msgTypeId, + DirectBuffer buffer, + int index, + int length) + { + switch (msgTypeId) + { + case BeginFW.TYPE_ID: + final BeginFW begin = beginRO.wrap(buffer, index, index + length); + onLifecycleBegin(begin); + break; + case EndFW.TYPE_ID: + final EndFW end = endRO.wrap(buffer, index, index + length); + onLifecycleEnd(end); + break; + case AbortFW.TYPE_ID: + final AbortFW abort = abortRO.wrap(buffer, index, index + length); + onLifecycleAbort(abort); + break; + case ResetFW.TYPE_ID: + final ResetFW reset = resetRO.wrap(buffer, index, index + length); + onLifecycleReset(reset); + break; + default: + break; + } + } + + private void onLifecycleBegin( + BeginFW begin) + { + final long traceId = begin.traceId(); + state = McpState.openingReply(state); + doLifecycleWindow(traceId); + handler.onLifecycleOpened(traceId); + } + + private void onLifecycleEnd( + EndFW end) + { + final long traceId = end.traceId(); + state = McpState.closedReply(state); + cleanupStreams(traceId); + doLifecycleEnd(traceId); + handler.onLifecycleClosed(); + } + + private void onLifecycleAbort( + AbortFW abort) + { + final long traceId = abort.traceId(); + state = McpState.closedReply(state); + cleanupStreams(traceId); + doLifecycleAbort(traceId); + handler.onLifecycleClosed(); + } + + private void onLifecycleReset( + ResetFW reset) + { + final long traceId = reset.traceId(); + state = McpState.closedInitial(state); + cleanupStreams(traceId); + handler.onLifecycleClosed(); + } + + void doLifecycleBegin( + long traceId) + { + final McpBeginExFW beginEx = mcpBeginExRW + .wrap(codecBuffer, 0, codecBuffer.capacity()) + .typeId(mcpTypeId) + .lifecycle(l -> l.sessionId(handler.cache.sessionId)) + .build(); + + receiver = newStream(this::onLifecycleMessage, originId, routedId, initialId, + initialSeq, initialAck, initialMax, traceId, handler.cache.authorization, 0L, beginEx); + state = McpState.openingInitial(state); + } + + private void doLifecycleWindow( + long traceId) + { + doWindow(receiver, originId, routedId, replyId, replySeq, replyAck, replyMax, + traceId, handler.cache.authorization, 0L, 0); + } + + void doLifecycleEnd( + long traceId) + { + if (!McpState.initialClosed(state)) + { + cleanupStreams(traceId); + doEnd(receiver, originId, routedId, initialId, initialSeq, initialAck, initialMax, + traceId, handler.cache.authorization); + state = McpState.closedInitial(state); + } + } + + private void doLifecycleAbort( + long traceId) + { + if (!McpState.initialClosed(state)) + { + doAbort(receiver, originId, routedId, initialId, initialSeq, initialAck, initialMax, + traceId, handler.cache.authorization); + state = McpState.closedInitial(state); + } + } + } + + abstract class McpListHydrater + { + protected abstract int kind(); + + protected abstract void injectInitialBeginEx( + McpBeginExFW.Builder builder, + String sessionId); + + final void hydrate( + HandlerImpl handler) + { + final McpProxyCache.McpListCache listCache = handler.cache.cacheOf(kind()); + if (handler.cache.populated) + { + listCache.acquire(acquired -> onAcquireComplete(handler, acquired)); + } + else + { + listCache.get((k, v) -> onGetComplete(handler, v)); + } + } + + private void onGetComplete( + HandlerImpl handler, + String value) + { + if (handler.stopped || handler.lifecycle == null) + { + return; + } + + if (value == null) + { + handler.cache.cacheOf(kind()).acquire(acquired -> onAcquireComplete(handler, acquired)); + } + } + + private void onAcquireComplete( + HandlerImpl handler, + boolean acquired) + { + if (handler.stopped || handler.lifecycle == null) + { + return; + } + + if (acquired) + { + startListStream(handler); + } + else + { + handler.listener.onError(kind()); + } + } + + private void startListStream( + HandlerImpl handler) + { + final long traceId = supplyTraceId.getAsLong(); + final McpListHydrateStream stream = new McpListHydrateStream(handler); + handler.lifecycle.register(stream); + stream.doListHydrateBegin(traceId); + } + + final class McpListHydrateStream + { + private final HandlerImpl handler; + private final long originId; + private final long routedId; + private final long initialId; + private final long replyId; + private final ExpandableArrayBuffer bodyBuffer; + + private int state; + private long initialSeq; + private long initialAck; + private int initialMax; + private long replySeq; + private long replyAck; + private int replyMax; + private MessageConsumer receiver; + private int bodyLen; + private boolean settled; + private boolean failed; + + McpListHydrateStream( + HandlerImpl handler) + { + this.handler = handler; + this.originId = handler.cache.bindingId; + this.routedId = handler.cache.bindingId; + this.bodyBuffer = new ExpandableArrayBuffer(); + this.initialId = supplyInitialId.applyAsLong(routedId); + this.replyId = supplyReplyId.applyAsLong(initialId); + this.replyMax = bufferPool.slotCapacity(); + } + + private void onListHydrateMessage( + int msgTypeId, + DirectBuffer buffer, + int index, + int length) + { + switch (msgTypeId) + { + case BeginFW.TYPE_ID: + final BeginFW begin = beginRO.wrap(buffer, index, index + length); + onListHydrateBegin(begin); + break; + case DataFW.TYPE_ID: + final DataFW data = dataRO.wrap(buffer, index, index + length); + onListHydrateData(data); + break; + case EndFW.TYPE_ID: + final EndFW end = endRO.wrap(buffer, index, index + length); + onListHydrateEnd(end); + break; + case AbortFW.TYPE_ID: + final AbortFW abort = abortRO.wrap(buffer, index, index + length); + onListHydrateAbort(abort); + break; + case ResetFW.TYPE_ID: + final ResetFW reset = resetRO.wrap(buffer, index, index + length); + onListHydrateReset(reset); + break; + case WindowFW.TYPE_ID: + final WindowFW window = windowRO.wrap(buffer, index, index + length); + onListHydrateWindow(window); + break; + default: + break; + } + } + + private void onListHydrateBegin( + BeginFW begin) + { + replySeq = begin.sequence(); + replyAck = begin.acknowledge(); + state = McpState.openingReply(state); + doListHydrateWindow(begin.traceId()); + } + + private void onListHydrateData( + DataFW data) + { + replySeq = data.sequence() + data.reserved(); + + final OctetsFW payload = data.payload(); + if (payload != null) + { + final int payloadLen = payload.sizeof(); + bodyBuffer.putBytes(bodyLen, payload.buffer(), payload.offset(), payloadLen); + bodyLen += payloadLen; + } + + replyAck = replySeq; + doListHydrateWindow(data.traceId()); + } + + private void onListHydrateEnd( + EndFW end) + { + final long traceId = end.traceId(); + state = McpState.closedReply(state); + if (bodyLen > 0) + { + final String value = bodyBuffer.getStringWithoutLengthUtf8(0, bodyLen); + handler.cache.cacheOf(kind()).put(value, k -> terminal(traceId)); + } + else + { + failed = true; + terminal(traceId); + } + } + + private void onListHydrateAbort( + AbortFW abort) + { + final long traceId = abort.traceId(); + state = McpState.closedReply(state); + failed = true; + doListHydrateAbort(traceId); + terminal(traceId); + } + + private void onListHydrateReset( + ResetFW reset) + { + final long traceId = reset.traceId(); + state = McpState.closedInitial(state); + failed = true; + doListHydrateReset(traceId); + terminal(traceId); + } + + private void onListHydrateWindow( + WindowFW window) + { + if (McpState.initialClosing(state) && !McpState.initialClosed(state)) + { + doListHydrateEnd(window.traceId()); + } + } + + void doListHydrateBegin( + long traceId) + { + final McpBeginExFW beginEx = mcpBeginExRW + .wrap(codecBuffer, 0, codecBuffer.capacity()) + .typeId(mcpTypeId) + .inject(builder -> injectInitialBeginEx(builder, handler.cache.sessionId)) + .build(); + + receiver = newStream(this::onListHydrateMessage, originId, routedId, initialId, + initialSeq, initialAck, initialMax, traceId, handler.cache.authorization, 0L, beginEx); + state = McpState.openingInitial(state); + state = McpState.closingInitial(state); + } + + void doListHydrateEnd( + long traceId) + { + if (!McpState.initialClosed(state)) + { + doEnd(receiver, originId, routedId, initialId, + initialSeq, initialAck, initialMax, traceId, handler.cache.authorization); + state = McpState.closedInitial(state); + } + } + + private void doListHydrateAbort( + long traceId) + { + if (!McpState.initialClosed(state)) + { + doAbort(receiver, originId, routedId, initialId, + initialSeq, initialAck, initialMax, traceId, handler.cache.authorization); + state = McpState.closedInitial(state); + } + } + + private void doListHydrateReset( + long traceId) + { + if (!McpState.replyClosed(state)) + { + doReset(receiver, originId, routedId, replyId, + replySeq, replyAck, replyMax, traceId, handler.cache.authorization); + state = McpState.closedReply(state); + } + } + + private void doListHydrateWindow( + long traceId) + { + doWindow(receiver, originId, routedId, replyId, replySeq, replyAck, replyMax, + traceId, handler.cache.authorization, 0L, 0); + } + + private void terminal( + long traceId) + { + if (!settled) + { + settled = true; + if (handler.lifecycle != null) + { + handler.lifecycle.unregister(this); + } + handler.cache.cacheOf(kind()).release(k -> {}); + if (failed && !handler.stopped) + { + handler.listener.onError(kind()); + } + } + } + } + } + + private final class McpToolsListHydrater extends McpListHydrater + { + @Override + protected int kind() + { + return KIND_TOOLS_LIST; + } + + @Override + protected void injectInitialBeginEx( + McpBeginExFW.Builder builder, + String sessionId) + { + builder.toolsList(t -> t.sessionId(sessionId)); + } + } + + private final class McpResourcesListHydrater extends McpListHydrater + { + @Override + protected int kind() + { + return KIND_RESOURCES_LIST; + } + + @Override + protected void injectInitialBeginEx( + McpBeginExFW.Builder builder, + String sessionId) + { + builder.resourcesList(r -> r.sessionId(sessionId)); + } + } + + private final class McpPromptsListHydrater extends McpListHydrater + { + @Override + protected int kind() + { + return KIND_PROMPTS_LIST; + } + + @Override + protected void injectInitialBeginEx( + McpBeginExFW.Builder builder, + String sessionId) + { + builder.promptsList(p -> p.sessionId(sessionId)); + } + } + + private MessageConsumer newStream( + MessageConsumer sender, + long originId, + long routedId, + long streamId, + long sequence, + long acknowledge, + int maximum, + long traceId, + long authorization, + long affinity, + Flyweight extension) + { + final BeginFW begin = beginRW.wrap(writeBuffer, 0, writeBuffer.capacity()) + .originId(originId) + .routedId(routedId) + .streamId(streamId) + .sequence(sequence) + .acknowledge(acknowledge) + .maximum(maximum) + .traceId(traceId) + .authorization(authorization) + .affinity(affinity) + .extension(extension.buffer(), extension.offset(), extension.sizeof()) + .build(); + + final MessageConsumer receiver = + streamFactory.newStream(begin.typeId(), begin.buffer(), begin.offset(), begin.sizeof(), sender); + assert receiver != null; + + receiver.accept(begin.typeId(), begin.buffer(), begin.offset(), begin.sizeof()); + + return receiver; + } + + private void doEnd( + MessageConsumer receiver, + long originId, + long routedId, + long streamId, + long sequence, + long acknowledge, + int maximum, + long traceId, + long authorization) + { + final EndFW end = endRW.wrap(writeBuffer, 0, writeBuffer.capacity()) + .originId(originId) + .routedId(routedId) + .streamId(streamId) + .sequence(sequence) + .acknowledge(acknowledge) + .maximum(maximum) + .traceId(traceId) + .authorization(authorization) + .build(); + + receiver.accept(end.typeId(), end.buffer(), end.offset(), end.sizeof()); + } + + private void doAbort( + MessageConsumer receiver, + long originId, + long routedId, + long streamId, + long sequence, + long acknowledge, + int maximum, + long traceId, + long authorization) + { + final AbortFW abort = abortRW.wrap(writeBuffer, 0, writeBuffer.capacity()) + .originId(originId) + .routedId(routedId) + .streamId(streamId) + .sequence(sequence) + .acknowledge(acknowledge) + .maximum(maximum) + .traceId(traceId) + .authorization(authorization) + .build(); + + receiver.accept(abort.typeId(), abort.buffer(), abort.offset(), abort.sizeof()); + } + + private void doReset( + MessageConsumer receiver, + long originId, + long routedId, + long streamId, + long sequence, + long acknowledge, + int maximum, + long traceId, + long authorization) + { + final ResetFW reset = resetRW.wrap(writeBuffer, 0, writeBuffer.capacity()) + .originId(originId) + .routedId(routedId) + .streamId(streamId) + .sequence(sequence) + .acknowledge(acknowledge) + .maximum(maximum) + .traceId(traceId) + .authorization(authorization) + .build(); + + receiver.accept(reset.typeId(), reset.buffer(), reset.offset(), reset.sizeof()); + } + + private void doWindow( + MessageConsumer receiver, + long originId, + long routedId, + long streamId, + long sequence, + long acknowledge, + int maximum, + long traceId, + long authorization, + long budgetId, + int padding) + { + final WindowFW window = windowRW.wrap(writeBuffer, 0, writeBuffer.capacity()) + .originId(originId) + .routedId(routedId) + .streamId(streamId) + .sequence(sequence) + .acknowledge(acknowledge) + .maximum(maximum) + .traceId(traceId) + .authorization(authorization) + .budgetId(budgetId) + .padding(padding) + .build(); + + receiver.accept(window.typeId(), window.buffer(), window.offset(), window.sizeof()); + } +} diff --git a/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/cache/McpProxyCacheListener.java b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/cache/McpProxyCacheListener.java new file mode 100644 index 0000000000..7ec42d6bf7 --- /dev/null +++ b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/cache/McpProxyCacheListener.java @@ -0,0 +1,25 @@ +/* + * Copyright 2021-2024 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. + */ +package io.aklivity.zilla.runtime.binding.mcp.internal.stream.cache; + +interface McpProxyCacheListener +{ + void onOpened(); + + void onError( + int kind); + + void onClosed(); +} diff --git a/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/cache/McpProxyCacheManager.java b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/cache/McpProxyCacheManager.java new file mode 100644 index 0000000000..cc6f49e89d --- /dev/null +++ b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/cache/McpProxyCacheManager.java @@ -0,0 +1,249 @@ +/* + * Copyright 2021-2024 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. + */ +package io.aklivity.zilla.runtime.binding.mcp.internal.stream.cache; + +import static io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.McpBeginExFW.KIND_RESOURCES_LIST; +import static io.aklivity.zilla.runtime.engine.concurrent.Signaler.NO_CANCEL_ID; + +import java.time.Instant; +import java.util.Arrays; + +import io.aklivity.zilla.runtime.binding.mcp.internal.McpConfiguration; +import io.aklivity.zilla.runtime.engine.EngineContext; +import io.aklivity.zilla.runtime.engine.concurrent.Signaler; + +public final class McpProxyCacheManager implements McpProxyCacheListener +{ + private static final int KIND_SLOTS = KIND_RESOURCES_LIST + 1; + + private final McpProxyCacheHydrater hydrater; + private final McpProxyCache cache; + private final Signaler signaler; + private final long[] hydrateBackoffMs; + private final long[] hydrateRetryIds; + + private McpProxyCacheHandler handler; + private long refreshCancelId; + private long reconnectCancelId; + private long sessionBackoffMs; + private boolean stopped; + + McpProxyCacheManager( + McpProxyCacheHydrater hydrater, + McpProxyCache cache, + Signaler signaler) + { + this.hydrater = hydrater; + this.cache = cache; + this.signaler = signaler; + this.hydrateBackoffMs = new long[KIND_SLOTS]; + this.hydrateRetryIds = new long[KIND_SLOTS]; + Arrays.fill(this.hydrateRetryIds, NO_CANCEL_ID); + this.refreshCancelId = NO_CANCEL_ID; + this.reconnectCancelId = NO_CANCEL_ID; + } + + public void start() + { + cache.onReady = this::onCacheReady; + handler = hydrater.attach(cache, this); + handler.start(); + } + + public void stop() + { + stopped = true; + cancelRefresh(); + cancelReconnect(); + for (int kind : cache.caches().keySet()) + { + cancelHydrateRetry(kind); + } + if (handler != null) + { + handler.stop(); + handler = null; + } + cache.onReady = null; + } + + @Override + public void onOpened() + { + if (stopped || handler == null) + { + return; + } + Arrays.fill(hydrateBackoffMs, 0L); + sessionBackoffMs = 0L; + for (int kind : cache.caches().keySet()) + { + handler.hydrate(kind); + } + } + + @Override + public void onError( + int kind) + { + if (!stopped) + { + scheduleHydrateRetry(kind); + } + } + + @Override + public void onClosed() + { + if (stopped) + { + return; + } + cancelRefresh(); + for (int kind : cache.caches().keySet()) + { + cancelHydrateRetry(kind); + } + handler = null; + scheduleReconnect(); + } + + private void onCacheReady() + { + if (stopped) + { + return; + } + cache.releaseLifecycle(k -> {}); + scheduleRefresh(); + } + + private void scheduleRefresh() + { + if (cache.cacheTtl == null) + { + return; + } + cancelRefresh(); + refreshCancelId = signaler.signalAt( + Instant.now().plus(cache.cacheTtl), 0, this::onRefreshed); + } + + private void onRefreshed( + int signalId) + { + refreshCancelId = NO_CANCEL_ID; + if (stopped || handler == null) + { + return; + } + for (int kind : cache.caches().keySet()) + { + handler.hydrate(kind); + } + } + + private void scheduleHydrateRetry( + int kind) + { + cancelHydrateRetry(kind); + long delay = hydrateBackoffMs[kind]; + delay = delay == 0L ? cache.leaseRetry.toMillis() : Math.min(delay * 2L, cache.leaseTtl.toMillis()); + hydrateBackoffMs[kind] = delay; + hydrateRetryIds[kind] = signaler.signalAt( + Instant.now().plusMillis(delay), kind, this::onHydrateRetry); + } + + private void onHydrateRetry( + int kind) + { + hydrateRetryIds[kind] = NO_CANCEL_ID; + if (stopped || handler == null) + { + return; + } + handler.hydrate(kind); + } + + private void scheduleReconnect() + { + cancelReconnect(); + long delay = sessionBackoffMs; + delay = delay == 0L ? cache.leaseRetry.toMillis() : Math.min(delay * 2L, cache.leaseTtl.toMillis()); + sessionBackoffMs = delay; + reconnectCancelId = signaler.signalAt( + Instant.now().plusMillis(delay), 0, this::onReconnected); + } + + private void onReconnected( + int signalId) + { + reconnectCancelId = NO_CANCEL_ID; + if (stopped) + { + return; + } + handler = hydrater.attach(cache, this); + handler.start(); + } + + private void cancelRefresh() + { + if (refreshCancelId != NO_CANCEL_ID) + { + signaler.cancel(refreshCancelId); + refreshCancelId = NO_CANCEL_ID; + } + } + + private void cancelReconnect() + { + if (reconnectCancelId != NO_CANCEL_ID) + { + signaler.cancel(reconnectCancelId); + reconnectCancelId = NO_CANCEL_ID; + } + } + + private void cancelHydrateRetry( + int kind) + { + if (hydrateRetryIds[kind] != NO_CANCEL_ID) + { + signaler.cancel(hydrateRetryIds[kind]); + hydrateRetryIds[kind] = NO_CANCEL_ID; + } + } + + public static final class Factory + { + private final McpProxyCacheHydrater hydrater; + private final Signaler signaler; + + public Factory( + McpConfiguration config, + EngineContext context) + { + this.hydrater = new McpProxyCacheHydrater(config, context); + this.signaler = context.signaler(); + } + + public McpProxyCacheManager create( + McpProxyCache cache) + { + return new McpProxyCacheManager(hydrater, cache, signaler); + } + } +} 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 6c6bfbd561..4fa01e143d 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 @@ -19,8 +19,11 @@ import static io.aklivity.zilla.runtime.binding.mcp.internal.McpConfiguration.MCP_CLIENT_NAME; import static io.aklivity.zilla.runtime.binding.mcp.internal.McpConfiguration.MCP_CLIENT_VERSION; import static io.aklivity.zilla.runtime.binding.mcp.internal.McpConfiguration.MCP_ELICITATION_ID; +import static io.aklivity.zilla.runtime.binding.mcp.internal.McpConfiguration.MCP_HYDRATE_FILTER; import static io.aklivity.zilla.runtime.binding.mcp.internal.McpConfiguration.MCP_INACTIVITY_TIMEOUT; import static io.aklivity.zilla.runtime.binding.mcp.internal.McpConfiguration.MCP_KEEPALIVE_TOLERANCE; +import static io.aklivity.zilla.runtime.binding.mcp.internal.McpConfiguration.MCP_LEASE_RETRY; +import static io.aklivity.zilla.runtime.binding.mcp.internal.McpConfiguration.MCP_LEASE_TTL; import static io.aklivity.zilla.runtime.binding.mcp.internal.McpConfiguration.MCP_SERVER_NAME; import static io.aklivity.zilla.runtime.binding.mcp.internal.McpConfiguration.MCP_SERVER_VERSION; import static io.aklivity.zilla.runtime.binding.mcp.internal.McpConfiguration.MCP_SESSION_ID; @@ -48,6 +51,9 @@ public class McpConfigurationTest public static final String MCP_SSE_KEEPALIVE_INTERVAL_NAME = "zilla.binding.mcp.sse.keepalive.interval"; public static final String MCP_ALT_SVC_ENABLED_NAME = "zilla.binding.mcp.alt.svc.enabled"; public static final String MCP_ALT_SVC_MAX_AGE_NAME = "zilla.binding.mcp.alt.svc.max.age"; + public static final String MCP_HYDRATE_FILTER_NAME = "zilla.binding.mcp.hydrate.filter"; + public static final String MCP_LEASE_TTL_NAME = "zilla.binding.mcp.lease.ttl"; + public static final String MCP_LEASE_RETRY_NAME = "zilla.binding.mcp.lease.retry"; @Test public void shouldVerifyConstants() throws Exception @@ -66,5 +72,8 @@ public void shouldVerifyConstants() throws Exception assertEquals(MCP_SSE_KEEPALIVE_INTERVAL.name(), MCP_SSE_KEEPALIVE_INTERVAL_NAME); assertEquals(MCP_ALT_SVC_ENABLED.name(), MCP_ALT_SVC_ENABLED_NAME); assertEquals(MCP_ALT_SVC_MAX_AGE.name(), MCP_ALT_SVC_MAX_AGE_NAME); + assertEquals(MCP_HYDRATE_FILTER.name(), MCP_HYDRATE_FILTER_NAME); + assertEquals(MCP_LEASE_TTL.name(), MCP_LEASE_TTL_NAME); + assertEquals(MCP_LEASE_RETRY.name(), MCP_LEASE_RETRY_NAME); } } diff --git a/runtime/binding-mcp/src/test/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/McpProxyCacheIT.java b/runtime/binding-mcp/src/test/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/McpProxyCacheIT.java new file mode 100644 index 0000000000..399ecbce40 --- /dev/null +++ b/runtime/binding-mcp/src/test/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/McpProxyCacheIT.java @@ -0,0 +1,284 @@ +/* + * Copyright 2021-2024 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. + */ +package io.aklivity.zilla.runtime.binding.mcp.internal.stream; + +import static io.aklivity.zilla.runtime.binding.mcp.internal.McpConfigurationTest.MCP_HYDRATE_FILTER_NAME; +import static io.aklivity.zilla.runtime.binding.mcp.internal.McpConfigurationTest.MCP_SESSION_ID_NAME; +import static io.aklivity.zilla.runtime.engine.test.EngineRule.ENGINE_BUFFER_SLOT_CAPACITY_NAME; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.junit.rules.RuleChain.outerRule; + +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.DisableOnDebug; +import org.junit.rules.TestRule; +import org.junit.rules.Timeout; + +import io.aklivity.k3po.runtime.junit.annotation.ScriptProperty; +import io.aklivity.k3po.runtime.junit.annotation.Specification; +import io.aklivity.k3po.runtime.junit.rules.K3poRule; +import io.aklivity.zilla.runtime.engine.test.EngineRule; +import io.aklivity.zilla.runtime.engine.test.annotation.Configuration; +import io.aklivity.zilla.runtime.engine.test.annotation.Configure; + +public class McpProxyCacheIT +{ + private static final String ENGINE_WORKERS_NAME = "zilla.engine.workers"; + + private final K3poRule k3po = new K3poRule() + .addScriptRoot("app", "io/aklivity/zilla/specs/binding/mcp/streams/application"); + + private final TestRule timeout = new DisableOnDebug(new Timeout(10, SECONDS)); + + private final EngineRule engine = new EngineRule() + .directory("target/zilla-itests") + .countersBufferCapacity(8192) + .configurationRoot("io/aklivity/zilla/specs/binding/mcp/config") + .external("app1") + .external("app2") + .configure(MCP_SESSION_ID_NAME, "%s::sessionId".formatted(McpProxyCacheIT.class.getName())) + .clean(); + + @Rule + public final TestRule chain = outerRule(engine).around(k3po).around(timeout); + + @Test + @Configuration("proxy.cache.yaml") + @Specification({ + "${app}/cache.hydrate/server" }) + @ScriptProperty("serverAddress \"zilla://streams/app1\"") + public void shouldHydrate() throws Exception + { + k3po.finish(); + } + + @Test + @Configuration("proxy.cache.yaml") + @Specification({ + "${app}/cache.hydrate.error/server" }) + @ScriptProperty("serverAddress \"zilla://streams/app1\"") + public void shouldHydrateError() throws Exception + { + k3po.finish(); + } + + @Test + @Configuration("proxy.cache.credentials.yaml") + @Specification({ + "${app}/cache.hydrate.credentials/server" }) + @ScriptProperty("serverAddress \"zilla://streams/app1\"") + public void shouldHydrateWithCredentials() throws Exception + { + k3po.finish(); + } + + @Test + @Configuration("proxy.cache.toolkit.yaml") + @Specification({ + "${app}/cache.hydrate.toolkit/server" }) + public void shouldHydrateToolkit() throws Exception + { + k3po.finish(); + } + + @Test + @Configuration("proxy.cache.seeded.yaml") + @Specification({ + "${app}/cache.serve.initialize/client" }) + public void shouldServeInitialize() throws Exception + { + k3po.finish(); + } + + @Test + @Configuration("proxy.cache.yaml") + @Specification({ + "${app}/cache.hydrate.lifecycle.reconnect/server" }) + @ScriptProperty("serverAddress \"zilla://streams/app1\"") + public void shouldReconnectAfterLifecycleAbort() throws Exception + { + k3po.finish(); + } + + @Test + @Configuration("proxy.cache.seeded.yaml") + @Specification({ + "${app}/cache.serve.tools.list/client" }) + @Configure(name = MCP_HYDRATE_FILTER_NAME, value = "tools") + public void shouldServeToolsList() throws Exception + { + k3po.finish(); + } + + @Test + @Configuration("proxy.cache.refresh.yaml") + @Specification({ + "${app}/cache.refresh.tools/server" }) + @ScriptProperty("serverAddress \"zilla://streams/app1\"") + @Configure(name = MCP_HYDRATE_FILTER_NAME, value = "tools") + public void shouldRefreshTools() throws Exception + { + k3po.finish(); + } + + @Test + @Configuration("proxy.cache.refresh.yaml") + @Specification({ + "${app}/cache.refresh.tools.error/server" }) + @ScriptProperty("serverAddress \"zilla://streams/app1\"") + @Configure(name = MCP_HYDRATE_FILTER_NAME, value = "tools") + public void shouldRefreshToolsError() throws Exception + { + k3po.finish(); + } + + @Test + @Configuration("proxy.cache.refresh.yaml") + @Specification({ + "${app}/cache.refresh.tools.error.retry/server" }) + @ScriptProperty("serverAddress \"zilla://streams/app1\"") + @Configure(name = MCP_HYDRATE_FILTER_NAME, value = "tools") + public void shouldRetryAfterToolsRefreshError() throws Exception + { + k3po.finish(); + } + + @Test + @Configuration("proxy.cache.yaml") + @Specification({ + "${app}/cache.serve.tools.list.during.hydrate/server", + "${app}/cache.serve.tools.list.during.hydrate/client" }) + @ScriptProperty("serverAddress \"zilla://streams/app1\"") + @Configure(name = MCP_HYDRATE_FILTER_NAME, value = "tools") + public void shouldServeToolsListDuringHydrate() throws Exception + { + k3po.finish(); + } + + @Test + @Configuration("proxy.cache.refresh.yaml") + @Specification({ + "${app}/cache.refresh.tools.contended/server" }) + @ScriptProperty("serverAddress \"zilla://streams/app1\"") + @Configure(name = ENGINE_WORKERS_NAME, value = "2") + @Configure(name = MCP_HYDRATE_FILTER_NAME, value = "tools") + @Configure(name = MCP_SESSION_ID_NAME, + value = "io.aklivity.zilla.runtime.binding.mcp.internal.stream.McpProxyCacheIT::contendedSessionId") + public void shouldRefreshToolsContended() throws Exception + { + k3po.finish(); + } + + @Test + @Configuration("proxy.cache.seeded.yaml") + @Specification({ + "${app}/cache.serve.resources.list/client" }) + @Configure(name = MCP_HYDRATE_FILTER_NAME, value = "resources") + public void shouldServeResourcesList() throws Exception + { + k3po.finish(); + } + + @Test + @Configuration("proxy.cache.refresh.yaml") + @Specification({ + "${app}/cache.refresh.resources/server" }) + @ScriptProperty("serverAddress \"zilla://streams/app1\"") + @Configure(name = MCP_HYDRATE_FILTER_NAME, value = "resources") + public void shouldRefreshResources() throws Exception + { + k3po.finish(); + } + + @Test + @Configuration("proxy.cache.seeded.yaml") + @Specification({ + "${app}/cache.serve.prompts.list/client" }) + @Configure(name = MCP_HYDRATE_FILTER_NAME, value = "prompts") + public void shouldServePromptsList() throws Exception + { + k3po.finish(); + } + + @Test + @Configuration("proxy.cache.refresh.yaml") + @Specification({ + "${app}/cache.refresh.prompts/server" }) + @ScriptProperty("serverAddress \"zilla://streams/app1\"") + @Configure(name = MCP_HYDRATE_FILTER_NAME, value = "prompts") + public void shouldRefreshPrompts() throws Exception + { + k3po.finish(); + } + + @Test + @Configuration("proxy.cache.yaml") + @Specification({ + "${app}/cache.hydrate.10k/server" }) + @ScriptProperty("serverAddress \"zilla://streams/app1\"") + @Configure(name = ENGINE_BUFFER_SLOT_CAPACITY_NAME, value = "8192") + public void shouldHydrate10k() throws Exception + { + k3po.finish(); + } + + @Test + @Configuration("proxy.cache.yaml") + @Specification({ + "${app}/cache.hydrate.100k/server" }) + @ScriptProperty("serverAddress \"zilla://streams/app1\"") + @Configure(name = ENGINE_BUFFER_SLOT_CAPACITY_NAME, value = "8192") + public void shouldHydrate100k() throws Exception + { + k3po.finish(); + } + + @Test + @Configuration("proxy.cache.seeded.tools.10k.yaml") + @Specification({ + "${app}/cache.serve.tools.list.10k/client" }) + @Configure(name = MCP_HYDRATE_FILTER_NAME, value = "tools") + @Configure(name = ENGINE_BUFFER_SLOT_CAPACITY_NAME, value = "8192") + public void shouldServeToolsList10k() throws Exception + { + k3po.finish(); + } + + @Test + @Configuration("proxy.cache.seeded.tools.100k.yaml") + @Specification({ + "${app}/cache.serve.tools.list.100k/client" }) + @Configure(name = MCP_HYDRATE_FILTER_NAME, value = "tools") + @Configure(name = ENGINE_BUFFER_SLOT_CAPACITY_NAME, value = "8192") + public void shouldServeToolsList100k() throws Exception + { + k3po.finish(); + } + + public static String sessionId() + { + return "hydrate-1"; + } + + private static final String[] CONTENDED_SESSION_IDS = { "hydrate-A", "hydrate-B" }; + private static final AtomicInteger CONTENDED_SESSION_INDEX = new AtomicInteger(); + + public static String contendedSessionId() + { + return CONTENDED_SESSION_IDS[CONTENDED_SESSION_INDEX.getAndIncrement() % CONTENDED_SESSION_IDS.length]; + } +} diff --git a/runtime/binding-mcp/src/test/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/McpProxyLifecycleIT.java b/runtime/binding-mcp/src/test/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/McpProxyLifecycleIT.java new file mode 100644 index 0000000000..3bd4b655da --- /dev/null +++ b/runtime/binding-mcp/src/test/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/McpProxyLifecycleIT.java @@ -0,0 +1,114 @@ +/* + * Copyright 2021-2024 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. + */ +package io.aklivity.zilla.runtime.binding.mcp.internal.stream; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.junit.rules.RuleChain.outerRule; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.DisableOnDebug; +import org.junit.rules.TestRule; +import org.junit.rules.Timeout; + +import io.aklivity.k3po.runtime.junit.annotation.ScriptProperty; +import io.aklivity.k3po.runtime.junit.annotation.Specification; +import io.aklivity.k3po.runtime.junit.rules.K3poRule; +import io.aklivity.zilla.runtime.engine.test.EngineRule; +import io.aklivity.zilla.runtime.engine.test.annotation.Configuration; + +public class McpProxyLifecycleIT +{ + private final K3poRule k3po = new K3poRule() + .addScriptRoot("app", "io/aklivity/zilla/specs/binding/mcp/streams/application"); + + private final TestRule timeout = new DisableOnDebug(new Timeout(10, SECONDS)); + + private final EngineRule engine = new EngineRule() + .directory("target/zilla-itests") + .countersBufferCapacity(8192) + .configurationRoot("io/aklivity/zilla/specs/binding/mcp/config") + .external("app1") + .clean(); + + @Rule + public final TestRule chain = outerRule(engine).around(k3po).around(timeout); + + @Test + @Configuration("proxy.yaml") + @Specification({ + "${app}/lifecycle.server.write.abort/client", + "${app}/lifecycle.server.write.abort/server" }) + @ScriptProperty("serverAddress \"zilla://streams/app1\"") + public void shouldLifecycleServerWriteAbort() throws Exception + { + k3po.finish(); + } + + @Test + @Configuration("proxy.yaml") + @Specification({ + "${app}/lifecycle.server.write.close/client", + "${app}/lifecycle.server.write.close/server" }) + @ScriptProperty("serverAddress \"zilla://streams/app1\"") + public void shouldLifecycleServerWriteClose() throws Exception + { + k3po.finish(); + } + + @Test + @Configuration("proxy.yaml") + @Specification({ + "${app}/lifecycle.server.read.abort/client", + "${app}/lifecycle.server.read.abort/server" }) + @ScriptProperty("serverAddress \"zilla://streams/app1\"") + public void shouldLifecycleServerReadAbort() throws Exception + { + k3po.finish(); + } + + @Test + @Configuration("proxy.yaml") + @Specification({ + "${app}/lifecycle.client.write.abort/client", + "${app}/lifecycle.client.write.abort/server" }) + @ScriptProperty("serverAddress \"zilla://streams/app1\"") + public void shouldLifecycleClientWriteAbort() throws Exception + { + k3po.finish(); + } + + @Test + @Configuration("proxy.yaml") + @Specification({ + "${app}/lifecycle.client.write.close/client", + "${app}/lifecycle.client.write.close/server" }) + @ScriptProperty("serverAddress \"zilla://streams/app1\"") + public void shouldLifecycleClientWriteClose() throws Exception + { + k3po.finish(); + } + + @Test + @Configuration("proxy.yaml") + @Specification({ + "${app}/lifecycle.client.read.abort/client", + "${app}/lifecycle.client.read.abort/server" }) + @ScriptProperty("serverAddress \"zilla://streams/app1\"") + public void shouldLifecycleClientReadAbort() throws Exception + { + k3po.finish(); + } +} diff --git a/runtime/binding-tls/src/test/java/io/aklivity/zilla/runtime/binding/tls/internal/bench/TlsWorker.java b/runtime/binding-tls/src/test/java/io/aklivity/zilla/runtime/binding/tls/internal/bench/TlsWorker.java index faf84a21b6..0f151b9739 100644 --- a/runtime/binding-tls/src/test/java/io/aklivity/zilla/runtime/binding/tls/internal/bench/TlsWorker.java +++ b/runtime/binding-tls/src/test/java/io/aklivity/zilla/runtime/binding/tls/internal/bench/TlsWorker.java @@ -23,6 +23,7 @@ import java.nio.channels.SelectableChannel; import java.nio.file.Path; import java.time.Clock; +import java.time.Instant; import java.util.function.IntConsumer; import java.util.function.LongConsumer; import java.util.function.LongSupplier; @@ -632,6 +633,15 @@ public long signalAt( return NO_CANCEL_ID; } + @Override + public long signalAt( + Instant time, + int signalId, + IntConsumer handler) + { + return signalAt(time.toEpochMilli(), signalId, handler); + } + @Override public long signalAt( long timeMillis, @@ -647,6 +657,19 @@ public long signalAt( return NO_CANCEL_ID; } + @Override + public long signalAt( + Instant time, + long originId, + long routedId, + long streamId, + long traceId, + int signalId, + int contextId) + { + return signalAt(time.toEpochMilli(), originId, routedId, streamId, traceId, signalId, contextId); + } + @Override public long signalTask( Runnable task, diff --git a/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/concurrent/Signaler.java b/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/concurrent/Signaler.java index 9b82e8b3a7..e1165fe383 100644 --- a/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/concurrent/Signaler.java +++ b/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/concurrent/Signaler.java @@ -15,6 +15,7 @@ */ package io.aklivity.zilla.runtime.engine.concurrent; +import java.time.Instant; import java.util.function.IntConsumer; import org.agrona.DirectBuffer; @@ -57,6 +58,16 @@ public interface Signaler */ long signalAt(long timeMillis, int signalId, IntConsumer handler); + /** + * Schedules a lightweight timer signal to fire at the given {@link Instant}. + * + * @param time the instant at which to fire the signal + * @param signalId an application-defined signal identifier passed to {@code handler} + * @param handler the callback to invoke with {@code signalId} when the timer fires + * @return a cancel id that can be passed to {@link #cancel}, or {@link #NO_CANCEL_ID} + */ + long signalAt(Instant time, int signalId, IntConsumer handler); + /** * Immediately delivers a signal to the stream identified by * {@code (originId, routedId, streamId)} as if a synthetic frame had arrived. @@ -100,6 +111,20 @@ void signalNow(long originId, long routedId, long streamId, long traceId, int si */ long signalAt(long timeMillis, long originId, long routedId, long streamId, long traceId, int signalId, int contextId); + /** + * Schedules a signal to be delivered to the target stream at the given {@link Instant}. + * + * @param time the instant at which to deliver the signal + * @param originId the origin binding id + * @param routedId the routed binding id + * @param streamId the stream id + * @param traceId the trace identifier + * @param signalId an application-defined signal identifier + * @param contextId an application-defined context value + * @return a cancel id that can be passed to {@link #cancel}, or {@link #NO_CANCEL_ID} + */ + long signalAt(Instant time, long originId, long routedId, long streamId, long traceId, int signalId, int contextId); + /** * Schedules a {@link Runnable} task to run on the owning I/O thread, delivered as a * signal to the target stream after the task has been queued. diff --git a/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/internal/registry/EngineWorker.java b/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/internal/registry/EngineWorker.java index 6f0728f377..62e409e26d 100644 --- a/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/internal/registry/EngineWorker.java +++ b/runtime/engine/src/main/java/io/aklivity/zilla/runtime/engine/internal/registry/EngineWorker.java @@ -51,6 +51,7 @@ import java.nio.file.Path; import java.time.Clock; import java.time.Duration; +import java.time.Instant; import java.util.BitSet; import java.util.Collection; import java.util.Deque; @@ -2221,6 +2222,15 @@ public long signalAt( return timerId; } + @Override + public long signalAt( + Instant time, + int signalId, + IntConsumer handler) + { + return signalAt(time.toEpochMilli(), signalId, handler); + } + @Override public long signalAt( long timeMillis, @@ -2240,6 +2250,19 @@ public long signalAt( return timerId; } + @Override + public long signalAt( + Instant time, + long originId, + long routedId, + long streamId, + long traceId, + int signalId, + int contextId) + { + return signalAt(time.toEpochMilli(), originId, routedId, streamId, traceId, signalId, contextId); + } + @Override public long signalTask( Runnable task, diff --git a/runtime/engine/src/test/java/io/aklivity/zilla/runtime/engine/test/internal/resolver/TestResolverSpi.java b/runtime/engine/src/test/java/io/aklivity/zilla/runtime/engine/test/internal/resolver/TestResolverSpi.java index b99ad4d2fc..2d5220b675 100644 --- a/runtime/engine/src/test/java/io/aklivity/zilla/runtime/engine/test/internal/resolver/TestResolverSpi.java +++ b/runtime/engine/src/test/java/io/aklivity/zilla/runtime/engine/test/internal/resolver/TestResolverSpi.java @@ -15,10 +15,15 @@ */ package io.aklivity.zilla.runtime.engine.test.internal.resolver; +import java.util.Base64; +import java.util.Random; + import io.aklivity.zilla.runtime.engine.resolver.ResolverSpi; public class TestResolverSpi implements ResolverSpi { + private static final String RANDOM_BASE64_PREFIX = "randomBase64."; + public String resolve( String var) { @@ -36,6 +41,13 @@ else if ("EXPRESSION".equals(var)) { result = "${{test.EXPRESSION}}"; } + else if (var.startsWith(RANDOM_BASE64_PREFIX)) + { + final int length = Integer.parseInt(var.substring(RANDOM_BASE64_PREFIX.length())); + final byte[] bytes = new byte[length]; + new Random(length).nextBytes(bytes); + result = Base64.getEncoder().encodeToString(bytes); + } return result; } diff --git a/runtime/engine/src/test/java/io/aklivity/zilla/runtime/engine/test/internal/store/TestStore.java b/runtime/engine/src/test/java/io/aklivity/zilla/runtime/engine/test/internal/store/TestStore.java index d0c412912d..c4276685ff 100644 --- a/runtime/engine/src/test/java/io/aklivity/zilla/runtime/engine/test/internal/store/TestStore.java +++ b/runtime/engine/src/test/java/io/aklivity/zilla/runtime/engine/test/internal/store/TestStore.java @@ -16,6 +16,8 @@ package io.aklivity.zilla.runtime.engine.test.internal.store; import java.net.URL; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import io.aklivity.zilla.runtime.engine.Configuration; import io.aklivity.zilla.runtime.engine.EngineContext; @@ -25,9 +27,12 @@ public final class TestStore implements Store { public static final String NAME = "test"; + private final ConcurrentMap> storage; + public TestStore( Configuration config) { + this.storage = new ConcurrentHashMap<>(); } @Override @@ -46,6 +51,12 @@ public URL type() public TestStoreContext supply( EngineContext context) { - return new TestStoreContext(context); + return new TestStoreContext(context, this::acquireEntries); + } + + private ConcurrentMap acquireEntries( + long storeId) + { + return storage.computeIfAbsent(storeId, id -> new ConcurrentHashMap<>()); } } diff --git a/runtime/engine/src/test/java/io/aklivity/zilla/runtime/engine/test/internal/store/TestStoreContext.java b/runtime/engine/src/test/java/io/aklivity/zilla/runtime/engine/test/internal/store/TestStoreContext.java index 733f5a3357..1ee82975be 100644 --- a/runtime/engine/src/test/java/io/aklivity/zilla/runtime/engine/test/internal/store/TestStoreContext.java +++ b/runtime/engine/src/test/java/io/aklivity/zilla/runtime/engine/test/internal/store/TestStoreContext.java @@ -15,26 +15,38 @@ */ package io.aklivity.zilla.runtime.engine.test.internal.store; +import java.util.concurrent.ConcurrentMap; +import java.util.function.LongFunction; + import io.aklivity.zilla.runtime.engine.EngineContext; import io.aklivity.zilla.runtime.engine.concurrent.Signaler; import io.aklivity.zilla.runtime.engine.config.StoreConfig; import io.aklivity.zilla.runtime.engine.store.StoreContext; +import io.aklivity.zilla.runtime.engine.test.internal.store.config.TestStoreOptionsConfig; public final class TestStoreContext implements StoreContext { private final Signaler signaler; + private final LongFunction> supplyEntries; public TestStoreContext( - EngineContext context) + EngineContext context, + LongFunction> supplyEntries) { this.signaler = context.signaler(); + this.supplyEntries = supplyEntries; } @Override public TestStoreHandler attach( StoreConfig store) { - return new TestStoreHandler(store, signaler); + final ConcurrentMap entries = supplyEntries.apply(store.id); + if (store.options instanceof TestStoreOptionsConfig options && options.entries != null) + { + options.entries.forEach(entries::putIfAbsent); + } + return new TestStoreHandler(store, signaler, entries); } @Override diff --git a/runtime/engine/src/test/java/io/aklivity/zilla/runtime/engine/test/internal/store/TestStoreHandler.java b/runtime/engine/src/test/java/io/aklivity/zilla/runtime/engine/test/internal/store/TestStoreHandler.java index a2d95a3560..360319f7ec 100644 --- a/runtime/engine/src/test/java/io/aklivity/zilla/runtime/engine/test/internal/store/TestStoreHandler.java +++ b/runtime/engine/src/test/java/io/aklivity/zilla/runtime/engine/test/internal/store/TestStoreHandler.java @@ -15,9 +15,8 @@ */ package io.aklivity.zilla.runtime.engine.test.internal.store; -import java.util.HashMap; -import java.util.Map; import java.util.Objects; +import java.util.concurrent.ConcurrentMap; import java.util.function.BiConsumer; import java.util.function.Consumer; @@ -27,14 +26,15 @@ public final class TestStoreHandler implements StoreHandler { - private final Map entries; + private final ConcurrentMap entries; private final Signaler signaler; public TestStoreHandler( StoreConfig store, - Signaler signaler) + Signaler signaler, + ConcurrentMap entries) { - this.entries = new HashMap<>(); + this.entries = Objects.requireNonNull(entries); this.signaler = Objects.requireNonNull(signaler); } diff --git a/runtime/engine/src/test/java/io/aklivity/zilla/runtime/engine/test/internal/store/config/TestStoreOptionsConfig.java b/runtime/engine/src/test/java/io/aklivity/zilla/runtime/engine/test/internal/store/config/TestStoreOptionsConfig.java new file mode 100644 index 0000000000..04b3468b91 --- /dev/null +++ b/runtime/engine/src/test/java/io/aklivity/zilla/runtime/engine/test/internal/store/config/TestStoreOptionsConfig.java @@ -0,0 +1,43 @@ +/* + * Copyright 2021-2024 Aklivity Inc. + * + * Aklivity licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.aklivity.zilla.runtime.engine.test.internal.store.config; + +import java.util.Map; +import java.util.function.Function; + +import io.aklivity.zilla.runtime.engine.config.OptionsConfig; + +public final class TestStoreOptionsConfig extends OptionsConfig +{ + public final Map entries; + + public static TestStoreOptionsConfigBuilder builder() + { + return new TestStoreOptionsConfigBuilder<>(TestStoreOptionsConfig.class::cast); + } + + public static TestStoreOptionsConfigBuilder builder( + Function mapper) + { + return new TestStoreOptionsConfigBuilder<>(mapper); + } + + TestStoreOptionsConfig( + Map entries) + { + this.entries = entries; + } +} diff --git a/runtime/engine/src/test/java/io/aklivity/zilla/runtime/engine/test/internal/store/config/TestStoreOptionsConfigAdapter.java b/runtime/engine/src/test/java/io/aklivity/zilla/runtime/engine/test/internal/store/config/TestStoreOptionsConfigAdapter.java new file mode 100644 index 0000000000..8ecc88fa65 --- /dev/null +++ b/runtime/engine/src/test/java/io/aklivity/zilla/runtime/engine/test/internal/store/config/TestStoreOptionsConfigAdapter.java @@ -0,0 +1,75 @@ +/* + * Copyright 2021-2024 Aklivity Inc. + * + * Aklivity licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.aklivity.zilla.runtime.engine.test.internal.store.config; + +import jakarta.json.Json; +import jakarta.json.JsonObject; +import jakarta.json.JsonObjectBuilder; +import jakarta.json.JsonString; + +import io.aklivity.zilla.runtime.engine.config.OptionsConfig; +import io.aklivity.zilla.runtime.engine.config.OptionsConfigAdapterSpi; + +public final class TestStoreOptionsConfigAdapter implements OptionsConfigAdapterSpi +{ + private static final String ENTRIES_NAME = "entries"; + + @Override + public Kind kind() + { + return Kind.STORE; + } + + @Override + public String type() + { + return "test"; + } + + @Override + public JsonObject adaptToJson( + OptionsConfig options) + { + TestStoreOptionsConfig testOptions = (TestStoreOptionsConfig) options; + + JsonObjectBuilder object = Json.createObjectBuilder(); + + if (testOptions.entries != null && + !testOptions.entries.isEmpty()) + { + JsonObjectBuilder entries = Json.createObjectBuilder(); + testOptions.entries.forEach(entries::add); + object.add(ENTRIES_NAME, entries); + } + + return object.build(); + } + + @Override + public OptionsConfig adaptFromJson( + JsonObject object) + { + TestStoreOptionsConfigBuilder testOptions = TestStoreOptionsConfig.builder(); + + if (object != null && object.containsKey(ENTRIES_NAME)) + { + object.getJsonObject(ENTRIES_NAME) + .forEach((key, value) -> testOptions.entry(key, ((JsonString) value).getString())); + } + + return testOptions.build(); + } +} diff --git a/runtime/engine/src/test/java/io/aklivity/zilla/runtime/engine/test/internal/store/config/TestStoreOptionsConfigBuilder.java b/runtime/engine/src/test/java/io/aklivity/zilla/runtime/engine/test/internal/store/config/TestStoreOptionsConfigBuilder.java new file mode 100644 index 0000000000..8ce9223ad5 --- /dev/null +++ b/runtime/engine/src/test/java/io/aklivity/zilla/runtime/engine/test/internal/store/config/TestStoreOptionsConfigBuilder.java @@ -0,0 +1,61 @@ +/* + * Copyright 2021-2024 Aklivity Inc. + * + * Aklivity licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.aklivity.zilla.runtime.engine.test.internal.store.config; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.Function; + +import io.aklivity.zilla.runtime.engine.config.ConfigBuilder; +import io.aklivity.zilla.runtime.engine.config.OptionsConfig; + +public final class TestStoreOptionsConfigBuilder extends ConfigBuilder> +{ + private final Function mapper; + + private Map entries; + + TestStoreOptionsConfigBuilder( + Function mapper) + { + this.mapper = mapper; + } + + @Override + @SuppressWarnings("unchecked") + protected Class> thisType() + { + return (Class>) getClass(); + } + + public TestStoreOptionsConfigBuilder entry( + String key, + String value) + { + if (entries == null) + { + entries = new LinkedHashMap<>(); + } + entries.put(key, value); + return this; + } + + @Override + public T build() + { + return mapper.apply(new TestStoreOptionsConfig(entries)); + } +} diff --git a/runtime/engine/src/test/resources/META-INF/services/io.aklivity.zilla.runtime.engine.config.OptionsConfigAdapterSpi b/runtime/engine/src/test/resources/META-INF/services/io.aklivity.zilla.runtime.engine.config.OptionsConfigAdapterSpi index 54da69ad2e..594152f2f6 100644 --- a/runtime/engine/src/test/resources/META-INF/services/io.aklivity.zilla.runtime.engine.config.OptionsConfigAdapterSpi +++ b/runtime/engine/src/test/resources/META-INF/services/io.aklivity.zilla.runtime.engine.config.OptionsConfigAdapterSpi @@ -3,3 +3,4 @@ io.aklivity.zilla.runtime.engine.test.internal.guard.config.TestGuardOptionsConf io.aklivity.zilla.runtime.engine.test.internal.vault.config.TestVaultOptionsConfigAdapter io.aklivity.zilla.runtime.engine.test.internal.exporter.config.TestExporterOptionsConfigAdapter io.aklivity.zilla.runtime.engine.test.internal.catalog.config.TestCatalogOptionsConfigAdapter +io.aklivity.zilla.runtime.engine.test.internal.store.config.TestStoreOptionsConfigAdapter diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/config/proxy.cache.credentials.yaml b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/config/proxy.cache.credentials.yaml new file mode 100644 index 0000000000..1836c1fd3d --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/config/proxy.cache.credentials.yaml @@ -0,0 +1,36 @@ +# +# Copyright 2021-2024 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. +# + +--- +name: test +guards: + test_guard: + type: test + options: + credentials: "{token}" +stores: + memory0: + type: test +bindings: + app0: + type: mcp + kind: proxy + options: + cache: + store: memory0 + authorization: + test_guard: + credentials: "{token}" + exit: app1 diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/config/proxy.cache.refresh.yaml b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/config/proxy.cache.refresh.yaml new file mode 100644 index 0000000000..ce94797b82 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/config/proxy.cache.refresh.yaml @@ -0,0 +1,29 @@ +# +# Copyright 2021-2024 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. +# + +--- +name: test +stores: + memory0: + type: test +bindings: + app0: + type: mcp + kind: proxy + options: + cache: + store: memory0 + ttl: PT1S + exit: app1 diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/config/proxy.cache.seeded.tools.100k.yaml b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/config/proxy.cache.seeded.tools.100k.yaml new file mode 100644 index 0000000000..a32f495885 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/config/proxy.cache.seeded.tools.100k.yaml @@ -0,0 +1,36 @@ +# +# Copyright 2021-2024 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. +# + +--- +name: test +stores: + memory0: + type: test + options: + entries: + tools: |- + {"tools":[{"name":"big_tool","description":"${{test.randomBase64.100000}}"}]} + resources: |- + {"resources":[]} + prompts: |- + {"prompts":[]} +bindings: + app0: + type: mcp + kind: proxy + options: + cache: + store: memory0 + exit: app1 diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/config/proxy.cache.seeded.tools.10k.yaml b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/config/proxy.cache.seeded.tools.10k.yaml new file mode 100644 index 0000000000..18e66982d2 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/config/proxy.cache.seeded.tools.10k.yaml @@ -0,0 +1,36 @@ +# +# Copyright 2021-2024 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. +# + +--- +name: test +stores: + memory0: + type: test + options: + entries: + tools: |- + {"tools":[{"name":"big_tool","description":"${{test.randomBase64.10000}}"}]} + resources: |- + {"resources":[]} + prompts: |- + {"prompts":[]} +bindings: + app0: + type: mcp + kind: proxy + options: + cache: + store: memory0 + exit: app1 diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/config/proxy.cache.seeded.yaml b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/config/proxy.cache.seeded.yaml new file mode 100644 index 0000000000..c4f0a64d3b --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/config/proxy.cache.seeded.yaml @@ -0,0 +1,36 @@ +# +# Copyright 2021-2024 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. +# + +--- +name: test +stores: + memory0: + type: test + options: + entries: + tools: |- + {"tools":[{"name": "get_weather","title": "Weather Information Provider","description": "Get current weather information for a location","inputSchema": {"type": "object","properties": {"location": {"type": "string","description": "City name or zip code"}},"required": ["location"]},"icons": [{"src": "https://example.com/weather-icon.png","mimeType": "image/png","sizes": ["48x48"]}],"execution": {"taskSupport": "optional"}}]} + resources: |- + {"resources":[{"uri": "file:///docs/welcome.md","name": "welcome","description": "Welcome document","mimeType": "text/markdown"}]} + prompts: |- + {"prompts":[{"name": "summarize","description": "Summarize a document"}]} +bindings: + app0: + type: mcp + kind: proxy + options: + cache: + store: memory0 + exit: app1 diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/config/proxy.cache.toolkit.yaml b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/config/proxy.cache.toolkit.yaml new file mode 100644 index 0000000000..137255fc92 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/config/proxy.cache.toolkit.yaml @@ -0,0 +1,34 @@ +# +# Copyright 2021-2024 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. +# + +--- +name: test +stores: + memory0: + type: test +bindings: + app0: + type: mcp + kind: proxy + options: + cache: + store: memory0 + routes: + - exit: app1 + when: + - toolkit: bluesky + - exit: app2 + when: + - toolkit: quartz diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/config/proxy.cache.yaml b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/config/proxy.cache.yaml new file mode 100644 index 0000000000..da77a0ec12 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/config/proxy.cache.yaml @@ -0,0 +1,28 @@ +# +# Copyright 2021-2024 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. +# + +--- +name: test +stores: + memory0: + type: test +bindings: + app0: + type: mcp + kind: proxy + options: + cache: + store: memory0 + exit: app1 diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/schema/mcp.schema.patch.json b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/schema/mcp.schema.patch.json index d0ce7ac383..70c7edb77a 100644 --- a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/schema/mcp.schema.patch.json +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/schema/mcp.schema.patch.json @@ -38,6 +38,51 @@ { "properties": { + "cache": + { + "title": "Cache", + "type": "object", + "properties": + { + "store": + { + "title": "Store", + "type": "string" + }, + "ttl": + { + "title": "Refresh TTL", + "type": "string", + "format": "duration", + "default": "PT5M" + }, + "authorization": + { + "title": "Authorization", + "type": "object", + "patternProperties": + { + "^[A-Za-z0-9_-]+$": + { + "type": "object", + "properties": + { + "credentials": + { + "title": "Credentials", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false, + "maxProperties": 1 + } + }, + "required": [ "store" ], + "additionalProperties": false + }, "prompts": { "title": "Local Prompts", diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.hydrate.100k/client.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.hydrate.100k/client.rpt new file mode 100644 index 0000000000..5db5e82e91 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.hydrate.100k/client.rpt @@ -0,0 +1,99 @@ +# +# Copyright 2021-2024 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/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("hydrate-1") + .build() + .build()} + +read notify LIFECYCLE_INITIALIZED + +connect await LIFECYCLE_INITIALIZED + "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .promptsList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write close + +read '{"prompts":[]}' +read closed + +read notify PROMPTS_LIST_COMPLETE + +connect await PROMPTS_LIST_COMPLETE + "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .toolsList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write close + +read '{"tools":[{"name":"big_tool","description":"' +read ${core:randomBase64(100000)} +read '"}]}' +read closed + +read notify TOOLS_LIST_COMPLETE + +connect await TOOLS_LIST_COMPLETE + "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .resourcesList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write close + +read '{"resources":[]}' +read closed diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.hydrate.100k/server.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.hydrate.100k/server.rpt new file mode 100644 index 0000000000..0d2c7919b8 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.hydrate.100k/server.rpt @@ -0,0 +1,101 @@ +# +# Copyright 2021-2024 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. +# + +property serverAddress "zilla://streams/app0" + +accept ${serverAddress} + option zilla:window 8192 + option zilla:transmission "half-duplex" + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("hydrate-1") + .build() + .build()} +write flush + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .promptsList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write flush + +read closed + +write '{"prompts":[]}' +write flush + +write close + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .toolsList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write flush + +read closed + +write '{"tools":[{"name":"big_tool","description":"' +write ${core:randomBase64(100000)} +write '"}]}' +write flush + +write close + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .resourcesList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write flush + +read closed + +write '{"resources":[]}' +write flush + +write close diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.hydrate.10k/client.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.hydrate.10k/client.rpt new file mode 100644 index 0000000000..e8ccb15d8e --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.hydrate.10k/client.rpt @@ -0,0 +1,99 @@ +# +# Copyright 2021-2024 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/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("hydrate-1") + .build() + .build()} + +read notify LIFECYCLE_INITIALIZED + +connect await LIFECYCLE_INITIALIZED + "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .promptsList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write close + +read '{"prompts":[]}' +read closed + +read notify PROMPTS_LIST_COMPLETE + +connect await PROMPTS_LIST_COMPLETE + "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .toolsList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write close + +read '{"tools":[{"name":"big_tool","description":"' +read ${core:randomBase64(10000)} +read '"}]}' +read closed + +read notify TOOLS_LIST_COMPLETE + +connect await TOOLS_LIST_COMPLETE + "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .resourcesList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write close + +read '{"resources":[]}' +read closed diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.hydrate.10k/server.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.hydrate.10k/server.rpt new file mode 100644 index 0000000000..5c12dbe549 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.hydrate.10k/server.rpt @@ -0,0 +1,101 @@ +# +# Copyright 2021-2024 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. +# + +property serverAddress "zilla://streams/app0" + +accept ${serverAddress} + option zilla:window 8192 + option zilla:transmission "half-duplex" + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("hydrate-1") + .build() + .build()} +write flush + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .promptsList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write flush + +read closed + +write '{"prompts":[]}' +write flush + +write close + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .toolsList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write flush + +read closed + +write '{"tools":[{"name":"big_tool","description":"' +write ${core:randomBase64(10000)} +write '"}]}' +write flush + +write close + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .resourcesList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write flush + +read closed + +write '{"resources":[]}' +write flush + +write close diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.hydrate.credentials/client.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.hydrate.credentials/client.rpt new file mode 100644 index 0000000000..c9f36fb586 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.hydrate.credentials/client.rpt @@ -0,0 +1,103 @@ +# +# Copyright 2021-2024 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. +# + +property authorization 1L + +connect "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + option zilla:authorization ${authorization} + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("hydrate-1") + .build() + .build()} + +read notify LIFECYCLE_INITIALIZED + +connect await LIFECYCLE_INITIALIZED + "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + option zilla:authorization ${authorization} + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .promptsList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write close + +read '{"prompts":[]}' +read closed + +read notify PROMPTS_LIST_COMPLETE + +connect await PROMPTS_LIST_COMPLETE + "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + option zilla:authorization ${authorization} + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .toolsList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write close + +read '{"tools":[]}' +read closed + +read notify TOOLS_LIST_COMPLETE + +connect await TOOLS_LIST_COMPLETE + "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + option zilla:authorization ${authorization} + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .resourcesList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write close + +read '{"resources":[]}' +read closed diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.hydrate.credentials/server.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.hydrate.credentials/server.rpt new file mode 100644 index 0000000000..d69381ac53 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.hydrate.credentials/server.rpt @@ -0,0 +1,101 @@ +# +# Copyright 2021-2024 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. +# + +property serverAddress "zilla://streams/app0" +property authorization 1L + +accept ${serverAddress} + option zilla:window 8192 + option zilla:transmission "half-duplex" + option zilla:authorization ${authorization} + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("hydrate-1") + .build() + .build()} +write flush + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .promptsList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write flush + +read closed + +write '{"prompts":[]}' +write flush + +write close + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .toolsList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write flush + +read closed + +write '{"tools":[]}' +write flush + +write close + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .resourcesList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write flush + +read closed + +write '{"resources":[]}' +write flush + +write close diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.hydrate.error/client.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.hydrate.error/client.rpt new file mode 100644 index 0000000000..2bd6913d64 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.hydrate.error/client.rpt @@ -0,0 +1,97 @@ +# +# Copyright 2021-2024 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/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("hydrate-1") + .build() + .build()} + +read notify LIFECYCLE_INITIALIZED + +connect await LIFECYCLE_INITIALIZED + "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .promptsList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write close + +read '{"prompts":[]}' +read closed + +read notify PROMPTS_LIST_COMPLETE + +connect await PROMPTS_LIST_COMPLETE + "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .toolsList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write close + +read aborted + +read notify TOOLS_LIST_COMPLETE + +connect await TOOLS_LIST_COMPLETE + "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .resourcesList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write close + +read '{"resources":[]}' +read closed diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.hydrate.error/server.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.hydrate.error/server.rpt new file mode 100644 index 0000000000..b56d6ec903 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.hydrate.error/server.rpt @@ -0,0 +1,92 @@ +# +# Copyright 2021-2024 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. +# + +property serverAddress "zilla://streams/app0" + +accept ${serverAddress} + option zilla:window 8192 + option zilla:transmission "half-duplex" + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("hydrate-1") + .build() + .build()} +write flush + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .promptsList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write flush + +read closed + +write '{"prompts":[]}' +write flush + +write close + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .toolsList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write abort + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .resourcesList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write flush + +read closed + +write '{"resources":[]}' +write flush + +write close diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.hydrate.lifecycle.reconnect/client.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.hydrate.lifecycle.reconnect/client.rpt new file mode 100644 index 0000000000..2ef85ea9a7 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.hydrate.lifecycle.reconnect/client.rpt @@ -0,0 +1,190 @@ +# +# Copyright 2021-2024 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/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("hydrate-1") + .build() + .build()} + +read notify LIFECYCLE_OPEN + +read await ALL_HYDRATED + +read aborted + +read notify LIFECYCLE_ABORTED + +connect await LIFECYCLE_OPEN + "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .promptsList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write close + +read '{"prompts":[]}' +read closed + +read notify PROMPTS_HYDRATED + +connect await PROMPTS_HYDRATED + "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .toolsList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write close + +read '{"tools":[{"name":"get_weather"}]}' +read closed + +read notify TOOLS_HYDRATED + +connect await TOOLS_HYDRATED + "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .resourcesList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write close + +read '{"resources":[]}' +read closed + +read notify ALL_HYDRATED + +connect await LIFECYCLE_ABORTED + "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("hydrate-1") + .build() + .build()} + +read notify LIFECYCLE2_OPEN + +connect await LIFECYCLE2_OPEN + "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .promptsList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write close + +read '{"prompts":[]}' +read closed + +read notify PROMPTS2_HYDRATED + +connect await PROMPTS2_HYDRATED + "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .toolsList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write close + +read '{"tools":[{"name":"get_weather"},{"name":"get_time"}]}' +read closed + +read notify TOOLS2_HYDRATED + +connect await TOOLS2_HYDRATED + "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .resourcesList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write close + +read '{"resources":[]}' +read closed diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.hydrate.lifecycle.reconnect/server.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.hydrate.lifecycle.reconnect/server.rpt new file mode 100644 index 0000000000..338171aa39 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.hydrate.lifecycle.reconnect/server.rpt @@ -0,0 +1,184 @@ +# +# Copyright 2021-2024 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. +# + +property serverAddress "zilla://streams/app0" + +accept ${serverAddress} + option zilla:window 8192 + option zilla:transmission "half-duplex" + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("hydrate-1") + .build() + .build()} +write flush + +write await ALL_HYDRATED + +write abort + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .promptsList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write flush + +read closed + +write '{"prompts":[]}' +write flush + +write close + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .toolsList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write flush + +read closed + +write '{"tools":[{"name":"get_weather"}]}' +write flush + +write close + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .resourcesList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write flush + +read closed + +write '{"resources":[]}' +write flush + +write notify ALL_HYDRATED + +write close + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("hydrate-1") + .build() + .build()} +write flush + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .promptsList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write flush + +read closed + +write '{"prompts":[]}' +write flush + +write close + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .toolsList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write flush + +read closed + +write '{"tools":[{"name":"get_weather"},{"name":"get_time"}]}' +write flush + +write close + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .resourcesList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write flush + +read closed + +write '{"resources":[]}' +write flush + +write close diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.hydrate.toolkit/client.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.hydrate.toolkit/client.rpt new file mode 100644 index 0000000000..5d65835f03 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.hydrate.toolkit/client.rpt @@ -0,0 +1,184 @@ +# +# Copyright 2021-2024 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/app1" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("hydrate-1") + .build() + .build()} + +read notify APP1_LIFECYCLE + +connect await APP1_LIFECYCLE + "zilla://streams/app1" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .promptsList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write close + +read '{"prompts":[{"name":"summarize"}]}' +read closed + +read notify APP1_PROMPTS + +connect await APP1_PROMPTS + "zilla://streams/app1" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .toolsList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write close + +read '{"tools":[{"name":"get_weather"}]}' +read closed + +read notify APP1_TOOLS + +connect await APP1_TOOLS + "zilla://streams/app1" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .resourcesList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write close + +read '{"resources":[{"uri":"file:///bluesky.txt"}]}' +read closed + +read notify APP1_RESOURCES + +connect await APP1_RESOURCES + "zilla://streams/app2" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("hydrate-1") + .build() + .build()} + +read notify APP2_LIFECYCLE + +connect await APP2_LIFECYCLE + "zilla://streams/app2" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .promptsList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write close + +read '{"prompts":[{"name":"translate"}]}' +read closed + +read notify APP2_PROMPTS + +connect await APP2_PROMPTS + "zilla://streams/app2" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .toolsList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write close + +read '{"tools":[{"name":"get_current_time"}]}' +read closed + +read notify APP2_TOOLS + +connect await APP2_TOOLS + "zilla://streams/app2" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .resourcesList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write close + +read '{"resources":[{"uri":"file:///quartz.txt"}]}' +read closed diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.hydrate.toolkit/server.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.hydrate.toolkit/server.rpt new file mode 100644 index 0000000000..d7929b6bbd --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.hydrate.toolkit/server.rpt @@ -0,0 +1,181 @@ +# +# Copyright 2021-2024 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/app1" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("hydrate-1") + .build() + .build()} +write flush + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .promptsList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write flush + +read closed + +write '{"prompts":[{"name":"summarize"}]}' +write flush + +write close + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .toolsList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write flush + +read closed + +write '{"tools":[{"name":"get_weather"}]}' +write flush + +write close + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .resourcesList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write flush + +read closed + +write '{"resources":[{"uri":"file:///bluesky.txt"}]}' +write flush + +write close + + +accept "zilla://streams/app2" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("hydrate-1") + .build() + .build()} +write flush + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .promptsList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write flush + +read closed + +write '{"prompts":[{"name":"translate"}]}' +write flush + +write close + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .toolsList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write flush + +read closed + +write '{"tools":[{"name":"get_current_time"}]}' +write flush + +write close + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .resourcesList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write flush + +read closed + +write '{"resources":[{"uri":"file:///quartz.txt"}]}' +write flush + +write close diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.hydrate/client.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.hydrate/client.rpt new file mode 100644 index 0000000000..679bab97b7 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.hydrate/client.rpt @@ -0,0 +1,98 @@ +# +# Copyright 2021-2024 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/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("hydrate-1") + .build() + .build()} + +read notify LIFECYCLE_INITIALIZED + +connect await LIFECYCLE_INITIALIZED + "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .promptsList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write close + +read '{"prompts":[]}' +read closed + +read notify PROMPTS_LIST_COMPLETE + +connect await PROMPTS_LIST_COMPLETE + "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .toolsList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write close + +read '{"tools":[]}' +read closed + +read notify TOOLS_LIST_COMPLETE + +connect await TOOLS_LIST_COMPLETE + "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .resourcesList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write close + +read '{"resources":[]}' +read closed + diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.hydrate/server.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.hydrate/server.rpt new file mode 100644 index 0000000000..72b7f11a6e --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.hydrate/server.rpt @@ -0,0 +1,100 @@ +# +# Copyright 2021-2024 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. +# + +property serverAddress "zilla://streams/app0" + +accept ${serverAddress} + option zilla:window 8192 + option zilla:transmission "half-duplex" + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("hydrate-1") + .build() + .build()} +write flush + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .promptsList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write flush + +read closed + +write '{"prompts":[]}' +write flush + +write close + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .toolsList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write flush + +read closed + +write '{"tools":[]}' +write flush + +write close + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .resourcesList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write flush + +read closed + +write '{"resources":[]}' +write flush + +write close + diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.refresh.prompts/client.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.refresh.prompts/client.rpt new file mode 100644 index 0000000000..39561d5a7b --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.refresh.prompts/client.rpt @@ -0,0 +1,76 @@ +# +# Copyright 2021-2024 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/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("hydrate-1") + .build() + .build()} + +read notify LIFECYCLE_INITIALIZED + +connect await LIFECYCLE_INITIALIZED + "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .promptsList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write close + +read '{"prompts":[{"name":"summarize"}]}' +read closed + +read notify HYDRATED + +connect await HYDRATED + "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .promptsList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write close + +read '{"prompts":[{"name":"summarize"},{"name":"translate"}]}' +read closed diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.refresh.prompts/server.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.refresh.prompts/server.rpt new file mode 100644 index 0000000000..eb5cb84daa --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.refresh.prompts/server.rpt @@ -0,0 +1,79 @@ +# +# Copyright 2021-2024 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. +# + +property serverAddress "zilla://streams/app0" + +accept ${serverAddress} + option zilla:window 8192 + option zilla:transmission "half-duplex" + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("hydrate-1") + .build() + .build()} +write flush + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .promptsList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write flush + +read closed + +write '{"prompts":[{"name":"summarize"}]}' +write flush + +write close + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .promptsList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write flush + +read closed + +write '{"prompts":[{"name":"summarize"},{"name":"translate"}]}' +write flush + +write close diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.refresh.resources/client.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.refresh.resources/client.rpt new file mode 100644 index 0000000000..7382668fed --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.refresh.resources/client.rpt @@ -0,0 +1,76 @@ +# +# Copyright 2021-2024 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/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("hydrate-1") + .build() + .build()} + +read notify LIFECYCLE_INITIALIZED + +connect await LIFECYCLE_INITIALIZED + "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .resourcesList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write close + +read '{"resources":[{"uri":"file:///docs/welcome.md","name":"welcome"}]}' +read closed + +read notify HYDRATED + +connect await HYDRATED + "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .resourcesList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write close + +read '{"resources":[{"uri":"file:///docs/welcome.md","name":"welcome"},{"uri":"file:///docs/changelog.md","name":"changelog"}]}' +read closed diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.refresh.resources/server.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.refresh.resources/server.rpt new file mode 100644 index 0000000000..8c1bc7392f --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.refresh.resources/server.rpt @@ -0,0 +1,79 @@ +# +# Copyright 2021-2024 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. +# + +property serverAddress "zilla://streams/app0" + +accept ${serverAddress} + option zilla:window 8192 + option zilla:transmission "half-duplex" + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("hydrate-1") + .build() + .build()} +write flush + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .resourcesList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write flush + +read closed + +write '{"resources":[{"uri":"file:///docs/welcome.md","name":"welcome"}]}' +write flush + +write close + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .resourcesList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write flush + +read closed + +write '{"resources":[{"uri":"file:///docs/welcome.md","name":"welcome"},{"uri":"file:///docs/changelog.md","name":"changelog"}]}' +write flush + +write close diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.refresh.tools.contended/client.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.refresh.tools.contended/client.rpt new file mode 100644 index 0000000000..6b7315962a --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.refresh.tools.contended/client.rpt @@ -0,0 +1,77 @@ +# +# Copyright 2021-2024 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/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("hydrate-A") + .build() + .build()} + +connected + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("hydrate-A") + .build() + .build()} + +read notify LIFECYCLE_INITIALIZED + +connect await LIFECYCLE_INITIALIZED + "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .toolsList() + .sessionId("hydrate-A") + .build() + .build()} + +connected + +write close + +read '{"tools":[{"name":"get_weather"}]}' +read closed + +read notify TOOLS_LIST_COMPLETE + +connect await TOOLS_LIST_COMPLETE + "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .toolsList() + .sessionId("hydrate-A") + .build() + .build()} + +connected + +write close + +read '{"tools":[{"name":"get_weather"},{"name":"get_time"}]}' +read closed diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.refresh.tools.contended/server.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.refresh.tools.contended/server.rpt new file mode 100644 index 0000000000..0009e15327 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.refresh.tools.contended/server.rpt @@ -0,0 +1,80 @@ +# +# Copyright 2021-2024 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. +# + + +property serverAddress "zilla://streams/app0" + +accept ${serverAddress} + option zilla:window 8192 + option zilla:transmission "half-duplex" + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("hydrate-A") + .build() + .build()} + +connected + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("hydrate-A") + .build() + .build()} +write flush + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .toolsList() + .sessionId("hydrate-A") + .build() + .build()} + +connected + +write flush + +read closed + +write '{"tools":[{"name":"get_weather"}]}' +write flush + +write close + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .toolsList() + .sessionId("hydrate-A") + .build() + .build()} + +connected + +write flush + +read closed + +write '{"tools":[{"name":"get_weather"},{"name":"get_time"}]}' +write flush + +write close diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.refresh.tools.error.retry/client.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.refresh.tools.error.retry/client.rpt new file mode 100644 index 0000000000..7f7c73352e --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.refresh.tools.error.retry/client.rpt @@ -0,0 +1,97 @@ +# +# Copyright 2021-2024 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/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("hydrate-1") + .build() + .build()} + +read notify LIFECYCLE_INITIALIZED + +connect await LIFECYCLE_INITIALIZED + "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .toolsList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write close + +read '{"tools":[{"name":"get_weather"}]}' +read closed + +read notify HYDRATED + +connect await HYDRATED + "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .toolsList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write close + +read aborted + +read notify REFRESH_ABORTED + +connect await REFRESH_ABORTED + "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .toolsList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write close + +read '{"tools":[{"name":"get_weather"},{"name":"get_time"}]}' +read closed diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.refresh.tools.error.retry/server.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.refresh.tools.error.retry/server.rpt new file mode 100644 index 0000000000..4f6a74c9c1 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.refresh.tools.error.retry/server.rpt @@ -0,0 +1,93 @@ +# +# Copyright 2021-2024 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. +# + + +property serverAddress "zilla://streams/app0" + +accept ${serverAddress} + option zilla:window 8192 + option zilla:transmission "half-duplex" + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("hydrate-1") + .build() + .build()} +write flush + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .toolsList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write flush + +read closed + +write '{"tools":[{"name":"get_weather"}]}' +write flush + +write close + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .toolsList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write abort + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .toolsList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write flush + +read closed + +write '{"tools":[{"name":"get_weather"},{"name":"get_time"}]}' +write flush + +write close diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.refresh.tools.error/client.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.refresh.tools.error/client.rpt new file mode 100644 index 0000000000..3240c4f0ec --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.refresh.tools.error/client.rpt @@ -0,0 +1,76 @@ +# +# Copyright 2021-2024 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/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("hydrate-1") + .build() + .build()} + +read notify LIFECYCLE_INITIALIZED + +connect await LIFECYCLE_INITIALIZED + "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .toolsList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write close + +read '{"tools":[{"name":"get_weather"}]}' +read closed + +read notify HYDRATED + +connect await HYDRATED + "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .toolsList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write close + +read aborted diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.refresh.tools.error/server.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.refresh.tools.error/server.rpt new file mode 100644 index 0000000000..9192aa81de --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.refresh.tools.error/server.rpt @@ -0,0 +1,73 @@ +# +# Copyright 2021-2024 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. +# + + +property serverAddress "zilla://streams/app0" + +accept ${serverAddress} + option zilla:window 8192 + option zilla:transmission "half-duplex" + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("hydrate-1") + .build() + .build()} +write flush + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .toolsList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write flush + +read closed + +write '{"tools":[{"name":"get_weather"}]}' +write flush + +write close + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .toolsList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write abort diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.refresh.tools/client.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.refresh.tools/client.rpt new file mode 100644 index 0000000000..0fda06f081 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.refresh.tools/client.rpt @@ -0,0 +1,76 @@ +# +# Copyright 2021-2024 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/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("hydrate-1") + .build() + .build()} + +read notify LIFECYCLE_INITIALIZED + +connect await LIFECYCLE_INITIALIZED + "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .toolsList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write close + +read '{"tools":[{"name":"get_weather"}]}' +read closed + +read notify HYDRATED + +connect await HYDRATED + "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .toolsList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write close + +read '{"tools":[{"name":"get_weather"},{"name":"get_time"}]}' +read closed diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.refresh.tools/server.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.refresh.tools/server.rpt new file mode 100644 index 0000000000..f8d145d316 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.refresh.tools/server.rpt @@ -0,0 +1,79 @@ +# +# Copyright 2021-2024 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. +# + +property serverAddress "zilla://streams/app0" + +accept ${serverAddress} + option zilla:window 8192 + option zilla:transmission "half-duplex" + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("hydrate-1") + .build() + .build()} +write flush + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .toolsList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write flush + +read closed + +write '{"tools":[{"name":"get_weather"}]}' +write flush + +write close + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .toolsList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write flush + +read closed + +write '{"tools":[{"name":"get_weather"},{"name":"get_time"}]}' +write flush + +write close diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.serve.initialize/client.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.serve.initialize/client.rpt new file mode 100644 index 0000000000..18272ac030 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.serve.initialize/client.rpt @@ -0,0 +1,34 @@ +# +# Copyright 2021-2024 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/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("agent-1") + .build() + .build()} + +connected + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("agent-1") + .build() + .build()} diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.serve.initialize/server.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.serve.initialize/server.rpt new file mode 100644 index 0000000000..588a71eec7 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.serve.initialize/server.rpt @@ -0,0 +1,40 @@ +# +# Copyright 2021-2024 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. +# + + +property serverAddress "zilla://streams/app0" + +accept ${serverAddress} + option zilla:window 8192 + option zilla:transmission "half-duplex" + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("agent-1") + .build() + .build()} + +connected + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("agent-1") + .build() + .build()} +write flush diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.serve.prompts.list/client.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.serve.prompts.list/client.rpt new file mode 100644 index 0000000000..b0d753d6f1 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.serve.prompts.list/client.rpt @@ -0,0 +1,62 @@ +# +# Copyright 2021-2024 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/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("agent-1") + .build() + .build()} + +connected + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("agent-1") + .build() + .build()} + +read notify LIFECYCLE_INITIALIZED + +connect await LIFECYCLE_INITIALIZED + "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .promptsList() + .sessionId("agent-1") + .build() + .build()} + +connected + +write close + +read '{"prompts":' + '[' + '{' + '"name": "summarize",' + '"description": "Summarize a document"' + '}' + ']' + '}' +read closed diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.serve.prompts.list/server.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.serve.prompts.list/server.rpt new file mode 100644 index 0000000000..5411037b3c --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.serve.prompts.list/server.rpt @@ -0,0 +1,67 @@ +# +# Copyright 2021-2024 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. +# + + +property serverAddress "zilla://streams/app0" + +accept ${serverAddress} + option zilla:window 8192 + option zilla:transmission "half-duplex" + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("agent-1") + .build() + .build()} + +connected + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("agent-1") + .build() + .build()} +write flush + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .promptsList() + .sessionId("agent-1") + .build() + .build()} + +connected + +write flush + +read closed + +write '{"prompts":' + '[' + '{' + '"name": "summarize",' + '"description": "Summarize a document"' + '}' + ']' + '}' +write flush + +write close diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.serve.resources.list/client.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.serve.resources.list/client.rpt new file mode 100644 index 0000000000..197f038b1e --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.serve.resources.list/client.rpt @@ -0,0 +1,64 @@ +# +# Copyright 2021-2024 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/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("agent-1") + .build() + .build()} + +connected + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("agent-1") + .build() + .build()} + +read notify LIFECYCLE_INITIALIZED + +connect await LIFECYCLE_INITIALIZED + "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .resourcesList() + .sessionId("agent-1") + .build() + .build()} + +connected + +write close + +read '{"resources":' + '[' + '{' + '"uri": "file:///docs/welcome.md",' + '"name": "welcome",' + '"description": "Welcome document",' + '"mimeType": "text/markdown"' + '}' + ']' + '}' +read closed diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.serve.resources.list/server.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.serve.resources.list/server.rpt new file mode 100644 index 0000000000..b71b650b78 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.serve.resources.list/server.rpt @@ -0,0 +1,69 @@ +# +# Copyright 2021-2024 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. +# + + +property serverAddress "zilla://streams/app0" + +accept ${serverAddress} + option zilla:window 8192 + option zilla:transmission "half-duplex" + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("agent-1") + .build() + .build()} + +connected + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("agent-1") + .build() + .build()} +write flush + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .resourcesList() + .sessionId("agent-1") + .build() + .build()} + +connected + +write flush + +read closed + +write '{"resources":' + '[' + '{' + '"uri": "file:///docs/welcome.md",' + '"name": "welcome",' + '"description": "Welcome document",' + '"mimeType": "text/markdown"' + '}' + ']' + '}' +write flush + +write close diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.serve.tools.list.100k/client.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.serve.tools.list.100k/client.rpt new file mode 100644 index 0000000000..ed7a83e8b5 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.serve.tools.list.100k/client.rpt @@ -0,0 +1,57 @@ +# +# Copyright 2021-2024 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/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("agent-1") + .build() + .build()} + +connected + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("agent-1") + .build() + .build()} + +read notify LIFECYCLE_INITIALIZED + +connect await LIFECYCLE_INITIALIZED + "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .toolsList() + .sessionId("agent-1") + .build() + .build()} + +connected + +write close + +read '{"tools":[{"name":"big_tool","description":"' +read ${core:randomBase64(100000)} +read '"}]}' +read closed diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.serve.tools.list.100k/server.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.serve.tools.list.100k/server.rpt new file mode 100644 index 0000000000..f8a56f9b01 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.serve.tools.list.100k/server.rpt @@ -0,0 +1,62 @@ +# +# Copyright 2021-2024 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. +# + + +property serverAddress "zilla://streams/app0" + +accept ${serverAddress} + option zilla:window 8192 + option zilla:transmission "half-duplex" + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("agent-1") + .build() + .build()} + +connected + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("agent-1") + .build() + .build()} +write flush + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .toolsList() + .sessionId("agent-1") + .build() + .build()} + +connected + +write flush + +read closed + +write '{"tools":[{"name":"big_tool","description":"' +write ${core:randomBase64(100000)} +write '"}]}' +write flush + +write close diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.serve.tools.list.10k/client.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.serve.tools.list.10k/client.rpt new file mode 100644 index 0000000000..868459b404 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.serve.tools.list.10k/client.rpt @@ -0,0 +1,57 @@ +# +# Copyright 2021-2024 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/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("agent-1") + .build() + .build()} + +connected + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("agent-1") + .build() + .build()} + +read notify LIFECYCLE_INITIALIZED + +connect await LIFECYCLE_INITIALIZED + "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .toolsList() + .sessionId("agent-1") + .build() + .build()} + +connected + +write close + +read '{"tools":[{"name":"big_tool","description":"' +read ${core:randomBase64(10000)} +read '"}]}' +read closed diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.serve.tools.list.10k/server.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.serve.tools.list.10k/server.rpt new file mode 100644 index 0000000000..53913ec521 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.serve.tools.list.10k/server.rpt @@ -0,0 +1,62 @@ +# +# Copyright 2021-2024 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. +# + + +property serverAddress "zilla://streams/app0" + +accept ${serverAddress} + option zilla:window 8192 + option zilla:transmission "half-duplex" + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("agent-1") + .build() + .build()} + +connected + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("agent-1") + .build() + .build()} +write flush + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .toolsList() + .sessionId("agent-1") + .build() + .build()} + +connected + +write flush + +read closed + +write '{"tools":[{"name":"big_tool","description":"' +write ${core:randomBase64(10000)} +write '"}]}' +write flush + +write close diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.serve.tools.list.during.hydrate/client.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.serve.tools.list.during.hydrate/client.rpt new file mode 100644 index 0000000000..1deb7fb322 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.serve.tools.list.during.hydrate/client.rpt @@ -0,0 +1,36 @@ +# +# Copyright 2021-2024 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/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("agent-1") + .build() + .build()} + +connected + +write notify CLIENT_AWAITER_REGISTERED + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("agent-1") + .build() + .build()} diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.serve.tools.list.during.hydrate/server.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.serve.tools.list.during.hydrate/server.rpt new file mode 100644 index 0000000000..e3349e1408 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.serve.tools.list.during.hydrate/server.rpt @@ -0,0 +1,61 @@ +# +# Copyright 2021-2024 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. +# + +property serverAddress "zilla://streams/app0" + +accept ${serverAddress} + option zilla:window 8192 + option zilla:transmission "half-duplex" + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("hydrate-1") + .build() + .build()} +write flush + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .toolsList() + .sessionId("hydrate-1") + .build() + .build()} + +connected + +write flush + +read closed + +read await CLIENT_AWAITER_REGISTERED + +write '{"tools":[{"name":"get_weather"}]}' +write flush + +write close diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.serve.tools.list/client.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.serve.tools.list/client.rpt new file mode 100644 index 0000000000..8fc5ece1f6 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.serve.tools.list/client.rpt @@ -0,0 +1,83 @@ +# +# Copyright 2021-2024 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/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("agent-1") + .build() + .build()} + +connected + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("agent-1") + .build() + .build()} + +read notify LIFECYCLE_INITIALIZED + +connect await LIFECYCLE_INITIALIZED + "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .toolsList() + .sessionId("agent-1") + .build() + .build()} + +connected + +write close + +read '{"tools":' + '[' + '{' + '"name": "get_weather",' + '"title": "Weather Information Provider",' + '"description": "Get current weather information for a location",' + '"inputSchema": {' + '"type": "object",' + '"properties": {' + '"location": {' + '"type": "string",' + '"description": "City name or zip code"' + '}' + '},' + '"required": ["location"]' + '},' + '"icons": [' + '{' + '"src": "https://example.com/weather-icon.png",' + '"mimeType": "image/png",' + '"sizes": ["48x48"]' + '}' + '],' + '"execution": {' + '"taskSupport": "optional"' + '}' + '}' + ']' + '}' +read closed diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.serve.tools.list/server.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.serve.tools.list/server.rpt new file mode 100644 index 0000000000..15c683b361 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/cache.serve.tools.list/server.rpt @@ -0,0 +1,88 @@ +# +# Copyright 2021-2024 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. +# + + +property serverAddress "zilla://streams/app0" + +accept ${serverAddress} + option zilla:window 8192 + option zilla:transmission "half-duplex" + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("agent-1") + .build() + .build()} + +connected + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("agent-1") + .build() + .build()} +write flush + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .toolsList() + .sessionId("agent-1") + .build() + .build()} + +connected + +write flush + +read closed + +write '{"tools":' + '[' + '{' + '"name": "get_weather",' + '"title": "Weather Information Provider",' + '"description": "Get current weather information for a location",' + '"inputSchema": {' + '"type": "object",' + '"properties": {' + '"location": {' + '"type": "string",' + '"description": "City name or zip code"' + '}' + '},' + '"required": ["location"]' + '},' + '"icons": [' + '{' + '"src": "https://example.com/weather-icon.png",' + '"mimeType": "image/png",' + '"sizes": ["48x48"]' + '}' + '],' + '"execution": {' + '"taskSupport": "optional"' + '}' + '}' + ']' + '}' +write flush + +write close diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/lifecycle.client.read.abort/client.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/lifecycle.client.read.abort/client.rpt new file mode 100644 index 0000000000..fed6415d49 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/lifecycle.client.read.abort/client.rpt @@ -0,0 +1,62 @@ +# +# Copyright 2021-2024 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/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("agent-1") + .build() + .build()} + +connected + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("agent-1") + .build() + .build()} + +read notify LIFECYCLE_OPEN + +read await TOOLS_LIST_COMPLETE + +read aborted + +connect await LIFECYCLE_OPEN + "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .toolsList() + .sessionId("agent-1") + .build() + .build()} + +connected + +write close + +read '{"tools":[]}' +read closed + +read notify TOOLS_LIST_COMPLETE diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/lifecycle.client.read.abort/server.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/lifecycle.client.read.abort/server.rpt new file mode 100644 index 0000000000..11e7d93d09 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/lifecycle.client.read.abort/server.rpt @@ -0,0 +1,65 @@ +# +# Copyright 2021-2024 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. +# + +property serverAddress "zilla://streams/app0" + +accept ${serverAddress} + option zilla:window 8192 + option zilla:transmission "half-duplex" + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("agent-1") + .build() + .build()} + +connected + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("agent-1") + .build() + .build()} +write flush + +write await TOOLS_LIST_COMPLETE + +write abort + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .toolsList() + .sessionId("agent-1") + .build() + .build()} + +connected + +write flush + +read closed + +write '{"tools":[]}' +write flush + +write close + +write notify TOOLS_LIST_COMPLETE diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/lifecycle.client.write.abort/client.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/lifecycle.client.write.abort/client.rpt new file mode 100644 index 0000000000..beb276996c --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/lifecycle.client.write.abort/client.rpt @@ -0,0 +1,62 @@ +# +# Copyright 2021-2024 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/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("agent-1") + .build() + .build()} + +connected + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("agent-1") + .build() + .build()} + +read notify LIFECYCLE_OPEN + +write await TOOLS_LIST_COMPLETE + +write abort + +connect await LIFECYCLE_OPEN + "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .toolsList() + .sessionId("agent-1") + .build() + .build()} + +connected + +write close + +read '{"tools":[]}' +read closed + +read notify TOOLS_LIST_COMPLETE diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/lifecycle.client.write.abort/server.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/lifecycle.client.write.abort/server.rpt new file mode 100644 index 0000000000..2fdb6aa927 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/lifecycle.client.write.abort/server.rpt @@ -0,0 +1,65 @@ +# +# Copyright 2021-2024 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. +# + +property serverAddress "zilla://streams/app0" + +accept ${serverAddress} + option zilla:window 8192 + option zilla:transmission "half-duplex" + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("agent-1") + .build() + .build()} + +connected + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("agent-1") + .build() + .build()} +write flush + +read await TOOLS_LIST_COMPLETE + +read aborted + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .toolsList() + .sessionId("agent-1") + .build() + .build()} + +connected + +write flush + +read closed + +write '{"tools":[]}' +write flush + +write close + +write notify TOOLS_LIST_COMPLETE diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/lifecycle.client.write.close/client.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/lifecycle.client.write.close/client.rpt new file mode 100644 index 0000000000..04f1b08d74 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/lifecycle.client.write.close/client.rpt @@ -0,0 +1,62 @@ +# +# Copyright 2021-2024 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/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("agent-1") + .build() + .build()} + +connected + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("agent-1") + .build() + .build()} + +read notify LIFECYCLE_OPEN + +write await TOOLS_LIST_COMPLETE + +write close + +connect await LIFECYCLE_OPEN + "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .toolsList() + .sessionId("agent-1") + .build() + .build()} + +connected + +write close + +read '{"tools":[]}' +read closed + +read notify TOOLS_LIST_COMPLETE diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/lifecycle.client.write.close/server.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/lifecycle.client.write.close/server.rpt new file mode 100644 index 0000000000..c5b996d011 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/lifecycle.client.write.close/server.rpt @@ -0,0 +1,65 @@ +# +# Copyright 2021-2024 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. +# + +property serverAddress "zilla://streams/app0" + +accept ${serverAddress} + option zilla:window 8192 + option zilla:transmission "half-duplex" + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("agent-1") + .build() + .build()} + +connected + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("agent-1") + .build() + .build()} +write flush + +read await TOOLS_LIST_COMPLETE + +read closed + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .toolsList() + .sessionId("agent-1") + .build() + .build()} + +connected + +write flush + +read closed + +write '{"tools":[]}' +write flush + +write close + +write notify TOOLS_LIST_COMPLETE diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/lifecycle.server.read.abort/client.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/lifecycle.server.read.abort/client.rpt new file mode 100644 index 0000000000..beb276996c --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/lifecycle.server.read.abort/client.rpt @@ -0,0 +1,62 @@ +# +# Copyright 2021-2024 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/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("agent-1") + .build() + .build()} + +connected + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("agent-1") + .build() + .build()} + +read notify LIFECYCLE_OPEN + +write await TOOLS_LIST_COMPLETE + +write abort + +connect await LIFECYCLE_OPEN + "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .toolsList() + .sessionId("agent-1") + .build() + .build()} + +connected + +write close + +read '{"tools":[]}' +read closed + +read notify TOOLS_LIST_COMPLETE diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/lifecycle.server.read.abort/server.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/lifecycle.server.read.abort/server.rpt new file mode 100644 index 0000000000..2fdb6aa927 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/lifecycle.server.read.abort/server.rpt @@ -0,0 +1,65 @@ +# +# Copyright 2021-2024 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. +# + +property serverAddress "zilla://streams/app0" + +accept ${serverAddress} + option zilla:window 8192 + option zilla:transmission "half-duplex" + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("agent-1") + .build() + .build()} + +connected + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("agent-1") + .build() + .build()} +write flush + +read await TOOLS_LIST_COMPLETE + +read aborted + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .toolsList() + .sessionId("agent-1") + .build() + .build()} + +connected + +write flush + +read closed + +write '{"tools":[]}' +write flush + +write close + +write notify TOOLS_LIST_COMPLETE diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/lifecycle.server.write.abort/client.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/lifecycle.server.write.abort/client.rpt new file mode 100644 index 0000000000..fed6415d49 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/lifecycle.server.write.abort/client.rpt @@ -0,0 +1,62 @@ +# +# Copyright 2021-2024 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/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("agent-1") + .build() + .build()} + +connected + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("agent-1") + .build() + .build()} + +read notify LIFECYCLE_OPEN + +read await TOOLS_LIST_COMPLETE + +read aborted + +connect await LIFECYCLE_OPEN + "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .toolsList() + .sessionId("agent-1") + .build() + .build()} + +connected + +write close + +read '{"tools":[]}' +read closed + +read notify TOOLS_LIST_COMPLETE diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/lifecycle.server.write.abort/server.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/lifecycle.server.write.abort/server.rpt new file mode 100644 index 0000000000..11e7d93d09 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/lifecycle.server.write.abort/server.rpt @@ -0,0 +1,65 @@ +# +# Copyright 2021-2024 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. +# + +property serverAddress "zilla://streams/app0" + +accept ${serverAddress} + option zilla:window 8192 + option zilla:transmission "half-duplex" + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("agent-1") + .build() + .build()} + +connected + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("agent-1") + .build() + .build()} +write flush + +write await TOOLS_LIST_COMPLETE + +write abort + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .toolsList() + .sessionId("agent-1") + .build() + .build()} + +connected + +write flush + +read closed + +write '{"tools":[]}' +write flush + +write close + +write notify TOOLS_LIST_COMPLETE diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/lifecycle.server.write.close/client.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/lifecycle.server.write.close/client.rpt new file mode 100644 index 0000000000..92be4e68a9 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/lifecycle.server.write.close/client.rpt @@ -0,0 +1,62 @@ +# +# Copyright 2021-2024 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/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("agent-1") + .build() + .build()} + +connected + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("agent-1") + .build() + .build()} + +read notify LIFECYCLE_OPEN + +read await TOOLS_LIST_COMPLETE + +read closed + +connect await LIFECYCLE_OPEN + "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .toolsList() + .sessionId("agent-1") + .build() + .build()} + +connected + +write close + +read '{"tools":[]}' +read closed + +read notify TOOLS_LIST_COMPLETE diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/lifecycle.server.write.close/server.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/lifecycle.server.write.close/server.rpt new file mode 100644 index 0000000000..99b1598da4 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/lifecycle.server.write.close/server.rpt @@ -0,0 +1,65 @@ +# +# Copyright 2021-2024 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. +# + +property serverAddress "zilla://streams/app0" + +accept ${serverAddress} + option zilla:window 8192 + option zilla:transmission "half-duplex" + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("agent-1") + .build() + .build()} + +connected + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("agent-1") + .build() + .build()} +write flush + +write await TOOLS_LIST_COMPLETE + +write close + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .toolsList() + .sessionId("agent-1") + .build() + .build()} + +connected + +write flush + +read closed + +write '{"tools":[]}' +write flush + +write close + +write notify TOOLS_LIST_COMPLETE diff --git a/specs/binding-mcp.spec/src/test/java/io/aklivity/zilla/specs/binding/mcp/streams/application/ApplicationIT.java b/specs/binding-mcp.spec/src/test/java/io/aklivity/zilla/specs/binding/mcp/streams/application/ApplicationIT.java index 21c7cb5552..9703900d16 100644 --- a/specs/binding-mcp.spec/src/test/java/io/aklivity/zilla/specs/binding/mcp/streams/application/ApplicationIT.java +++ b/specs/binding-mcp.spec/src/test/java/io/aklivity/zilla/specs/binding/mcp/streams/application/ApplicationIT.java @@ -549,6 +549,60 @@ public void shouldGetPromptWith100kMessageWithProgress() throws Exception k3po.finish(); } + @Test + @Specification({ + "${app}/lifecycle.server.write.abort/client", + "${app}/lifecycle.server.write.abort/server"}) + public void shouldLifecycleServerWriteAbort() throws Exception + { + k3po.finish(); + } + + @Test + @Specification({ + "${app}/lifecycle.server.write.close/client", + "${app}/lifecycle.server.write.close/server"}) + public void shouldLifecycleServerWriteClose() throws Exception + { + k3po.finish(); + } + + @Test + @Specification({ + "${app}/lifecycle.server.read.abort/client", + "${app}/lifecycle.server.read.abort/server"}) + public void shouldLifecycleServerReadAbort() throws Exception + { + k3po.finish(); + } + + @Test + @Specification({ + "${app}/lifecycle.client.write.abort/client", + "${app}/lifecycle.client.write.abort/server"}) + public void shouldLifecycleClientWriteAbort() throws Exception + { + k3po.finish(); + } + + @Test + @Specification({ + "${app}/lifecycle.client.write.close/client", + "${app}/lifecycle.client.write.close/server"}) + public void shouldLifecycleClientWriteClose() throws Exception + { + k3po.finish(); + } + + @Test + @Specification({ + "${app}/lifecycle.client.read.abort/client", + "${app}/lifecycle.client.read.abort/server"}) + public void shouldLifecycleClientReadAbort() throws Exception + { + k3po.finish(); + } + @Test @Specification({ "${app}/lifecycle.events.evict/client", diff --git a/specs/binding-mcp.spec/src/test/java/io/aklivity/zilla/specs/binding/mcp/streams/cache/ProxyCacheIT.java b/specs/binding-mcp.spec/src/test/java/io/aklivity/zilla/specs/binding/mcp/streams/cache/ProxyCacheIT.java new file mode 100644 index 0000000000..ad0321b313 --- /dev/null +++ b/specs/binding-mcp.spec/src/test/java/io/aklivity/zilla/specs/binding/mcp/streams/cache/ProxyCacheIT.java @@ -0,0 +1,209 @@ +/* + * Copyright 2021-2024 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. + */ +package io.aklivity.zilla.specs.binding.mcp.streams.cache; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.junit.rules.RuleChain.outerRule; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.DisableOnDebug; +import org.junit.rules.TestRule; +import org.junit.rules.Timeout; + +import io.aklivity.k3po.runtime.junit.annotation.Specification; +import io.aklivity.k3po.runtime.junit.rules.K3poRule; + +public class ProxyCacheIT +{ + private final K3poRule k3po = new K3poRule() + .addScriptRoot("app", "io/aklivity/zilla/specs/binding/mcp/streams/application"); + + private final TestRule timeout = new DisableOnDebug(new Timeout(5, SECONDS)); + + @Rule + public final TestRule chain = outerRule(k3po).around(timeout); + + @Test + @Specification({ + "${app}/cache.hydrate/client", + "${app}/cache.hydrate/server" }) + public void shouldHydrate() throws Exception + { + k3po.finish(); + } + + @Test + @Specification({ + "${app}/cache.hydrate.error/client", + "${app}/cache.hydrate.error/server" }) + public void shouldHydrateError() throws Exception + { + k3po.finish(); + } + + @Test + @Specification({ + "${app}/cache.hydrate.credentials/client", + "${app}/cache.hydrate.credentials/server" }) + public void shouldHydrateWithCredentials() throws Exception + { + k3po.finish(); + } + + @Test + @Specification({ + "${app}/cache.hydrate.toolkit/client", + "${app}/cache.hydrate.toolkit/server" }) + public void shouldHydrateToolkit() throws Exception + { + k3po.finish(); + } + + @Test + @Specification({ + "${app}/cache.serve.initialize/client", + "${app}/cache.serve.initialize/server" }) + public void shouldServeInitialize() throws Exception + { + k3po.finish(); + } + + @Test + @Specification({ + "${app}/cache.hydrate.lifecycle.reconnect/client", + "${app}/cache.hydrate.lifecycle.reconnect/server" }) + public void shouldReconnectAfterLifecycleAbort() throws Exception + { + k3po.finish(); + } + + @Test + @Specification({ + "${app}/cache.serve.tools.list/client", + "${app}/cache.serve.tools.list/server" }) + public void shouldServeToolsList() throws Exception + { + k3po.finish(); + } + + @Test + @Specification({ + "${app}/cache.refresh.tools/client", + "${app}/cache.refresh.tools/server" }) + public void shouldRefreshTools() throws Exception + { + k3po.finish(); + } + + @Test + @Specification({ + "${app}/cache.refresh.tools.error/client", + "${app}/cache.refresh.tools.error/server" }) + public void shouldRefreshToolsError() throws Exception + { + k3po.finish(); + } + + @Test + @Specification({ + "${app}/cache.refresh.tools.error.retry/client", + "${app}/cache.refresh.tools.error.retry/server" }) + public void shouldRetryAfterToolsRefreshError() throws Exception + { + k3po.finish(); + } + + @Test + @Specification({ + "${app}/cache.refresh.tools.contended/client", + "${app}/cache.refresh.tools.contended/server" }) + public void shouldRefreshToolsContended() throws Exception + { + k3po.finish(); + } + + @Test + @Specification({ + "${app}/cache.serve.resources.list/client", + "${app}/cache.serve.resources.list/server" }) + public void shouldServeResourcesList() throws Exception + { + k3po.finish(); + } + + @Test + @Specification({ + "${app}/cache.refresh.resources/client", + "${app}/cache.refresh.resources/server" }) + public void shouldRefreshResources() throws Exception + { + k3po.finish(); + } + + @Test + @Specification({ + "${app}/cache.serve.prompts.list/client", + "${app}/cache.serve.prompts.list/server" }) + public void shouldServePromptsList() throws Exception + { + k3po.finish(); + } + + @Test + @Specification({ + "${app}/cache.refresh.prompts/client", + "${app}/cache.refresh.prompts/server" }) + public void shouldRefreshPrompts() throws Exception + { + k3po.finish(); + } + + @Test + @Specification({ + "${app}/cache.hydrate.10k/client", + "${app}/cache.hydrate.10k/server" }) + public void shouldHydrate10k() throws Exception + { + k3po.finish(); + } + + @Test + @Specification({ + "${app}/cache.hydrate.100k/client", + "${app}/cache.hydrate.100k/server" }) + public void shouldHydrate100k() throws Exception + { + k3po.finish(); + } + + @Test + @Specification({ + "${app}/cache.serve.tools.list.10k/client", + "${app}/cache.serve.tools.list.10k/server" }) + public void shouldServeToolsList10k() throws Exception + { + k3po.finish(); + } + + @Test + @Specification({ + "${app}/cache.serve.tools.list.100k/client", + "${app}/cache.serve.tools.list.100k/server" }) + public void shouldServeToolsList100k() throws Exception + { + k3po.finish(); + } +} diff --git a/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/schema/store/test.schema.patch.json b/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/schema/store/test.schema.patch.json index 97fcf166b8..dcbf69d9a1 100644 --- a/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/schema/store/test.schema.patch.json +++ b/specs/engine.spec/src/main/scripts/io/aklivity/zilla/specs/engine/schema/store/test.schema.patch.json @@ -26,6 +26,22 @@ "type": { "const": "test" + }, + "options": + { + "properties": + { + "entries": + { + "title": "Initial entries", + "type": "object", + "additionalProperties": + { + "type": "string" + } + } + }, + "additionalProperties": false } } }