Skip to content

fix(binding-mcp): honor south exits' real capabilities in route resolution - #2529

Merged
jfallows merged 2 commits into
developfrom
claude/zilla-issue-2524-h7sxbm
Sep 3, 2026
Merged

fix(binding-mcp): honor south exits' real capabilities in route resolution#2529
jfallows merged 2 commits into
developfrom
claude/zilla-issue-2524-h7sxbm

Conversation

@jfallows

@jfallows jfallows commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Description

McpBindingConfig.resolve()/resolveAll() — the shared route-resolution path used by both live request routing and McpProxyCacheHydrater's cache hydration — decided whether a route serves a given capability (tools/prompts/resources) using only the route's static when: condition, never consulting realCapabilitiesByRoute, the mechanism that actually learns a south route's real, live capabilities from its own KIND_LIFECYCLE handshake. Since an unrestricted when: condition (e.g. selecting only on toolkit:) is treated as serving every capability, a route in front of a tools-only south exit (mcp-kafka) was still probed for prompts/resources, surfacing as continuous BINDING_MCP_HYDRATE_FAILED ... ROUTE_FAILED during cache hydration, plus the same gap in live request routing.

Tracing the two other binding types named in the issue (mcp-kafka-connect, mcp-schema-registry) further: both are pure byte-transparent relays into an internally-composited mcp-openapi binding, itself a transparent relay into a further-composited mcp-http binding — the actual KIND_LIFECYCLE handshake for all three terminates in McpHttpProxyFactory, which had the same bug in a more severe form (no fallback at all, unconditional echo).

Changes

  1. binding-mcpMcpBindingConfig now intersects a route's static capability set with its recorded real capabilities once that exit's lifecycle handshake has reported them, at both call sites sharing this resolution path: cache hydration (resolveAll(traceId, kind)) and live request routing (resolve/resolveAll on McpBeginExFW). Before that handshake completes, no real capabilities are recorded yet and the static set is used as-is, so this narrows an over-broad match rather than gating on wire timing.
  2. binding-mcp-kafka — tools-only by construction (its route condition schema rejects prompt:/resource: selectors), so its KIND_LIFECYCLE reply now always declares SERVER_TOOLS instead of echoing back whatever capabilities bits the connecting north forwarded for its own client's elicitation support.
  3. binding-mcp-http — not tools-only (McpOpenapiCompositeGenerator routes some OpenAPI operations to tools and others to resources depending on the spec), so a fixed constant would be wrong here. McpHttpBindingConfig.serverCapabilities() now computes real capabilities from what's actually configured (SERVER_TOOLS when tools() is non-empty, SERVER_RESOURCES when resources() is non-empty), and McpHttpProxyFactory's lifecycle reply declares that instead of the client-forwarded value. This transitively fixes mcp-kafka-connect and mcp-schema-registry too (both only ever generate tool routes through the composite chain in practice), as well as mcp-openapi/mcp-http used directly with resource-only routes (confirmed schema-valid: neither binding's schema requires at least one tool route).

Testing

  • McpBindingConfigTest: unit tests proving a route with an unrestricted condition is excluded from prompts resolution once real capabilities are recorded as tools-only, and still included beforehand (before the lifecycle handshake completes).
  • McpKafkaProxyFactoryTest: proves the lifecycle reply always declares tools-only regardless of what the client forwarded.
  • McpHttpBindingConfigTest: proves serverCapabilities() returns the correct bits for tools-only, resources-only, both, and neither configured.

Build verification caveat: ./mvnw verify could not be run in the sandbox this was authored in — flyweight-maven-plugin and other internal artifacts require resolving from maven.packages.aklivity.io, which redirects to an auth flow unavailable there. Changes were reviewed carefully by hand (types, imports, checkstyle conventions, generated-flyweight API shapes cross-checked against existing working code) but CI is the first real compile/checkstyle/test confirmation.

Fixes #2524

🤖 Generated with Claude Code


Generated by Claude Code

…ution

McpBindingConfig.resolve()/resolveAll() decided whether a route serves a
given capability (tools/prompts/resources) using only the route's static
when: condition, never consulting realCapabilitiesByRoute -- the capabilities
a south exit actually declares over its own KIND_LIFECYCLE handshake. Since
an unrestricted when: condition (e.g. selecting only on toolkit:) is treated
as serving every capability, a route in front of a tools-only south exit was
still probed for prompts/resources, surfacing as continuous
BINDING_MCP_HYDRATE_FAILED ... ROUTE_FAILED during cache hydration and the
same gap in live request routing.

McpBindingConfig now intersects a route's static capability set with its
recorded real capabilities once that exit's lifecycle handshake has reported
them, at both call sites that share this resolution path: cache hydration
(resolveAll(traceId, kind)) and live request routing (resolve/resolveAll on
McpBeginExFW). Before that handshake completes, no real capabilities are
recorded yet and the static set is used as-is, so this narrows an
over-broad match rather than gating on wire timing.

mcp-kafka is tools-only by construction (its route condition schema rejects
prompt:/resource: selectors), so its KIND_LIFECYCLE reply now always
declares SERVER_TOOLS instead of echoing back whatever capabilities bits
the connecting north forwarded for its own client's elicitation support.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Px3kTeX1XXiUpEk4pLwcjq
…orth's

mcp-kafka-connect and mcp-schema-registry are pure byte-transparent relays
(McpKafkaConnectClientFactory / a like factory) into an internally-composed
mcp-openapi binding, which is itself a transparent relay into a further
internally-composed mcp-http binding -- the actual KIND_LIFECYCLE handshake
for all three of these south binding types terminates in McpHttpProxyFactory,
not in their own factories.

McpHttpProxyFactory's KIND_LIFECYCLE reply echoed back whatever capabilities
bits the connecting north forwarded verbatim, with no real declaration at
all -- the same class of bug already fixed for mcp-kafka, but mcp-http is
not tools-only: McpOpenapiCompositeGenerator routes some OpenAPI operations
to tools and others to resources depending on the spec, so a fixed constant
would be wrong here (and would misdeclare capabilities for anyone
configuring mcp-http/mcp-openapi directly with resource routes).

McpHttpBindingConfig.serverCapabilities() now computes real capabilities
from what's actually configured (SERVER_TOOLS when tools() is non-empty,
SERVER_RESOURCES when resources() is non-empty), and McpHttpProxyFactory's
lifecycle reply declares that instead of the client-forwarded value. This
closes the same routing gap from the prior commit for mcp-kafka-connect and
mcp-schema-registry (both of which, via the composite chain, only ever
generate tool routes into mcp-http in practice) as well as for mcp-openapi
and mcp-http used directly with resource routes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Px3kTeX1XXiUpEk4pLwcjq
@jfallows
jfallows merged commit d3c1061 into develop Sep 3, 2026
43 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

mcp(proxy): routing and cache hydration ignore south routes' real server capabilities, defaulting to "serves everything"

2 participants