Skip to content

Commit 1eb6b70

Browse files
jfallowsclaude
andauthored
feat(binding-mcp): mcp · proxy cache option (#1737) (#1774)
* test(binding-mcp): scaffold mcp cache binding IT contract (#1737) Adds the test-first scaffold for the upcoming mcp cache binding: McpCacheIT skeleton with all 22 planned test methods (Group A warmup tests active, Groups B/C/D/F/G/I marked @Ignore until their scripts land), four IT zilla.yaml configs, schema patch entries for kind: cache and options.warmup/ttl, and six fully-written Group A warmup .rpt scripts (lifecycle, tools/list, resources/list, prompts/list, lifecycle-persists, guarded-credentials). https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8 * test(binding-mcp): add peer-to-peer CacheIT for warmup scripts (#1737) Adds the missing client.rpt for each Group A cache warmup scenario and a CacheIT class in the spec project that runs every script pair peer-to-peer without Zilla. Verifies the scripts are self-consistent before any cache binding implementation exists. Verified locally: ./mvnw -pl specs/binding-mcp.spec verify -Dit.test=CacheIT runs all 6 tests to green. https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8 * test(binding-mcp): split cache ITs by group and add Group B list scripts (#1737) Splits the monolithic CacheIT/McpCacheIT into per-group classes (*WarmupIT, *ListIT, etc.) to keep each group focused as the script set grows. Group A (warmup) renamed to *WarmupIT. Adds Group B (list operations served from cache) — 4 scenarios: agent initialize, tools/list, resources/list, prompts/list. Each scenario carries paired client.rpt (agent at app0) and server.rpt (cache facade) so CacheListIT can verify the agent↔cache contract peer-to-peer. McpCacheListIT pairs the agent client.rpt with the Group A warmup server.rpt at app1 so the cache is populated before the agent's list arrives. B5 (list-before-warmup) stays @Ignored with a script TODO. Verified locally: CacheWarmupIT 6/6 + CacheListIT 4/4 green peer-to-peer. https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8 * test(binding-mcp): add downstream-error warmup scenario, drop Group D (#1737) The cache binding is one hop above the proxy; proxy fanout across exits is already covered by McpProxyIT (shouldList*WithToolkitMulti). The only cache-specific concern in the original Group D was resilience to a downstream error during warmup, which is more naturally a Group A (warmup) scenario than a fanout one. Adds cache.warmup.session.downstream.error: downstream ABORTs the tools/list stream during warmup; the lifecycle session must survive so the cache can continue with other list types or retry later. CacheWarmupIT 7/7 green peer-to-peer. https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8 * test(binding-mcp): regroup cache ITs by MCP method kind (#1737) Renames/splits the CacheWarmupIT and CacheListIT pairs (peer-to-peer and engine-driven) into method-scoped ITs aligned with the issue's Responsibilities-by-MCP-method table: CacheLifecycleIT — warmup session open/persist/error/guard, agent initialize-from-cache CacheToolsListIT — tools/list warmup + served-from-cache CacheResourcesListIT — resources/list warmup + served-from-cache CachePromptsListIT — prompts/list warmup + served-from-cache Same applies to the McpCache* counterparts. No script files moved; only the IT-class references changed. All 11 peer-to-peer tests verified green. Remaining roster slots (CacheToolsCallIT/ResourcesReadIT/PromptsGetIT for pass-through invocations, and the per-list-method store/refresh coverage) will be added as their scripts come online. https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8 * test(binding-mcp): fold cache into mcp.proxy with options.cache (#1737) Replaces the separate kind: cache binding with options.cache on the existing kind: proxy binding. Since all original two-binding topologies have an exact equivalent in the folded model, no expressiveness is lost and the engine "kind: cache" prerequisite goes away. Also: - Renames "warmup" → "hydrate" throughout (more precise terminology for cache population). - Flattens the warmup wrapper: authorization/store/ttl now live directly under options.cache instead of options.cache.warmup. - Drops the guarded vs. unguarded test split — the .rpt scripts don't observe authorization on BEGIN so the scenarios produce identical transcripts. - Renames IT classes: CacheXIT → ProxyCacheXIT and McpCacheXIT → McpProxyCacheXIT to align with the existing McpProxyIT neighbour. - Configs rewritten: cache.yaml/cache.multi.yaml/cache.refresh.yaml → proxy.cache.yaml/proxy.cache.multi.yaml/proxy.cache.refresh.yaml; each declares kind: proxy with options.cache and a stores: memory0 reference. - Schema patch: drop "cache" from kind enum; replace flat options.warmup/options.ttl with options.cache {store, ttl, authorization}; store is required. Verified: 10/10 ProxyCache*IT peer-to-peer tests pass. https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8 * test(binding-mcp): flatten cache scenario + method names (#1737) Renames scenarios and test methods to use "hydrate" / "serve" as verbs rather than as noun-modifiers. Examples: cache.hydrate.session.tools.list → cache.hydrate.tools cache.agent.tools.list.from.cache → cache.serve.tools.list shouldPopulateToolsViaHydrate → shouldHydrateTools shouldServeAgentToolsListFromCache → shouldServeToolsList shouldKeepHydrateSessionOpenAfter... → shouldHydratePersist Verified: 10/10 ProxyCache*IT peer-to-peer tests pass. https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8 * test(binding-mcp): add cache refresh scenarios + tests (#1737) Adds per-method refresh scenarios that model the cache re-issuing a list call on the same hydrate session after a TTL elapses, plus a refresh-error case where the refresh attempt aborts and the cache must retain its prior cached entry. Scenarios: cache.refresh.tools / cache.refresh.resources / cache.refresh.prompts cache.refresh.tools.error Tests: ProxyCacheToolsListIT.shouldRefreshTools ProxyCacheToolsListIT.shouldRefreshToolsError ProxyCacheResourcesListIT.shouldRefreshResources ProxyCachePromptsListIT.shouldRefreshPrompts (engine-driven counterparts added too) Also renames cache.hydrate.downstream.error -> cache.hydrate.error (and shouldHydrateDownstreamError -> shouldHydrateError) for the single-qualifier convention. Lease-contention coverage (cache.refresh.tools.contended) is deferred: the lease behavior is store-level, only meaningfully testable engine-driven via either a TestStore seeding hook or a multi-worker EngineRule. Both are downstream work from the cache binding implementation; the wire-level refresh tests above cover the protocol shape. Verified: 14/14 ProxyCache*IT peer-to-peer tests pass. https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8 * test(binding-mcp): add hydrating scenarios per list method (#1737) Adds three scenarios where the agent's list request arrives while the cache is still hydrating. The cache facade holds the request read in full, then waits for a per-method hydrate barrier to fire before writing the cached response. True timing isn't observable peer-to-peer, but the scripts document the required wire ordering for engine-driven tests to enforce. Scenarios + tests: cache.serve.tools.list.hydrating shouldServeToolsListHydrating cache.serve.resources.list.hydrating shouldServeResourcesListHydrating cache.serve.prompts.list.hydrating shouldServePromptsListHydrating Verified: 17/17 ProxyCache*IT peer-to-peer tests pass. https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8 * test(engine): add entries option to test store for seeded state Adds TestStoreOptionsConfig with a Map<String,String> entries field, exposed via the standard options-config builder/adapter pattern, and wires it through TestStoreContext so each new TestStoreHandler is pre-populated with the configured entries. Enables tests to set up store state declaratively before a binding that uses the store begins operating — e.g., seeding a lease lock key so a binding observes a held lease and exercises its already-locked code path deterministically. Example: stores: memory0: type: test options: entries: tools.lock: "worker-0" Verified: engine unit tests (320/320), engine.spec ITs (39/39), and binding-mcp.spec ProxyCache*IT (17/17) all pass. https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8 * test(binding-mcp): scaffold cache refresh contention test (#1737) Adds proxy.cache.contended.yaml — a TestStore configured with tools.lock pre-seeded to a foreign worker id — and a corresponding McpProxyCacheToolsListIT.shouldRefreshToolsContended test method (currently @Ignore'd until the cache binding implementation lands). When enabled, the test verifies that the cache binding consults the lease before issuing a refresh tools/list: with the lock held in the store, putIfAbsent returns the seeded value and the refresh path is skipped. The downstream server.rpt models only the hydrate exchange, so any spurious refresh tools/list would hit an unmatched stream and fail the test. https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8 * test(binding-mcp): proper contention scripts with multi-worker engine (#1737) Replaces the TestStore-seeded contention approach with a more faithful multi-worker engine test: - cache.refresh.tools.contended/{client,server}.rpt — models two hydrate sessions, exactly two tools/list calls on the wire (one initial hydrate by lease-winner, one refresh by lease-winner). Second worker's lifecycle is observed but its tools/list never hits the wire because the lease was lost. - McpProxyCacheContentionIT — new engine-driven IT class configured with ENGINE_WORKERS=2 so both cache binding instances genuinely race for the hydrate / refresh leases against the shared store-memory. @Ignore'd until cache binding implementation lands. - ProxyCacheToolsListIT.shouldRefreshToolsContended — peer-to-peer counterpart added to the existing list IT. - proxy.cache.contended.yaml dropped — no longer needed (the test uses proxy.cache.refresh.yaml which already references store-memory). Verified: 18/18 ProxyCache*IT peer-to-peer tests pass (was 17, added shouldRefreshToolsContended). https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8 * test(binding-mcp): address review feedback on PR #1774 - proxy.cache.yaml / proxy.cache.refresh.yaml: use binding-level exit instead of single-element routes for the single-exit case. - proxy.cache.multi.yaml → proxy.cache.toolkit.yaml. - Remove explanatory # comments from scripts; scenarios + script bodies are the documentation. - Remove class-level comment from McpProxyCacheContentionIT; same reasoning. Verified: 18/18 ProxyCache*IT peer-to-peer tests pass. https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8 * test(binding-mcp): fix checkstyle import ordering in McpProxyCacheContentionIT The static import of ENGINE_WORKERS was placed after java/org statics with a blank-line separator. Per the project convention static imports go in a single block alphabetically sorted by full path, so io.aklivity.* comes first. https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8 * test(binding-mcp): @Ignore engine-driven cache ITs until impl lands The engine-driven McpProxyCache*IT tests fail at engine bring-up because the proxy binding does not yet recognize options.cache. This is the expected test-first state, but CI cannot distinguish "expected failure until impl" from "regression". Add class-level @Ignore("TODO: enable when proxy cache option lands") to all 5 McpProxyCache*IT classes: McpProxyCacheLifecycleIT, McpProxyCacheToolsListIT, McpProxyCacheResourcesListIT, McpProxyCachePromptsListIT, McpProxyCacheContentionIT. The peer-to-peer ProxyCache*IT tests in specs/binding-mcp.spec remain active and green — they verify script self-consistency independent of the binding implementation. Verified: runtime/binding-mcp clean verify → BUILD SUCCESS, 146 tests run, 5 skipped (the ignored cache ITs). https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8 * feat(binding-mcp): add McpCacheConfig POJO + JSON adapter (#1737) First step of the proxy cache implementation: add the config-layer types that map to options.cache in the schema. No runtime behavior yet — McpProxyFactory still ignores the parsed config. - McpCacheConfig: immutable POJO with store, per-method ttl, authorization map (guard-name → credentials) - McpCacheConfigBuilder: fluent builder mirroring the existing McpAuthorizationConfigBuilder pattern - McpOptionsConfig: add cache field + 4-arg constructor - McpOptionsConfigBuilder: add cache() method (nested-builder pattern matching authorization()) - McpOptionsConfigAdapter: serialize/deserialize the cache block with ttl + per-guard authorization credentials Verified: binding-mcp tests pass, checkstyle clean, binding-mcp.spec SchemaTest still green. https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8 * feat(binding-mcp): originate hydrate lifecycle session at bring-up (#1737) Phase B of the proxy cache implementation. When options.cache is present, attach() now: - resolves the unconditional exit route - allocates a HydrateSession (initialId/replyId etc.) - schedules a signaler tick that fires immediately - the tick handler issues a lifecycle BEGIN downstream with sessionId="hydrate-1", modelled after KafkaGrpcRemoteServerFactory The session sends reply WINDOW on receipt of the downstream's BEGIN reply, and END on detach. No list-method enumeration yet; tools/ resources/prompts hydrate, store integration, lease coordination, and refresh land in later phases. McpProxyCacheLifecycleIT remains @Ignore'd until all four scenarios in the class pass; verified locally that the shouldHydrate scenario itself now runs to green when temporarily un-Ignored. Also adds store-memory test dependency (the cache configs reference type: memory for their backing store). https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8 * feat(binding-mcp): hydrate per-method list enumeration (#1737) Phase C — after the hydrate lifecycle reply arrives, the cache now chains tools/list, resources/list, prompts/list as three sequential sub-streams on the same hydrate session. Each list stream sends BEGIN+END (close write), receives BEGIN reply, DATA and END, then signals the parent HydrateSession to start the next. Response bodies are discarded for now; store integration arrives in Phase D. McpProxyCacheLifecycleIT now passes all 4 tests with the engine (shouldHydrate, shouldHydratePersist, shouldHydrateError, shouldServeInitialize) — class-level @Ignore removed. McpProxyCache{ToolsList,ResourcesList,PromptsList,Contention}IT remain @Ignore'd until serve-from-cache / refresh / lease land. Verified: ./mvnw -pl runtime/binding-mcp clean verify → 149 pass, 4 ITs skipped (the still-Ignored cache list / contention ITs). https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8 * feat(binding-mcp): persist hydrate response bodies to store (#1737) Phase D — resolve the configured store at attach() time via context.supplyStore(resolveId.applyAsLong(cache.store)), thread the handle through HydrateSession into each HydrateListStream, and on each list reply END write the accumulated body to the store under the per-method key ("tools" / "resources" / "prompts") with no expiry (Long.MAX_VALUE). Response bodies are buffered byte-by-byte into a per-list-stream byte[] that grows as needed. On END the accumulated bytes are decoded as UTF-8 and pushed to the StoreHandler via put(). No serve-from-cache yet — agent list requests still pass through to the existing proxy code path. Phase E will intercept them and respond from the store. Verified: ./mvnw -pl runtime/binding-mcp clean verify → 149 pass, 4 list/contention ITs still @Ignore'd. https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8 * feat(binding-mcp): serve list responses from cache (#1737) Adds McpCacheListServer that intercepts tools/resources/prompts list streams when options.cache is configured on a proxy binding, looks up the cached envelope via StoreHandler.get(), and emits the cached bytes as DATA followed by END without forwarding to upstream. Un-ignores the hydrate + serve tests across the per-kind cache IT classes; periodic refresh and hydrating-wait tests remain ignored pending later phases. * refactor(binding-mcp): introduce McpListCache; hydrate kinds in parallel (#1737) Move the per-kind store-key plumbing out of HydrateSession into a new McpListCache attached to McpBindingConfig, dropping the redundant cacheStores map. HydrateSession becomes a populator only; the cache (backed by StoreHandler) is the source of truth for "is kind X ready?" via an async get. With per-kind status independent of session sequencing, the three list-stream round-trips dispatch on the same worker tick - wall-clock hydration drops to a single round-trip, and an error on one kind no longer delays the others or blocks a retry on reconnect. https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8 * refactor(binding-mcp): address PR #1774 review feedback - Rename `HydrateSession` to `McpHydrateSession` to follow the binding's type-prefixed inner class convention. - Make `McpOptionsConfig` constructor package-private; construction is via `McpOptionsConfig.builder()`. - Replace the hardcoded `"hydrate-1"` constant in `McpProxyFactory` with a `Supplier<String>` obtained from `McpConfiguration.sessionIdSupplier()`. Cache ITs configure `MCP_SESSION_ID_NAME` to a static method returning `"hydrate-1"`, mirroring the `McpServerIT` override pattern. - Replace the three flat `Duration ttlTools/ttlResources/ttlPrompts` fields on `McpCacheConfig` with a nested `McpCacheTtlConfig` (built via `McpCacheConfigBuilder.ttl()` returning `McpCacheTtlConfigBuilder`). `McpOptionsConfigAdapter` serializes and deserializes the nested form. https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8 * refactor(binding-mcp): hoist sessions and hydrate state onto McpBindingConfig Replace the two factory-level maps on `McpProxyFactory` (`sessions: Map<String, McpLifecycleServer>` shared across all bindings, and `hydrateSessions: Long2ObjectHashMap<McpHydrateSession>`) with per-binding fields on `McpBindingConfig`: - `sessions: Map<String, McpProxySession>` — now correctly scoped per binding rather than per worker. Session ids are only meaningful within one binding's namespace. - `hydrate: McpProxyHydrate` — the per-binding hydrate session. `McpProxySession` and `McpProxyHydrate` are package-visible interfaces declared in `internal.config` so `McpBindingConfig` can type the fields without depending on the inner classes inside `McpProxyFactory`. The inner classes implement the interfaces — no behavior change. `McpLifecycleServer` gains a constructor reference to its `McpBindingConfig` so `cleanup` can call `binding.sessions.remove(sessionId)`. This sets up the per-binding hook that the upcoming per-kind factory extraction will route state through. https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8 * refactor(binding-mcp): extract McpProxyLifecycleFactory + dispatch table Move McpLifecycleServer and McpLifecycleClient from McpProxyFactory into a new McpProxyLifecycleFactory as inner classes - same enclosing-instance pattern, just a smaller enclosing factory. McpLifecycleServer (and the cross-class accessors `sender`, `originId`, `routedId`, `sessionId`, `supplyClient`) is package-private so the still-inline call/list dispatch in McpProxyFactory can pattern-match against it. McpLifecycleClient is also package-private because McpClient and McpListClient hold typed references to it. Introduce `Int2ObjectHashMap<BindingHandler> factories` on McpProxyFactory and delegate KIND_LIFECYCLE to the new factory via the dispatch table; other kinds remain inline until subsequent phases extract them. Mirrors KafkaClientFactory's per-kind factory pattern. McpProxyFactory drops 560 lines. https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8 * refactor(binding-mcp): extract McpProxyItemFactory for tools/call, prompts/get, resources/read Introduce an abstract McpProxyItemFactory implementing BindingHandler plus three concrete per-kind subclasses (McpProxyToolsCallFactory, McpProxyPromptsGetFactory, McpProxyResourcesReadFactory). The base owns the shared stream state machine - the McpServer and McpClient inner classes move verbatim out of McpProxyFactory - and exposes three hooks for the kind-specific bits: protected abstract int kind(); protected abstract void injectInitialBeginEx(McpBeginExFW.Builder, String sid, String identifier); protected abstract void injectReplyBeginEx(McpBeginExFW.Builder, String sid, McpBeginExFW upstream); Each subclass implements ~30 lines: its KIND constant and the tools-call/prompts-get/resources-read variants of the BEGIN extension builder. Naming "Item" reflects that the three operations are different verbs (call/get/read) acting on a single identified MCP item - opposite to the List kinds. McpProxyFactory registers all three subclasses in its existing Int2ObjectHashMap<BindingHandler> factories map next to the Phase 2 McpProxyLifecycleFactory entry; the newStream dispatcher's else branch collapses to factories.get(kind).newStream(...). The local McpServer, McpClient, and rewriteReplyBeginEx are removed from McpProxyFactory along with three now-unused flyweight fields (flushRO, challengeRO, mcpChallengeExRW) and the McpChallengeExFW import. Net change: McpProxyFactory shrinks 735 lines (3071 -> 2336); McpProxyItemFactory adds 1229 lines. Each subclass receives the same (McpConfiguration, EngineContext, LongFunction<McpBindingConfig>) constructor signature as the Phase 2 lifecycle factory, and the per-factory do-helpers (doBegin, doData, doEnd, doAbort, doFlush, doChallenge, doReset, doWindow) are local copies rather than parent-shared - matches Phase 2 precedent. No visibility widening was needed beyond what Phase 2 already opened on McpLifecycleServer/McpLifecycleClient. https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8 * test(binding-mcp): add MCP_HYDRATE_KIND_FILTER to scope per-kind cache hydrate ITs The parallel-hydrate path in McpHydrateSession.onBegin iterates over [KIND_TOOLS_LIST, KIND_RESOURCES_LIST, KIND_PROMPTS_LIST] and dispatches all three list streams on the same worker tick. The per-kind cache ITs (McpProxyCache{Resources,Prompts}ListIT.shouldHydrate{Resources,Prompts}) each only `read` their own kind's BEGIN ext, so the other two streams emit unexpected BEGINs that fail the script assertion. Updating each script to accept three BEGINs would push parallel-hydrate setup into tests whose intent is the single-kind hydrate behavior in isolation. Add MCP_HYDRATE_KIND_FILTER (IntPredicate, default `k -> true`) to McpConfiguration, exposed as `hydrateKindFilter()`. Mirrors the existing MCP_SESSION_ID supplier pattern: tests resolve a `Class::method` static reference; the decoder loads it via MethodHandle. Production keeps the default — all three kinds hydrate in parallel — so the contention IT exercises the real behavior unchanged. Each per-kind IT now configures the filter to its single KIND_*_LIST so only that kind's BEGIN is emitted and the existing per-kind scripts pass unmodified. Result locally: 3 hydrate failures fixed (McpProxyCacheToolsListIT had timed out, also resolved). Three `shouldServe*` failures remain — cache-serve reads empty payload — and are independent of hydrate dispatch. https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8 * refactor(binding-mcp): extract McpProxyListFactory for tools/list, prompts/list, resources/list Mirror the Phase 3 item-factory shape for the list slice. Introduce an abstract McpProxyListFactory implementing BindingHandler plus three concrete per-kind subclasses (McpProxyToolsListFactory, McpProxyPromptsListFactory, McpProxyResourcesListFactory). The base owns the shared state machine - McpListClient, McpListClientDecoder with its ten decode states, McpListServer (passthrough), and McpCacheListServer (cache-serve variant), plus the indexOfByte JSON helper - and exposes seven hooks for the kind-specific bits: protected abstract int kind(); protected abstract void injectInitialBeginEx(McpBeginExFW.Builder, String sid); protected abstract void injectReplyBeginEx(McpBeginExFW.Builder, String sid); protected abstract DirectBuffer listReplyOpenPrelude(); protected abstract JsonParserFactory listItemParserFactory(); protected abstract String arrayKey(); protected abstract String idKey(); Each subclass owns its per-kind prelude bytes (the JSON envelope-open literal), JsonParserFactory for the streaming item parser, array key ("tools" / "prompts" / "resources"), and id key ("name" or "uri" for resources). The kind field is removed from McpListClient, McpListServer, and McpCacheListServer - each factory instance is bound to one kind, so the seven `switch (kind)` blocks in those classes collapse to direct hook calls. Cache-vs-passthrough is now dispatched internally in McpProxyListFactory.newStream: when binding.cache is non-null an McpCacheListServer is constructed, otherwise an McpListServer with its McpListClient via lifecycle.supplyClient. The McpProxyFactory factories map gets three new entries (KIND_TOOLS_LIST / KIND_PROMPTS_LIST / KIND_RESOURCES_LIST) and the newStream dispatcher collapses to factories.get(kind).newStream(...) for every dispatched kind, including lifecycle - the per-kind factories enforce their own session/route preconditions. HydrateListStream, McpHydrateSession, and SIGNAL_INITIATE_HYDRATE stay in McpProxyFactory because they coordinate multi-kind hydrate from one session and aren't request-time dispatch paths; the small kind-switch in HydrateListStream.initiate persists. McpProxyFactory shrinks 1777 lines (2343 -> 566), shedding all list machinery plus six now-unused do* helpers (doBegin, doData, doAbort, two doFlush overloads, doChallenge, doReset), unused flyweight RO/RW fields and imports, and the no-longer-called sessionId(McpBeginExFW) helper. doEnd, doWindow, and a local newStream remain because HydrateListStream still calls them. McpProxyListFactory adds 2016 lines. No further visibility widening was needed beyond what Phase 2 opened on the lifecycle accessors. ITs: 156 pass / 3 fail / 8 skipped, identical to the pre-refactor baseline. The three `shouldServe*` failures are a pre-existing cache-serve empty-payload bug unrelated to this refactor. https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8 * fix(binding-mcp): pre-seed cache for non-hydrating cache.serve ITs The three shouldServe*List runtime ITs raced against hydrate: the agent sent its lifecycle BEGIN, McpProxyLifecycleFactory replied immediately (it has no hydrate awareness), the agent then sent its list BEGIN, and McpCacheListServer hit an empty cache because hydrate was still in flight - the agent received an empty payload. Make these ITs deterministic by pre-seeding the cache instead of racing against hydrate. Mid-hydrate is a distinct scenario that lifecycle gating will cover later (the .hydrating scripts and @Ignore'd shouldServe*ListHydrating methods are removed here; when lifecycle gating lands we'll re-introduce a clearer cache.serve.<kind>.refreshing flavor rather than overload "hydrating"). Changes: - New config proxy.cache.seeded.yaml using TestStore with options.entries carrying the tools/resources/prompts JSON. The existing proxy.cache.yaml is unchanged - hydrate ITs continue to verify an empty memory store. - Runtime shouldServe{Tools,Resources,Prompts}List ITs switch to proxy.cache.seeded.yaml and reuse the existing cache.hydrate/server script for the downstream (it only handles the hydrate-1 lifecycle accept + reply, which is exactly what a fully-cached binding produces on the wire when McpHydrateSession.onBegin sees every cache.get returning non-null and spawns no list streams). - Delete the three .hydrating script directories and the @Ignore'd shouldServe*ListHydrating test methods (both runtime and peer ITs). Runtime: 156 pass / 0 fail / 5 skipped (was 156p / 3f / 8s). Peer: 15 pass / 0 fail / 0 skipped. https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8 * feat(binding-mcp): gate agent lifecycle BEGIN reply on hydrate complete When proxy.cache is configured, the agent's lifecycle BEGIN reply now waits until the hydrate session has settled every kind permitted by MCP_HYDRATE_KIND_FILTER. By the time an agent's lifecycle is "initialized" from its point of view, every list method served by this binding is in the cache - subsequent tools/list, resources/list, prompts/list requests get a synchronous cache hit and never race the hydrate path. Bindings with no cache configured retain the previous synchronous BEGIN-reply behavior (binding.hydrate == null branch). Mechanism uses the engine's existing signal infrastructure: - McpHydrateSession tracks totalKinds (computed from the filter) and settledKinds. Each kind settles either from cache.get returning non-null at hydrate startup (pre-seeded store) or from the matching HydrateListStream's terminal write (cache.put completion, bodyLen==0 skip-put, or abort/reset). Once settledKinds == totalKinds, the session flips the complete latch and fires signaler.signalNow against every pending agent stream registered via awaitComplete. - McpProxyHydrate gains awaitComplete(originId, routedId, streamId, traceId, signalId). When complete is already true the signal fires immediately (synchronous fast path for the pre-seeded case); otherwise the (streamId, signalId) pair is queued. cleanup discards pending before tearing the session down. - McpProxyLifecycleFactory.McpLifecycleServer.onServerBegin now transitions initial state and emits WINDOW synchronously, then delegates the BEGIN reply via binding.hydrate.awaitComplete using the agent's reply-id as the signal target (the engine registers the binding's stream consumer in throttles[replyId], so SignalFW for that stream id routes back to onServerMessage). A new SignalFW case dispatches to onServerSignal, which fires doDeferredServerBegin for SIGNAL_HYDRATE_COMPLETE. The deferred path guards on replyOpened/replyClosed in case the agent abandoned before complete fired, and rebuilds beginEx inside the deferred path so the shared codecBuffer is fresh at fire time. - HydrateListStream now holds a back-reference to its parent McpHydrateSession (passed via the constructor) and a settled flag to dedupe completion when both onEnd and abort/reset fire on the same stream. McpProxyCacheLifecycleIT.shouldServeInitialize switches to proxy.cache.seeded.yaml: an empty memory store would block forever because the hydrate session's three list streams find no cooperating downstream in cache.hydrate/server. With the pre-seeded test store the hydrate session sees every cache.get return non-null synchronously, the complete latch flips before the agent's lifecycle arrives, and the deferred BEGIN reply takes the synchronous fast path. Runtime: 156 pass / 0 fail / 5 skipped (was 156p / 3f-then-0f after gating with the wrong streamId). https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8 * feat(binding-mcp): periodic refresh per kind on TTL elapse McpHydrateSession now uses signaler.signalAt to re-issue list-kind hydrate streams when the per-kind TTL elapses. Scheduling fires: - After each kind's initial hydrate completes (HydrateListStream reaches its terminal: cache.put completion, bodyLen==0 skip-put, or abort/reset — same gating dedupe applies, so each instance schedules at most one next refresh). - After each cache.get returns non-null at startup (pre-seeded value still refreshes on TTL, so the value stays fresh). - After each refresh stream's terminal (recursive, same code path). Each kind has its own signal id (SIGNAL_REFRESH_{TOOLS,RESOURCES, PROMPTS}) and TTL (McpCacheTtlConfig.tools / .resources / .prompts). When ttlForKind returns null (no cache.ttl block, or the block omits that kind), no refresh is scheduled - the cache value stays forever. On abort/reset of an in-flight refresh, the cache.put never fires, so the prior cached value is preserved (the test that validates this behaviour is now un-ignored as shouldRefreshToolsError). McpHydrateSession.settle takes the kind explicitly and both contributes to the initial-hydrate gating latch and schedules the next refresh. HydrateListStream forwards its kind into parent.settle(kind) - no behavioural change to the gating dedupe. McpProxyCache{Tools,Resources,Prompts}ListIT.shouldRefresh* and shouldRefreshToolsError no longer @Ignore - now run against proxy.cache.refresh.yaml (ttl tools=PT1S, resources=PT2S, prompts=PT3S) via the existing cache.refresh.<kind> / cache.refresh.<kind>.error spec scripts. Verified: 156 pass / 0 fail / 1 skipped (Contention IT class-level @Ignore for lease coordination remains). https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8 * feat(binding-mcp): cache hydrate session emits authorized BEGINs to downstream When options.cache.authorization is configured, McpProxyFactory.attach resolves the single configured guard via context.supplyGuard and calls guard.reauthorize(traceId, binding.id, 0L, credentials) once to mint a session token. The McpHydrateSession and HydrateListStream now stamp that token onto every outbound BEGIN/END/WINDOW in place of the prior hardcoded 0L authorization, so any downstream binding that runs a guard recognises the cache hydrate session as an authenticated principal. The map is constrained to a single guard entry by the schema patch (maxProperties: 1 under cache.authorization); the runtime picks that entry via iterator without defensive assertion, since invalid yaml is rejected at config-load time. Token freshness is left to the guard for now - the binding does not re-reauthorize per BEGIN. If a future guard implementation needs short-lived tokens, the call site is a single line and can move to per-stream resolution without changing the API shape. New test scaffolding: - specs/.../config/proxy.cache.auth.yaml - engine test_guard + test store pre-seeded with empty list payloads (so hydrate spawns no list streams; the authorization assertion lands on the lifecycle BEGIN alone) + proxy declaring cache.authorization.test_guard with matching credentials. - specs/.../streams/application/cache.hydrate.auth/{client,server}.rpt mirrors cache.hydrate/ but adds option zilla:authorization 1L (the TestGuard's first reauthorize result) to assert the proxy stamps the expected long on its hydrate BEGIN. - shouldHydrateAuth peer IT in ProxyCacheLifecycleIT and engine-driven IT in McpProxyCacheLifecycleIT. Verified: spec ProxyCache*IT 16 pass / 0 fail; runtime binding-mcp 157 pass / 0 fail / 1 skipped (was 156p/0f/1s; +1 shouldHydrateAuth). McpProxyCacheContentionIT remains the only @Ignore'd test pending lease coordination. https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8 * feat(binding-mcp): putIfAbsent lease coordination for hydrate + refresh When two workers attach the same proxy-cache binding, both would otherwise race to issue every list call on the wire. Coordinate via the configured store's putIfAbsent so exactly one worker issues each call. Two leases, one shared key namespace on the configured store: - Binding-wide "lifecycle.lock": acquired before opening the hydrate lifecycle stream to downstream. Loser polls every 100ms until the lock frees; winner holds it through the initial hydrate. Released inside markComplete once every kind permitted by the filter has settled, so other workers can then open their own lifecycle (independent stream onward) and observe the now-populated cache. - Per-kind "<kind>.lock": acquired before issuing a HydrateListStream (both initial and refresh paths). Loser at initial-hydrate time markSettled-s without scheduling a refresh - the lease holder owns refresh scheduling because settle (gated by the per-stream settled flag) is the only path that calls scheduleRefresh. Released after cache.put completes (or after onEnd's skip-put branch, or after an abort/reset terminal) and before parent.settle fires. settle is split into markSettled (gating-latch only) and settle (markSettled + scheduleRefresh). cache hits at startup, lease losses at initial hydrate, and lease losses at refresh-time all funnel through markSettled so they contribute to lifecycle gating without arming a refresh signal on a worker that didn't actually do the work. McpProxyCacheContentionIT (was class-level @Ignore'd) is now enabled, configured with MCP_HYDRATE_KIND_FILTER restricting to KIND_TOOLS_LIST (since cache.refresh.tools.contended/server.rpt only models the tools exchanges; resources and prompts are not in scope for this scenario). hydrateSessionId now cycles "hydrate-A"/"hydrate-B" per call so the two workers' attach-time calls match the script's session ids. Lease TTLs: - lifecycle: 30s (safety net for crashes; explicit release covers the happy path). - per-kind: 30s ditto. - Retry polling for lifecycle: 100ms. Verified: 157 pass / 0 fail / 0 skipped. Every test that was @Ignore'd at the start of the PR is now active and green. https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8 * refactor(binding-mcp): rename HydrateListStream to McpHydrateListStream Type-prefixed inner class naming convention matches the rest of the file (McpServer, McpClient, McpListServer, McpCacheListServer, McpLifecycleServer, McpLifecycleClient, McpHydrateSession). https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8 * test(engine): share TestStore entries across workers via per-Store map TestStoreHandler previously held its own HashMap<String, String> per-handler, so workers running in the same engine each saw an isolated copy of the configured entries (putIfAbsent from one worker was invisible to another). Lease coordination in binding-mcp's cache hydrate path relies on cross-worker putIfAbsent visibility, so move the entries map up one level: TestStore now owns a ConcurrentMap<Long, ConcurrentMap<String, String>> keyed by storeId, and TestStoreContext.attach hands out a reference to the per-storeId shared map to each worker's TestStoreHandler. Seeding via options.entries uses putIfAbsent so the first worker's attach is the one that loads the map; subsequent attaches see the existing entries and skip. No TTL behaviour was added — production binding code releases leases explicitly, and the existing skip-TTL semantics is sufficient for the cache test scenarios. binding-mcp switches its three remaining memory-store configs (proxy.cache.yaml, proxy.cache.refresh.yaml, proxy.cache.toolkit.yaml) to type: test, matching the dependency hygiene rule that implementations must not depend on other implementations - the cache ITs now run entirely against the engine's test-jar store. The store-memory test dependency drops out of runtime/binding-mcp/pom.xml. Verified: 157 pass / 0 fail / 0 skipped (all cache ITs, McpServerIT, McpClientIT, McpProxyIT, McpProxyCacheContentionIT) and the engine spec IT suite (39 / 0 / 0) — no regression in either project from the TestStore sharing change. https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8 * refactor(binding-mcp): extract McpCacheHydrater from McpProxyFactory McpProxyFactory had grown to ~566 lines absorbing the proxy-cache feature; the actual dispatch (parse BEGIN ext, look up by kind, delegate to a per-kind factory) was a small slice of that. The rest - McpHydrateSession, McpHydrateListStream, the SIGNAL_INITIATE_HYDRATE / SIGNAL_REFRESH_* / LEASE_TTL_MS / LEASE_RETRY_MS constants, the PendingAwait record, the local newStream/doEnd/doWindow helpers, the cache attach wiring (store resolve, McpListCache instantiate, cacheGuard reauthorize, McpHydrateSession construct, schedule SIGNAL_INITIATE_HYDRATE), and the matching detach cleanup - is now owned by a new sibling McpCacheHydrater. McpCacheHydrater exposes attach(McpBindingConfig) and detach(McpBindingConfig) mirroring the BindingHandler lifecycle the engine drives. McpProxyFactory constructs one in its ctor and delegates both calls. McpProxyFactory drops from 795 lines to 136 (a pure dispatcher now): keeps the factories map and its seven per-kind registrations, the bindings map, supplyGuard for McpBindingConfig construction, mcpBeginExRO/beginRO for parsing the inbound BEGIN to extract the kind, and the new hydrater field. McpCacheHydrater is 724 lines holding everything cache-hydrate: signal ids, lease ttls/retry, PendingAwait record, McpHydrateSession (still implementing McpProxyHydrate so McpProxyLifecycleFactory can defer the agent BEGIN reply via awaitComplete), McpHydrateListStream (settle / cache.put / lease release path unchanged), and per-worker flyweights duplicated for the hydrate stream-write helpers. The hydrate session keeps its own codecBuffer (separate UnsafeBuffer allocated against writeBuffer.capacity), matching the per-handler flyweight pattern. McpBindingConfig gains one accessor: long resolveId(String name) plus the corresponding private ToLongFunction<String> field initialized from binding.resolveId. Needed because McpCacheHydrater.attach receives only the wrapped McpBindingConfig and must resolve the cache store name to a binding id at attach time. Cleaner than passing the raw BindingConfig alongside. Verified: 158 pass / 0 fail / 0 skipped (identical to pre-refactor baseline). Checkstyle 0 violations. JaCoCo coverage met (bundle grew 116 -> 117 classes for the new McpCacheHydrater). https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8 * refactor(binding-mcp): per-kind cache hydrater hierarchy Address PR #1774 review comments: - #1 cache instantiation moves into McpBindingConfig ctor (supplyStore becomes a third constructor parameter, parallel to supplyGuard). - #2 PendingAwait -> McpSignalHandle as a standalone package-private record with signalVia(Signaler) convenience method; lives in its own file in the stream package. - #3 acquire-lifecycle-lease callback is a named method reference rather than an inline lambda; traceId is re-supplied inside the method body via supplyTraceId.getAsLong() instead of captured. - #4 doLifecycleBegin guard inverted to wrap the body, single return per method. - #5 separate signal handlers per kind; initial population is the first refresh. The binding-level orchestrator (McpProxyCacheHydrater) owns the lifecycle stream and tracks readiness via a populated counter rather than a totalKinds/settledKinds pair on a McpHydrateSession. Per-kind work moves into McpProxyCacheListHydrater (abstract base) with three concrete subclasses (McpProxyCacheToolsListHydrater / ResourcesListHydrater / PromptsListHydrater), mirroring the McpProxyListFactory hierarchy. - #6 cleanup's inline doEnd / state update is consolidated into a standard doInitialEnd helper on McpProxyCacheHydrater. Kind and signal id are passed via the abstract base's constructor as protected final fields rather than abstract methods - only the truly kind-specific behaviour (injectInitialBeginEx and ttl()) remains abstract. Each concrete subclass collapses to ~15 lines. McpProxyHydrate interface and the old McpCacheHydrater are deleted. McpProxyLifecycleFactory now calls binding.hydrater.register(handle) directly with an McpSignalHandle instance instead of going through an interface. McpListCache stays at its current public surface (get / put / acquireLease / releaseLease / acquireLifecycleLease / releaseLifecycleLease) - the readiness tracking lives on the orchestrator because it already knows the filtered kind set from hydrateKindFilter at attach time, so no expecting(kind) plumbing was needed. Files: - McpProxyHydrate.java deleted - McpCacheHydrater.java deleted (replaced) - McpProxyCacheHydrater.java new - per-binding orchestrator - McpProxyCacheListHydrater.java new - abstract base + 3 subclasses - McpSignalHandle.java new - McpBindingConfig.java - supplyStore ctor param, cache field initialised in ctor, hydrate field renamed to hydrater and retyped - McpProxyFactory.java - per-binding hydrater instantiation in attach - McpProxyLifecycleFactory.java - binding.hydrater.register instead of binding.hydrate.awaitComplete - McpClientFactory.java - 1-line ctor signature update for the now 3-arg McpBindingConfig ctor Verified: 158 pass / 0 fail / 0 skipped, identical to baseline. Checkstyle 0 violations. https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8 * refactor(binding-mcp): split per-kind subclasses into top-level files Address PR #1774 review comments asking that each concrete per-kind subclass living as a package-private sibling next to its abstract base be moved to its own top-level file. Three files affected, nine new files created: McpProxyItemFactory.java (1229 -> 1124) loses three subclasses: - McpProxyToolsCallFactory.java - McpProxyPromptsGetFactory.java - McpProxyResourcesReadFactory.java McpProxyListFactory.java (2016 -> 1819) loses three subclasses: - McpProxyToolsListFactory.java - McpProxyPromptsListFactory.java - McpProxyResourcesListFactory.java McpProxyCacheListHydrater.java (329 -> 257) loses three subclasses: - McpProxyCacheToolsListHydrater.java - McpProxyCacheResourcesListHydrater.java - McpProxyCachePromptsListHydrater.java Each new file has the standard Aklivity Community License header, a minimal import set (alphabetical within groups), and contains only the final class declaration extending the base. Base files trim imports that were only used by the lifted subclasses (Map and StreamingJson out of McpProxyListFactory; KIND_TOOLS_LIST / KIND_RESOURCES_LIST / KIND_PROMPTS_LIST static imports out of McpProxyCacheListHydrater). No visibility widening was needed - subclasses already touched only protected abstract hooks on the bases or package-private fields on parent objects. Verified: 158 pass / 0 fail / 0 skipped. Checkstyle 0 violations. https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8 * refactor(binding-mcp): collapse options.cache.ttl to a single Duration Drop McpCacheTtlConfig and its three per-kind fields (tools, resources, prompts) in favour of a single Duration on McpCacheConfig. No real-world signal that the per-kind knobs are needed; can be reintroduced additively if/when a use case appears. Default lives in the JSON schema (PT5M), with the adapter mirroring it as fallback when ttl is omitted. * fix(binding-mcp): reschedule cache refresh when lease lost onRefreshAcquireLeaseComplete previously only acted on the lease winner; losers stopped refreshing forever, leaving the cluster dependent on a single worker's timer. Mirror the initial-acquire path: losers reschedule their refresh so every worker stays armed and re-attempts on the next tick. * refactor(binding-mcp): pass EngineContext to McpBindingConfig Replace the (BindingConfig, LongFunction<GuardHandler>, LongFunction<StoreHandler>) ctor and its 1-arg sugar with a single (BindingConfig, EngineContext) ctor. McpBindingConfig pulls supplyGuard and supplyStore from the context directly, and the null-guards on the suppliers disappear because every caller now passes a real context. The supplier fields on McpProxyFactory and McpClientFactory go with it - they only existed to be passed through. Bind options.authorization and options.cache to locals so the long null-chains collapse. * refactor(binding-mcp): address PR #1774 review batch (phases A–M) - Rename MCP_HYDRATE_KIND_FILTER → MCP_HYDRATE_FILTER; expose lease ttl/retry as MCP_LEASE_TTL_MS / MCP_LEASE_RETRY_MS configuration properties (phases A, B) - Split McpListCache into per-kind instances; introduce McpLifecycleCache for lifecycle distributed lock; drop kind parameter from cache ops (phase C) - McpBindingConfig owns per-kind caches (toolsCache, resourcesCache, promptsCache, lifecycleCache) and the hydrater; Optional chains for guard/store resolution; McpProxyFactory attach/detach simplified (phases D, G) - Extract McpHydrateLifecycleStream inner class; guard-then-route ordering; complete End/Abort/Reset state gating; all fields final (phase E) - Decouple McpProxyCacheListHydrater from parent reference; pass EngineContext + LongSupplier supplyAuthorization directly; rename onMessage → onListHydrateMessage (phase F) - McpProxyLifecycleFactory: rename doDeferredServerBegin → doServerBeginDeferred; invert early-return guard (phase H) - McpProxyItemFactory: rename builder parameter b → builder in abstract signatures; update subclasses (phase I) - McpProxyListFactory: replace abstract kind() with final int kind field; make sessionId() abstract; move JsonParserFactory construction to base class constructor accepting List<String> pathIncludes; remove listItemParserFactory() abstract method (phases J, K) - IT helpers: extract sessionId() and hydrateXxxOnly() static helpers; update configure() calls to method-reference form (phase L) - proxy.cache.seeded.yaml: convert embedded JSON to YAML block scalars (phase M) https://claude.ai/code/session_01WNsipAt3RGwQoeFYVxwfL8 * refactor(binding-mcp): address PR #1774 review batch - Rename MCP_LEASE_TTL_MS/RETRY_MS to MCP_LEASE_TTL/RETRY as Duration properties with ISO-8601 defaults (PT30S, PT0.1S) - Extract list hydrate stream into inner class McpListHydrateStream with ExpandableArrayBuffer body accumulator - Add abstract signalId() override on each list hydrater subclass instead of passing signalId as constructor parameter - Replace String prefix with String8FW in McpRoutePrefix to eliminate per-stream byte allocation in list factory - Remove kind() abstract method from item/list factories; pass kind as constructor parameter and store as final field - Move sessions map initialization into McpBindingConfig constructor https://github.com/aklivity/zilla/pull/1774 * feat(engine): add Signaler.signalAt(Instant) overloads Provides Instant-accepting default methods that delegate to the existing long-based overloads via toEpochMilli(). Required by binding-mcp cache hydrater for cleaner lease retry scheduling. * refactor(binding-mcp): address PR #1774 review batch - McpListCache.acquire and McpLifecycleCache.acquireLifecycle now take Duration instead of long milliseconds; conversion happens internally - Drop "Lease" from on*AcquireComplete callback names on list hydrater - Move cacheGuard and cacheCredentials onto McpLifecycleCache as guard/credentials fields; McpBindingConfig no longer exposes them - Split lifecycle and list hydrate stream constructors from their behavioral work; creators explicitly call doListHydrateBegin/End and doLifecycleBegin post-construction - Add proper onListHydrateAbort/Reset handlers with do* helpers and McpState.initialClosed/replyClosed gating - Use Signaler.signalAt(Instant) overloads with Instant.now().plus(...) for lease retry and cache refresh scheduling https://github.com/aklivity/zilla/pull/1774 * test(engine): cover Signaler.signalAt(Instant) default methods Default methods in interfaces emit bytecode in the interface class, so adding them shifts Signaler from "no instructions" to "uncovered class" in JaCoCo's view. Cover both overloads via a stub Signaler that captures the delegated timeMillis to keep the engine's missed-class count under its threshold. * refactor(binding-mcp): reuse McpAuthorizationConfig under cache McpCacheConfig.authorization changes from Map<String, String> to McpAuthorizationConfig with an optional credentials field. Schema is unchanged (options.cache.authorization remains a single-entry guard-name map at the YAML level); the adapter now flattens the single map entry into the McpAuthorizationConfig fields on read and back to a single entry on write. The Map.Entry/Iterator dance in McpBindingConfig drops out — the cache guard and credentials read directly off the McpAuthorizationConfig. Note: at runtime there is no overlap between options.authorization (server/client kinds) and options.cache.authorization (proxy kind), so the type reuse is purely structural — credentials remains unused at the top-level site for now. A future change to the mcp client binding may use the top-level credentials to lay out a Bearer authorization header. https://github.com/aklivity/zilla/pull/1774 * refactor(binding-mcp): self-target binding for multi-route hydrate The hydrater previously sent its lifecycle and per-kind list streams directly to a single resolved route, so the cache only captured one route's data on multi-route bindings. Now the hydrater self-targets binding.id, letting the binding's own dispatch fan out across all routes via McpProxyListFactory's McpListServer aggregation path — the same code path that serves uncached agent list calls. - McpProxyCacheHydrater: routedId = binding.id (self-loop); drop pre-resolution and the `enabled` field; lifecycle acquired unconditionally on start. - McpProxyListFactory.newStream: detect self-loop (originId == routedId) and skip the cache server, falling through to McpListServer aggregation so the hydrater observes the merged result rather than serving from its own empty cache. - McpProxyLifecycleFactory.onServerBegin: when self-loop, bypass the hydrate-complete gating that otherwise defers the lifecycle reply BEGIN — the hydrater's own lifecycle would deadlock waiting for itself. The 8 spec-driven cache ITs that model the old direct-to-app1 wire shape are temporarily @Ignored — the new lazy-via-supplyClient wire shape requires re-authoring the corresponding `app1`-side server.rpt scripts, follow-up commit. https://github.com/aklivity/zilla/pull/1774 * refactor(binding-mcp): mechanical review cleanups - McpBindingConfig: keep cacheAuth as Optional<McpAuthorizationConfig> and derive cacheGuard/cacheCredentials via map chains, instead of unwrapping early. - McpProxyCacheListHydrater.onListHydrate{Abort,Reset}: drop the state-already-closed guards before McpState.closedReply/closedInitial setters — the bitmask is idempotent and the do{Abort,Reset} helpers carry their own closed-state guards. - McpProxyCacheListHydrater.onListHydrateEnd: use bodyBuffer.getStringWithoutLengthUtf8 instead of allocating a new String from the byte array. https://github.com/aklivity/zilla/pull/1774 * refactor(binding-mcp): defer list hydrate END to initial window doListHydrateBegin now marks the initial stream as closingInitial after sending BEGIN. The reactive doListHydrateEnd happens on receipt of the initial WINDOW frame — when initialClosing && !initialClosed, send END. This avoids the proactive END from the call site of the constructor and follows the standard close-on-window pattern. https://github.com/aklivity/zilla/pull/1774 * refactor(engine): remove default modifier on Signaler.signalAt(Instant) Per review feedback, the Signaler.signalAt(Instant, ...) overloads are now abstract — implementations provide them directly rather than inheriting a default that delegates via toEpochMilli(). - EngineWorker.EngineSignaler: convert via toEpochMilli at the entry point, delegating to the long-based variant. - KafkaClientConnectionPool.KafkaClientSignaler: delegate the simple variant to its inner Signaler; convert the stream variant via toEpochMilli. - TlsWorker.TlsSignaler (test bench): same toEpochMilli conversion. - Drop SignalerTest — it existed only to keep JaCoCo's missed-class count under threshold for the default-method bytecode; with abstract methods Signaler has no executable code to cover. https://github.com/aklivity/zilla/pull/1774 * test(binding-mcp): re-enable seeded-mode cache serve ITs With the self-target hydrate, a seeded cache no longer fans out to the downstream — no app1 traffic at hydrate time. Drop the \`cache.hydrate/server\` spec from the four seeded-mode tests (shouldServe{Initialize,ToolsList,ResourcesList,PromptsList}) and keep only the agent-side client spec. The script root override (@ScriptProperty serverAddress=app1) drops out alongside since the remaining client spec targets app0 by default. https://github.com/aklivity/zilla/pull/1774 * test(binding-mcp): drop redundant shouldHydrate IT In the new self-target hydrate model, shouldHydratePersist already exercises the full hydrate flow (lifecycle + all three list kinds), making shouldHydrate (which only modelled lifecycle direct to the downstream) redundant. Three cache hydrate ITs remain @Ignored — shouldHydrateError, shouldHydrateAuth (McpProxyCacheLifecycleIT), and shouldRefreshToolsContended (McpProxyCacheContentionIT) — each needs its app1-side spec script re-authored against the new lazy fan-out wire shape. https://github.com/aklivity/zilla/pull/1774 * test(binding-mcp): rename shouldHydratePersist to shouldHydrate The original shouldHydrate scenario only handled the lifecycle BEGIN handshake at app1, leaving the per-kind list streams that the proxy opens unmatched — the script was structurally incomplete and only passed in the legacy direct-route hydrate model because the proxy wasn't in the path to surface the unhandled streams. shouldHydratePersist already covered the substantive contract: one hydrate lifecycle is opened and reused across all three per-kind list hydrate streams (asserted structurally by the single 'accepted' block for lifecycle vs three for the list kinds). Promote shouldHydratePersist to shouldHydrate by replacing the cache.hydrate/{client,server}.rpt scripts with the persist versions, removing the now-redundant cache.hydrate.persist directory, and renaming the test methods in McpProxyCacheLifecycleIT and the peer-to-peer ProxyCacheLifecycleIT. https://github.com/aklivity/zilla/pull/1774 * test(binding-mcp): re-author cache hydrate scenarios for self-target Update spec scripts to match the wire shape produced by the proxy's multi-route hydrate via its own dispatch. All three list-kind streams arrive at app1 (lifecycle plus tools/list, resources/list, prompts/list), not just one — the previously incomplete scripts only happened to pass because the legacy direct-route hydrate did not surface them through McpListServer at app0. - cache.hydrate.error: extend server.rpt and client.rpt to handle all three list kinds; tools aborts, resources and prompts succeed so the remaining caches still populate. - cache.refresh.tools.contended: simplify to one lifecycle plus two tools/list streams (initial + refresh); the per-kind lease ensures only one worker's call reaches app1 per round. - Rename proxy.cache.auth.yaml to proxy.cache.credentials.yaml and cache.hydrate.auth/ scripts to cache.hydrate.credentials/; rename shouldHydrateAuth to shouldHydrateWithCredentials in both ITs. All 157 engine-driven binding-mcp ITs and the spec peer-to-peer ITs pass; the three previously @Ignored scenarios are now active. https://github.com/aklivity/zilla/pull/1774 * test(binding-mcp): drop redundant per-kind hydrate scenarios cache.hydrate.tools / cache.hydrate.resources / cache.hydrate.prompts were subsets of cache.hydrate (multi-kind): each only asserted "lifecycle BEGIN + one kind's list BEGIN", which the multi-kind scenario already verifies. The lifecycle-reuse property is structural (McpLifecycleClient.supplyClient with computeIfAbsent) and is exercised by the multi-kind scenario. Drop the three per-kind hydrate script directories and the shouldHydrateTools / shouldHydrateResources / shouldHydratePrompts test methods from both the engine-driven and peer-to-peer ITs. The per-kind ITs keep their shouldServe* and shouldRefresh* methods, which still benefit from MCP_HYDRATE_FILTER scoping the refresh path to one kind. https://github.com/aklivity/zilla/pull/1774 * test(binding-mcp): cache hydrate across multiple toolkit routes Add shouldHydrateToolkit on McpProxyCacheLifecycleIT — runs against proxy.cache.toolkit.yaml (two routes: bluesky → app1, quartz → app2) and verifies the hydrater fans out to both downstream routes via the binding's own McpListServer aggregation, with each route returning its own data and McpListServer merging into a single cached value. cache.hydrate.toolkit/server.rpt has eight accept blocks — one lifecycle plus three list streams per downstream route — capturing the full wire shape that distinguishes multi-route from single-route hydrate. The previously-orphan proxy.cache.toolkit.yaml is now referenced by a test. No peer-to-peer test pairs with this — k3po can't route a single client connection to app0 against accepts at app1 and app2 without the proxy in the middle, the same constraint that already excludes tools.list.toolkit.multi from ApplicationIT. https://github.com/aklivity/zilla/pull/1774 * test(binding-mcp): pair-up cache.hydrate.toolkit peer-to-peer Add client.rpt for cache.hydrate.toolkit and the corresponding shouldHydrateToolkit test in ProxyCacheLifecycleIT. The client connects directly to app1 and app2 (rather than via app0), sequenced with read notify / connect await barriers so the script structurally mirrors the server.rpt's accepts at app1 and app2 — independent of any proxy in the middle. https://github.com/aklivity/zilla/pull/1774 * refactor(binding-mcp): consolidate cache state in McpCacheContext Merge McpLifecycleCache and McpListCache into a single per-binding McpCacheContext that owns the store handle, guard, credentials, lease ttl/retry, cache ttl, and mutable session/authorization state. The former McpListCache is now an inner class of McpCacheContext exposed per-kind via tools(), resources(), prompts() accessors. Inline McpProxyCacheListHydrater plus its three kind-specific subclasses as inner classes of McpProxyCacheHydrater so they share the outer flyweights and reach session/authorization directly through the enclosing context, dropping the LongSupplier / Supplier<String> indirections. Positions the hydrater for a future per-worker shared instance that attaches/detaches contexts. * refactor(binding-mcp): share McpProxyCacheHydrater across bindings McpProxyCacheHydrater becomes a per-worker singleton owned by McpProxyFactory, with attach(McpCacheContext) and detach(McpCacheContext) methods that bind/unbind per-binding state. The three per-kind hydraters collapse to three stateless strategy fields on the outer; all state-machine methods take an McpCacheContext parameter and thread it through. McpCacheContext moves into internal.stream and absorbs the session state (awaiters, populated, expected, complete, sessionId, authorization) plus the bare-method-ref signal targets (onInitiateLifecycle, onRefresh, register). Active stream handlers register opaque Runnable cleanup hooks so detach can end them without the context importing inner stream types. McpBindingConfig drops the per-binding hydrater field; the lifecycle factory now registers awaiters via binding.cacheContext.register. * refactor(binding-mcp): drop unused McpCacheContext.listCache(kind) The kind-keyed dispatcher is unreachable now that per-kind strategies call tools() / resources() / prompts() directly via cacheOf(context). * refactor(binding-mcp): drop unused per-kind cleanup hooks Only the lifecycle stream needs explicit ending on detach — it is the long-lived "I'm subscribed" stream with no self-termination trigger. List streams self-terminate within a single round trip (closingInitial is set in doListHydrateBegin so we end as soon as the server's WINDOW arrives), and late callbacks on a detached context are already inert because scheduleRefresh's onRefresh signal checks detached, markReady bumps a counter against an empty awaiters list, and cache release is idempotent. Remove toolsCleanup / resourcesCleanup / promptsCleanup slots from McpCacheContext, the setCleanup / clearCleanup abstract methods on McpListHydrater with their three subclass overrides, and the doListHydrateCleanup helper on McpListHydrateStream. * refactor(binding-mcp): drop hydrater reference from cache context McpCacheContext no longer holds back-reference to McpProxyCacheHydrater. Signal scheduling sites use small context-capturing lambdas to invoke hydrater methods directly (sigId -> beginLifecycle(context) and sigId -> refresh(context)), so the trampoline methods onInitiateLifecycle and onRefresh on the context are gone too. McpCacheContext now takes the engine Signaler at construction time (provided via McpBindingConfig from EngineContext) which it uses directly for handle dispatch in register() and markComplete(). The detached guard moves into beginLifecycle and refresh on the hydrater where the orchestration actually happens. bind(hydrater, expected) becomes prepare(expected) — just session state reset, no hydrater stored. The lambda allocation per signal scheduling fires at most a handful of times per binding lifetime (attach, lease retry, cache TTL refresh) — nothing on the hot path. * refactor(binding-mcp): move orchestration into cache context McpCacheContext takes the per-worker hydrater as a final ctor field and owns the full lifecycle state machine. McpProxyFactory.attach calls context.start() (no args) and context.detach() directly; the hydrater's attach/detach/beg…
1 parent 115457a commit 1eb6b70

107 files changed

Lines changed: 11745 additions & 3019 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

runtime/binding-kafka/src/main/java/io/aklivity/zilla/runtime/binding/kafka/internal/stream/KafkaClientConnectionPool.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import static io.aklivity.zilla.runtime.engine.concurrent.Signaler.NO_CANCEL_ID;
2121
import static java.lang.System.currentTimeMillis;
2222

23+
import java.time.Instant;
2324
import java.util.List;
2425
import java.util.function.Consumer;
2526
import java.util.function.IntConsumer;
@@ -591,6 +592,15 @@ public long signalAt(
591592
return delegate.signalAt(timeMillis, signalId, handler);
592593
}
593594

595+
@Override
596+
public long signalAt(
597+
Instant time,
598+
int signalId,
599+
IntConsumer handler)
600+
{
601+
return delegate.signalAt(time, signalId, handler);
602+
}
603+
594604
@Override
595605
public void signalNow(
596606
long originId,
@@ -639,6 +649,19 @@ public long signalAt(
639649
return stream.doStreamSignalAt(traceId, timeMillis, signalId);
640650
}
641651

652+
@Override
653+
public long signalAt(
654+
Instant time,
655+
long originId,
656+
long routedId,
657+
long streamId,
658+
long traceId,
659+
int signalId,
660+
int contextId)
661+
{
662+
return signalAt(time.toEpochMilli(), originId, routedId, streamId, traceId, signalId, contextId);
663+
}
664+
642665
@Override
643666
public long signalTask(
644667
Runnable task,

runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/config/McpAuthorizationConfig.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
public final class McpAuthorizationConfig
2222
{
2323
public final String name;
24+
public final String credentials;
2425

2526
public transient String qname;
2627

@@ -36,8 +37,10 @@ public static <T> McpAuthorizationConfigBuilder<T> builder(
3637
}
3738

3839
McpAuthorizationConfig(
39-
String name)
40+
String name,
41+
String credentials)
4042
{
4143
this.name = name;
44+
this.credentials = credentials;
4245
}
4346
}

runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/config/McpAuthorizationConfigBuilder.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ public final class McpAuthorizationConfigBuilder<T> extends ConfigBuilder<T, Mcp
2323
private final Function<McpAuthorizationConfig, T> mapper;
2424

2525
private String name;
26+
private String credentials;
2627

2728
McpAuthorizationConfigBuilder(
2829
Function<McpAuthorizationConfig, T> mapper)
@@ -44,9 +45,16 @@ public McpAuthorizationConfigBuilder<T> name(
4445
return this;
4546
}
4647

48+
public McpAuthorizationConfigBuilder<T> credentials(
49+
String credentials)
50+
{
51+
this.credentials = credentials;
52+
return this;
53+
}
54+
4755
@Override
4856
public T build()
4957
{
50-
return mapper.apply(new McpAuthorizationConfig(name));
58+
return mapper.apply(new McpAuthorizationConfig(name, credentials));
5159
}
5260
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright 2021-2024 Aklivity Inc
3+
*
4+
* Licensed under the Aklivity Community License (the "License"); you may not use
5+
* this file except in compliance with the License. You may obtain a copy of the
6+
* License at
7+
*
8+
* https://www.aklivity.io/aklivity-community-license/
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OF ANY KIND, either express or implied. See the License for the
13+
* specific language governing permissions and limitations under the License.
14+
*/
15+
package io.aklivity.zilla.runtime.binding.mcp.config;
16+
17+
import static java.util.function.Function.identity;
18+
19+
import java.time.Duration;
20+
import java.util.function.Function;
21+
22+
public final class McpCacheConfig
23+
{
24+
public final String store;
25+
public final Duration ttl;
26+
public final McpAuthorizationConfig authorization;
27+
28+
McpCacheConfig(
29+
String store,
30+
Duration ttl,
31+
McpAuthorizationConfig authorization)
32+
{
33+
this.store = store;
34+
this.ttl = ttl;
35+
this.authorization = authorization;
36+
}
37+
38+
public static McpCacheConfigBuilder<McpCacheConfig> builder()
39+
{
40+
return new McpCacheConfigBuilder<>(identity());
41+
}
42+
43+
public static <T> McpCacheConfigBuilder<T> builder(
44+
Function<McpCacheConfig, T> mapper)
45+
{
46+
return new McpCacheConfigBuilder<>(mapper);
47+
}
48+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Copyright 2021-2024 Aklivity Inc
3+
*
4+
* Licensed under the Aklivity Community License (the "License"); you may not use
5+
* this file except in compliance with the License. You may obtain a copy of the
6+
* License at
7+
*
8+
* https://www.aklivity.io/aklivity-community-license/
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OF ANY KIND, either express or implied. See the License for the
13+
* specific language governing permissions and limitations under the License.
14+
*/
15+
package io.aklivity.zilla.runtime.binding.mcp.config;
16+
17+
import java.time.Duration;
18+
import java.util.function.Function;
19+
20+
import io.aklivity.zilla.runtime.engine.config.ConfigBuilder;
21+
22+
public final class McpCacheConfigBuilder<T> extends ConfigBuilder<T, McpCacheConfigBuilder<T>>
23+
{
24+
private final Function<McpCacheConfig, T> mapper;
25+
26+
private String store;
27+
private Duration ttl;
28+
private McpAuthorizationConfig authorization;
29+
30+
McpCacheConfigBuilder(
31+
Function<McpCacheConfig, T> mapper)
32+
{
33+
this.mapper = mapper;
34+
}
35+
36+
@Override
37+
@SuppressWarnings("unchecked")
38+
protected Class<McpCacheConfigBuilder<T>> thisType()
39+
{
40+
return (Class<McpCacheConfigBuilder<T>>) getClass();
41+
}
42+
43+
public McpCacheConfigBuilder<T> store(
44+
String store)
45+
{
46+
this.store = store;
47+
return this;
48+
}
49+
50+
public McpCacheConfigBuilder<T> ttl(
51+
Duration ttl)
52+
{
53+
this.ttl = ttl;
54+
return this;
55+
}
56+
57+
public McpCacheConfigBuilder<T> authorization(
58+
McpAuthorizationConfig authorization)
59+
{
60+
this.authorization = authorization;
61+
return this;
62+
}
63+
64+
public McpAuthorizationConfigBuilder<McpCacheConfigBuilder<T>> authorization()
65+
{
66+
return McpAuthorizationConfig.builder(this::authorization);
67+
}
68+
69+
@Override
70+
public T build()
71+
{
72+
return mapper.apply(new McpCacheConfig(store, ttl, authorization));
73+
}
74+
}

runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/config/McpOptionsConfig.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,18 @@ public final class McpOptionsConfig extends OptionsConfig
2424
public final List<McpPromptConfig> prompts;
2525
public final McpElicitationConfig elicitation;
2626
public final McpAuthorizationConfig authorization;
27+
public final McpCacheConfig cache;
2728

28-
public McpOptionsConfig(
29+
McpOptionsConfig(
2930
List<McpPromptConfig> prompts,
3031
McpElicitationConfig elicitation,
31-
McpAuthorizationConfig authorization)
32+
McpAuthorizationConfig authorization,
33+
McpCacheConfig cache)
3234
{
3335
this.prompts = prompts;
3436
this.elicitation = elicitation;
3537
this.authorization = authorization;
38+
this.cache = cache;
3639
}
3740

3841
public static McpOptionsConfigBuilder<McpOptionsConfig> builder()

runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/config/McpOptionsConfigBuilder.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ public final class McpOptionsConfigBuilder<T> extends ConfigBuilder<T, McpOption
2828
private List<McpPromptConfig> prompts;
2929
private McpElicitationConfig elicitation;
3030
private McpAuthorizationConfig authorization;
31+
private McpCacheConfig cache;
3132

3233
public McpOptionsConfigBuilder(
3334
Function<OptionsConfig, T> mapper)
@@ -71,6 +72,18 @@ public McpAuthorizationConfigBuilder<McpOptionsConfigBuilder<T>> authorization()
7172
return McpAuthorizationConfig.builder(this::authorization);
7273
}
7374

75+
public McpOptionsConfigBuilder<T> cache(
76+
McpCacheConfig cache)
77+
{
78+
this.cache = cache;
79+
return this;
80+
}
81+
82+
public McpCacheConfigBuilder<McpOptionsConfigBuilder<T>> cache()
83+
{
84+
return McpCacheConfig.builder(this::cache);
85+
}
86+
7487
@Override
7588
@SuppressWarnings("unchecked")
7689
protected Class<McpOptionsConfigBuilder<T>> thisType()
@@ -81,6 +94,6 @@ protected Class<McpOptionsConfigBuilder<T>> thisType()
8194
@Override
8295
public T build()
8396
{
84-
return mapper.apply(new McpOptionsConfig(prompts, elicitation, authorization));
97+
return mapper.apply(new McpOptionsConfig(prompts, elicitation, authorization, cache));
8598
}
8699
}

runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/McpConfiguration.java

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,21 @@
1414
*/
1515
package io.aklivity.zilla.runtime.binding.mcp.internal;
1616

17+
import static io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.McpBeginExFW.KIND_PROMPTS_LIST;
18+
import static io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.McpBeginExFW.KIND_RESOURCES_LIST;
19+
import static io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.McpBeginExFW.KIND_TOOLS_LIST;
1720
import static io.aklivity.zilla.runtime.engine.EngineConfiguration.ENGINE_WORKERS;
1821

1922
import java.lang.invoke.MethodHandle;
2023
import java.lang.invoke.MethodHandles;
2124
import java.lang.invoke.MethodType;
2225
import java.security.SecureRandom;
2326
import java.time.Duration;
27+
import java.util.HashSet;
2428
import java.util.HexFormat;
29+
import java.util.Set;
2530
import java.util.UUID;
31+
import java.util.function.IntPredicate;
2632
import java.util.function.Supplier;
2733

2834
import org.agrona.LangUtil;
@@ -46,6 +52,9 @@ public class McpConfiguration extends Configuration
4652
public static final PropertyDef<Duration> MCP_SSE_KEEPALIVE_INTERVAL;
4753
public static final BooleanPropertyDef MCP_ALT_SVC_ENABLED;
4854
public static final PropertyDef<Duration> MCP_ALT_SVC_MAX_AGE;
55+
public static final PropertyDef<IntPredicate> MCP_HYDRATE_FILTER;
56+
public static final PropertyDef<Duration> MCP_LEASE_TTL;
57+
public static final PropertyDef<Duration> MCP_LEASE_RETRY;
4958

5059
static
5160
{
@@ -72,6 +81,12 @@ public class McpConfiguration extends Configuration
7281
MCP_ALT_SVC_ENABLED = config.property("alt.svc.enabled", McpConfiguration::defaultAltSvcEnabled);
7382
MCP_ALT_SVC_MAX_AGE = config.property(Duration.class, "alt.svc.max.age",
7483
(c, v) -> Duration.parse(v), "PT24H");
84+
MCP_HYDRATE_FILTER = config.property(IntPredicate.class, "hydrate.filter",
85+
McpConfiguration::decodeHydrateFilter, McpConfiguration::defaultHydrateFilter);
86+
MCP_LEASE_TTL = config.property(Duration.class, "lease.ttl",
87+
(c, v) -> Duration.parse(v), "PT30S");
88+
MCP_LEASE_RETRY = config.property(Duration.class, "lease.retry",
89+
(c, v) -> Duration.parse(v), "PT0.1S");
7590
MCP_CONFIG = config;
7691
}
7792

@@ -146,6 +161,21 @@ public Duration altSvcMaxAge()
146161
return MCP_ALT_SVC_MAX_AGE.get(this);
147162
}
148163

164+
public IntPredicate hydrateFilter()
165+
{
166+
return MCP_HYDRATE_FILTER.get(this);
167+
}
168+
169+
public Duration leaseTtl()
170+
{
171+
return MCP_LEASE_TTL.get(this);
172+
}
173+
174+
public Duration leaseRetry()
175+
{
176+
return MCP_LEASE_RETRY.get(this);
177+
}
178+
149179
@FunctionalInterface
150180
public interface SessionIdSupplier
151181
{
@@ -258,6 +288,36 @@ private static ElicitationIdSupplier decodeElicitationIdSupplier(
258288
return supplier;
259289
}
260290

291+
private static IntPredicate decodeHydrateFilter(
292+
String value)
293+
{
294+
final Set<Integer> kinds = new HashSet<>();
295+
for (String name : value.split("\\s+"))
296+
{
297+
switch (name)
298+
{
299+
case "tools":
300+
kinds.add(KIND_TOOLS_LIST);
301+
break;
302+
case "resources":
303+
kinds.add(KIND_RESOURCES_LIST);
304+
break;
305+
case "prompts":
306+
kinds.add(KIND_PROMPTS_LIST);
307+
break;
308+
default:
309+
break;
310+
}
311+
}
312+
return kinds::contains;
313+
}
314+
315+
private static boolean defaultHydrateFilter(
316+
int kind)
317+
{
318+
return true;
319+
}
320+
261321
private static String defaultElicitationIdSupplier()
262322
{
263323
final byte[] bytes = new byte[4];

0 commit comments

Comments
 (0)