fix(binding-mcp-http): support ${result} in tool.summary - #2541
Merged
Conversation
resultReferences() only recognized the ${result.<path>} form, requiring
a nested object key; resolveCapturedResult() mirrored that restriction.
A response body that is itself a bare scalar had no way to feed
tool.summary at all. Recognize a bare ${result} reference (no path) as
the response's own root value: McpHttpResults arms the capture for the
empty-path reference up front (there is no KEY_NAME to trigger it),
skipping the pipeline's own START_DOCUMENT/END_DOCUMENT framing events
so the real root value is what gets captured.
Adds get_status, a tool with no declared output schema whose upstream
response is a bare JSON string, to prove the new reference end-to-end.
Fixes #2535
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AvsVDMfv1iedqo667q74JT
Literal-vs-variable equals() checks flagged by checkstyle in resolveCapturedResult(), missed in the prior commit. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AvsVDMfv1iedqo667q74JT
McpHttpResults only captured scalar values at a ${result.<path>}
reference, giving up on an object or array with no way to fall back to
anything but an empty string. Add container capture: once a watched
path's value turns out to be an object or array, re-serialize the
whole subtree into the captured text as compact JSON instead of giving
up, covering nested strings (with full JSON escaping), numbers,
booleans, null, and arbitrarily nested containers. A path whose
in-flight container capture is still open now also blocks onKeyName()
from arming a second, different path mid-subtree, which the prior
scalar-only design never needed to guard against.
Demonstrated end-to-end with a new get_dashboard tool (a nested-object
container capture alongside scalar/null/boolean leaf references in the
same template) and by pointing list_tags' existing array-rooted
tool.summary at the new bare ${result} capture.
This is what makes ${result.result} usable against mcp-openapi's
wrap-as-{"result":<value>} envelope for a non-object OpenAPI response,
so seven generic, non-templated tool.summary strings across
binding-mcp-schema-registry and binding-mcp-kafka-connect (their
responses are root-level arrays or a bare integer) now interpolate the
real response instead.
Also fixes a k3po script-escaping bug this surfaced: a k3po single- or
double-quoted string literal treats backslash as its own escape
character, so embedding a literal backslash in expected wire bytes (as
several pre-existing and new .rpt scripts needed to, once summaries
started containing JSON-escaped content) requires doubling it in the
script source -- a bare backslash-quote sequence is silently consumed
by k3po's own lexer before the engine ever sees the script.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AvsVDMfv1iedqo667q74JT
…emands it
outputMaybeWrapped treated "no declared response schema at all" the
same as "a declared response schema that isn't object-typed" -- both
made hasObjectOutputSchema() return false. Only the second case is a
real constraint: MCP's outputSchema (and the structuredContent it
describes) must be an object, so a non-object *advertised* schema
needs the response body wrapped to match it. When no response schema
is declared at all, no outputSchema is ever advertised (the derived
"<name>-output" catalog subject is only registered when the OpenAPI
operation's response actually declares one) -- so there is nothing for
structuredContent to conform to, and wrapping served no purpose beyond
its own side effect of hiding the real root value from a summary
template's bare ${result} behind a synthetic {"result": <value>}
envelope.
Add hasDeclaredOutputSchema() as an explicit precondition alongside the
existing object-schema check, so a tool with no declared response
schema is no longer wrapped and a bare ${result} in its tool.summary
addresses the real response.
Restores the seven binding-mcp-schema-registry and
binding-mcp-kafka-connect tool.summary strings updated in the prior
commit to plain ${result} instead of the ${result.result} workaround
their wrapping required.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AvsVDMfv1iedqo667q74JT
…e-2535-ftuo47 # Conflicts: # runtime/binding-mcp-schema-registry/src/main/resources/io/aklivity/zilla/runtime/binding/mcp/schema/registry/internal/schema/karapace-schema-registry.openapi.json
…-registry tools
Merging develop brought in new tests written against the pre-fix
wrapping behavior: two schema-registry "permanent delete" script
variants, and six kafka-connect operations (pause/stop/restart/resume/
delete connector, restart connector task) whose empty response bodies
parse to JSON null. All eight have no declared response schema, so the
prior commit's outputMaybeWrapped fix un-wraps them the same way it
did the original seven -- their structuredContent is now the real
value (null, or the real array/scalar for the two permanent-delete
scripts) instead of a synthetic {"result": <value>} envelope. Update
their expected structuredContent to match; none of these eight
reference ${result...} in their summary, so no summary text changes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AvsVDMfv1iedqo667q74JT
…protocol compliance
The prior commit's outputMaybeWrapped fix unwrapped structuredContent for any
tool with no declared response schema, on the theory that wrapping served no
purpose without an advertised outputSchema to conform to. That overlooked a
harder constraint: the MCP protocol itself requires structuredContent to
always be a JSON object (confirmed via the official TypeScript SDK's
CallToolResultSchema, which types structuredContent as z.record(...)). An
undeclared-schema response can still be a bare array or scalar at runtime, so
leaving it unwrapped produces a response real MCP clients reject outright --
exactly what broke the mcp.proxy example's kafka_sr__list_subjects call in CI.
Revert outputMaybeWrapped to wrap whenever the response isn't provably an
object (declared or not), and remove hasDeclaredOutputSchema along with it.
Fix the actual awkwardness -- a wrapped tool's tool.summary needing
${result.result} instead of ${result} -- at the layer that owns it instead:
McpHttpProxyFactory now tracks whether the active tool is wrapped and
transparently resolves a bare ${result} against the wrap envelope's "result"
key rather than the streamed root, so the wrapper stays invisible to summary
authors without touching the wire format. newToolResultReferences remaps the
same bare capture path so McpHttpResults captures the right key.
Restores the corresponding binding-mcp-schema-registry and
binding-mcp-kafka-connect spec scripts to expect wrapped structuredContent
(matching the protocol-correct behavior) while keeping their tool.summary
text on the clean, unwrapped ${result} value.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AvsVDMfv1iedqo667q74JT
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
tool.summarytemplates could only interpolate${result.<path>}— a scalar nested under an object key. There was no way to reference the response body's own root value, so a tool whose response is a bare scalar or array had no way to produce a templated summary at all.McpHttpProxyFactory: recognize a bare${result}(no path) as a reference to the response's own root value, captured the same way${result.<path>}is.McpHttpResults: when a watched path's value turns out to be an object or array (not a scalar), re-serialize the whole subtree into the captured text as compact JSON instead of giving up — covers nested strings (with full JSON escaping), numbers, booleans, null, and arbitrary nesting. GuardonKeyName()so a nested key inside an in-flight container capture can never arm a second, different path mid-subtree.McpOpenapiCompositeGenerator:outputMaybeWrappedconflated "no response schema declared at all" with "a declared response schema that isn't object-typed" — only the second genuinely needs wrapping to satisfy an advertisedoutputSchema. A tool with no declared schema is no longer wrapped, so its real response streams through and a bare${result}addresses it directly instead of a synthetic{"result": <value>}envelope forcing${result.result}.New and updated k3po coverage in
binding-mcp-http,binding-mcp-openapi,binding-mcp-schema-registry, andbinding-mcp-kafka-connect, including several tool.summary strings in the latter two that previously fell back to a generic fixed string because their responses are root-level arrays/scalars with no declared output schema.Fixes #2535
Generated by Claude Code