diff --git a/runtime/binding-mcp-http/src/main/java/io/aklivity/zilla/runtime/binding/mcp/http/internal/stream/McpHttpProxyFactory.java b/runtime/binding-mcp-http/src/main/java/io/aklivity/zilla/runtime/binding/mcp/http/internal/stream/McpHttpProxyFactory.java index a4c5d5788fc..aaeaaa2406f 100644 --- a/runtime/binding-mcp-http/src/main/java/io/aklivity/zilla/runtime/binding/mcp/http/internal/stream/McpHttpProxyFactory.java +++ b/runtime/binding-mcp-http/src/main/java/io/aklivity/zilla/runtime/binding/mcp/http/internal/stream/McpHttpProxyFactory.java @@ -1001,6 +1001,12 @@ private final class McpToolsCallProxy extends McpHttpProxy // response completes to resolve tool.summary's ${result.*} references without re-scanning a buffer private final Map capturedResults = new HashMap<>(); + // true when this tool's structuredContent is McpHttpResultWrap-wrapped as {"result":} to satisfy + // the MCP wire contract that structuredContent is always a JSON object; a bare ${result} then means the + // real (unwrapped) response value, so it is resolved against the captured "result" key rather than the + // captured root -- keeping the wrapper an implementation detail invisible to tool.summary authors + private boolean resultWrapped; + // the non-2xx response mode: relays the raw upstream body as escaped text with no JsonPipeline at all // (the body is not guaranteed to be valid JSON), using errorGenerator directly the same way responseStep // uses responseGenerator — wrap against encodeSlot's live position, drive via consumed()/length() @@ -1353,9 +1359,10 @@ void responseBegin( final String summaryTemplate = tool != null ? tool.summary : null; final List resultPaths = tool != null ? toolResultReferences(tool) : List.of(); + resultWrapped = tool != null && tool.outputMaybeWrapped; responseGenerator = JsonEx.createGenerator(); JsonStream stream = JsonEx.stream(JsonEx.createParser()); - if (tool != null && tool.outputMaybeWrapped) + if (resultWrapped) { // nothing proves the upstream body is already an object; route it through the // transform that decides, from the real body's own first event, whether it actually @@ -1453,15 +1460,19 @@ private JsonPipeline.Status errorRelayStep( return status; } - // Resolves a result. reference from the values McpHttpResults captured while structuredContent - // streamed past, replacing a re-scan of a fully buffered response copy. + // Resolves a result. reference, or a bare result reference (the response's own root value), + // from the values McpHttpResults captured while structuredContent streamed past, replacing a + // re-scan of a fully buffered response copy. When resultWrapped, the captured root is the + // McpHttpResultWrap envelope rather than the real value, so a bare ${result} is redirected to the + // captured "result" key instead -- see resultWrapped's field doc. private String resolveCapturedResult( String expression) { String value = ""; - if (expression.startsWith("result.")) + if ("result".equals(expression) || expression.startsWith("result.")) { - final String captured = capturedResults.get(expression.substring(7)); + final String path = "result".equals(expression) ? (resultWrapped ? "result" : "") : expression.substring(7); + final String captured = capturedResults.get(path); value = captured != null ? captured : ""; } return value; @@ -2398,7 +2409,14 @@ private List toolResultReferences( private List newToolResultReferences( McpHttpToolConfig tool) { - return resultReferences(tool.summary); + final List paths = resultReferences(tool.summary); + if (tool.outputMaybeWrapped) + { + // the streamed root is the McpHttpResultWrap envelope, not the real value -- capture "result" + // (the envelope's own value key) in place of a bare root capture, matching resolveCapturedResult + paths.replaceAll(path -> path.isEmpty() ? "result" : path); + } + return paths; } private void appendQuery( @@ -2827,7 +2845,8 @@ private static List argReferences( // Extracts the result. references from a tool.summary template (e.g. "result.number" from // "Created pull request #${result.number}"), the set McpHttpResults is asked to capture as the response - // streams past. + // streams past. A bare ${result} (no path) — the response body's own root value — is recorded as the + // empty-string path, McpHttpResults' sentinel for a root capture. private static List resultReferences( String template) { @@ -2835,17 +2854,30 @@ private static List resultReferences( if (template != null) { int index = 0; - int start = template.indexOf("${result.", index); + int start = template.indexOf("${result", index); while (start >= 0) { - final int end = template.indexOf('}', start); - if (end < 0) + final int afterKeyword = start + 8; + if (afterKeyword < template.length() && template.charAt(afterKeyword) == '.') { - break; + final int end = template.indexOf('}', afterKeyword); + if (end < 0) + { + break; + } + result.add(template.substring(afterKeyword + 1, end)); + index = end + 1; } - result.add(template.substring(start + 9, end)); - index = end + 1; - start = template.indexOf("${result.", index); + else if (afterKeyword < template.length() && template.charAt(afterKeyword) == '}') + { + result.add(""); + index = afterKeyword + 1; + } + else + { + index = afterKeyword; + } + start = template.indexOf("${result", index); } } return result; diff --git a/runtime/binding-mcp-http/src/main/java/io/aklivity/zilla/runtime/binding/mcp/http/internal/transform/McpHttpResults.java b/runtime/binding-mcp-http/src/main/java/io/aklivity/zilla/runtime/binding/mcp/http/internal/transform/McpHttpResults.java index 41571c94352..3cd5deddaa6 100644 --- a/runtime/binding-mcp-http/src/main/java/io/aklivity/zilla/runtime/binding/mcp/http/internal/transform/McpHttpResults.java +++ b/runtime/binding-mcp-http/src/main/java/io/aklivity/zilla/runtime/binding/mcp/http/internal/transform/McpHttpResults.java @@ -25,22 +25,39 @@ import io.aklivity.zilla.runtime.common.json.JsonTransform; /** - * Watches a {@code tools/call} response body as it streams past and captures the scalar value at each of a - * fixed set of dotted {@code result.} references (e.g. {@code number}, {@code data.id}) — the paths a - * configured {@code tool.summary} template interpolates — into {@code captured}, while forwarding every event - * downstream unchanged ({@link #identity()} returns {@code true}). This lets a summary be resolved once the - * response has fully streamed past without re-scanning a buffered copy, the response-side counterpart to + * Watches a {@code tools/call} response body as it streams past and captures the value at each of a fixed set + * of dotted {@code result.} references (e.g. {@code number}, {@code data.id}) — the paths a configured + * {@code tool.summary} template interpolates — into {@code captured}, while forwarding every event downstream + * unchanged ({@link #identity()} returns {@code true}). This lets a summary be resolved once the response has + * fully streamed past without re-scanning a buffered copy, the response-side counterpart to * {@link McpHttpArguments}'s request-side capture. *

* Each configured path tracks its own match progress independently against the shared document depth, mirroring * the depth/matched-segment algorithm a one-shot buffered lookup would use (match segment {@code n} of a path * only at a {@code KEY_NAME} seen at depth {@code n + 1}), extended to watch several paths — of possibly - * different lengths and depths — over one incremental pass instead of one re-scan per path. Only scalar events - * ({@code VALUE_STRING}, {@code VALUE_NUMBER}, {@code VALUE_TRUE}, {@code VALUE_FALSE}) are captured. A value - * spanning more than one input window accumulates across every fragment into {@code text} and only commits - * once {@link JsonSource#deferredBytes()} reports the value complete; since only one value can ever be - * captured "in flight" at a time (JSON parsing is strictly sequential), a single reused accumulator is - * sufficient. + * different lengths and depths — over one incremental pass instead of one re-scan per path. + *

+ * A path whose value turns out to be a scalar ({@code VALUE_STRING}, {@code VALUE_NUMBER}, {@code VALUE_TRUE}, + * {@code VALUE_FALSE}, {@code VALUE_NULL}) captures that scalar's own text verbatim (a string capturing its + * content unquoted, a number its digits, {@code true}/{@code false}/{@code null} their literal spelling) — + * exactly what a template author who wrote {@code Created pull request #${result.number}} expects to see + * substituted in place of {@code ${result.number}}. A path whose value turns out to be an object or array + * instead re-serializes that whole subtree into {@code text} as compact JSON (see {@link #captureContainer}), + * so a template can still say something meaningful about a response shaped as a list or nested record even + * though there is no single scalar to point at. A value spanning more than one input window accumulates across + * every fragment into {@code text} and only commits once {@link JsonSource#deferredBytes()} reports the value + * complete (for a container capture, once the container's own matching close event is reached); since only one + * value can ever be captured "in flight" at a time (JSON parsing is strictly sequential), a single reused + * accumulator is sufficient. While one path's capture is in flight ({@code awaiting != -1}), {@link #onKeyName} + * is skipped entirely — a nested key inside the very subtree being serialized must never be allowed to arm a + * second, different path and corrupt the first one's still-open accumulation. + *

+ * The empty path (zero segments) denotes a bare {@code result} reference — the response body's own root + * value, with no key to match a {@code KEY_NAME} against — so it is armed as {@code awaiting} up front by + * {@link #reset()} instead of via {@link #onKeyName}. {@link #transform} excludes the pipeline's own + * {@code START_DOCUMENT}/{@code END_DOCUMENT} framing events from reaching {@link #capture} so that framing, + * not the real root value, is what the root reference would otherwise capture first; the first real event + * after that is the document's own root, whatever shape it turns out to be. *

* This is a mediating, structure-inspecting transform sitting in front of a byte-preferring terminal sink * (see {@code common-json}'s verbatim-validate design notes), so it cannot forward a downstream @@ -55,6 +72,9 @@ */ public final class McpHttpResults implements JsonTransform { + private static final String[] EMPTY_SEGMENTS = new String[0]; + private static final char[] HEX_DIGITS = "0123456789abcdef".toCharArray(); + private final Map captured; private final String[] paths; private final String[][] segments; @@ -88,6 +108,8 @@ public void consumed( private JsonController upstream; private int depth; private int awaiting = -1; + private int captureDepth; + private boolean nestedValueOpen; public McpHttpResults( Map captured, @@ -98,7 +120,7 @@ public McpHttpResults( this.segments = new String[this.paths.length][]; for (int i = 0; i < this.paths.length; i++) { - this.segments[i] = this.paths[i].split("\\."); + this.segments[i] = this.paths[i].isEmpty() ? EMPTY_SEGMENTS : this.paths[i].split("\\."); } this.matched = new int[this.paths.length]; } @@ -108,10 +130,16 @@ public void reset() { depth = 0; awaiting = -1; + captureDepth = 0; + nestedValueOpen = false; text.setLength(0); for (int i = 0; i < matched.length; i++) { matched[i] = 0; + if (segments[i].length == 0) + { + awaiting = i; + } } } @@ -129,7 +157,7 @@ public Status transform( JsonSink sink) { upstream = control; - if (awaiting != -1) + if (awaiting != -1 && event != JsonEvent.START_DOCUMENT && event != JsonEvent.END_DOCUMENT) { capture(awaiting, event, source); } @@ -145,7 +173,12 @@ public Status transform( depth--; break; case KEY_NAME: - onKeyName(source); + // a nested key inside a subtree already being serialized for some other in-flight capture + // must never be allowed to arm a second, different path -- see the class Javadoc + if (awaiting == -1) + { + onKeyName(source); + } break; default: break; @@ -195,40 +228,213 @@ private void onKeyName( // Appends this fragment to the in-flight value's accumulator, committing to `captured` (and clearing // `awaiting`, so the next KEY_NAME can arm a fresh capture) only once deferredBytes() reports no more - // fragments follow; a structural value (an object/array where a scalar was expected) gives up - // immediately, matching the existing unresolved-path fallback to the empty string. A VERBATIM event - // rides alongside the structured event stream for the same value rather than substituting for it (see - // the class Javadoc), so it is ignored here rather than treated as an unexpected structural event. + // fragments follow. A VERBATIM event rides alongside the structured event stream for the same value + // rather than substituting for it (see the class Javadoc), so it is ignored here rather than treated as + // an unexpected event. Once already inside a container capture (captureDepth > 0), every event -- keys, + // nested values, nested containers -- routes to captureContainer() instead; a START_OBJECT/START_ARRAY + // seen here for the first time is what puts it there. private void capture( int index, JsonEvent event, JsonSource source) + { + if (captureDepth > 0 || event == JsonEvent.START_OBJECT || event == JsonEvent.START_ARRAY) + { + captureContainer(index, event, source); + } + else + { + switch (event) + { + case VALUE_STRING: + case VALUE_NUMBER: + text.append(source.getStringView()); + if (!source.deferredBytes()) + { + captured.put(paths[index], text.toString()); + text.setLength(0); + awaiting = -1; + } + break; + case VALUE_TRUE: + captured.put(paths[index], "true"); + awaiting = -1; + break; + case VALUE_FALSE: + captured.put(paths[index], "false"); + awaiting = -1; + break; + case VALUE_NULL: + captured.put(paths[index], "null"); + awaiting = -1; + break; + case VERBATIM: + break; + default: + awaiting = -1; + break; + } + } + } + + // Re-serializes an object/array value into `text` as compact JSON while it streams past, one event at a + // time, tracking captureDepth (the nesting depth *within this captured subtree*, independent of the + // document-wide `depth` field) to know when the value's own matching close event is reached. A comma is + // owed before any new key or value except the first child of a container or the value right after a key + // -- appendSeparator() decides this from the last character already written rather than a separate + // "first child" flag per nesting level, since '{', '[' and ':' are exactly the characters nothing else + // in a compact JSON document ever ends a token with. A string or number value spanning more than one + // input window is tracked via nestedValueOpen the same way the top-level scalar case tracks it via + // deferredBytes() directly -- the leading separator/quote is written only for the first fragment, the + // closing quote only for the last. + private void captureContainer( + int index, + JsonEvent event, + JsonSource source) { switch (event) { + case START_OBJECT: + appendSeparator(); + text.append('{'); + captureDepth++; + break; + case START_ARRAY: + appendSeparator(); + text.append('['); + captureDepth++; + break; + case END_OBJECT: + text.append('}'); + closeContainer(index); + break; + case END_ARRAY: + text.append(']'); + closeContainer(index); + break; + case KEY_NAME: + appendSeparator(); + text.append('"'); + appendEscaped(source.getStringView()); + text.append('"').append(':'); + break; case VALUE_STRING: + if (!nestedValueOpen) + { + appendSeparator(); + text.append('"'); + nestedValueOpen = true; + } + appendEscaped(source.getStringView()); + if (!source.deferredBytes()) + { + text.append('"'); + nestedValueOpen = false; + } + break; case VALUE_NUMBER: + if (!nestedValueOpen) + { + appendSeparator(); + nestedValueOpen = true; + } text.append(source.getStringView()); if (!source.deferredBytes()) { - captured.put(paths[index], text.toString()); - text.setLength(0); - awaiting = -1; + nestedValueOpen = false; } break; case VALUE_TRUE: - captured.put(paths[index], "true"); - awaiting = -1; + appendSeparator(); + text.append("true"); break; case VALUE_FALSE: - captured.put(paths[index], "false"); - awaiting = -1; + appendSeparator(); + text.append("false"); + break; + case VALUE_NULL: + appendSeparator(); + text.append("null"); break; case VERBATIM: break; default: - awaiting = -1; break; } } + + // A comma is owed before a new key or value unless it would be the first child of the container just + // opened (last character '{' or '[') or the value immediately following a key (last character ':') -- + // none of which a completed value (a closing quote, digit, e/l, or nested '}'/']') ever ends with, so + // this single check replaces a separate "first child of this nesting level" flag per depth. + private void appendSeparator() + { + if (text.length() > 0) + { + final char last = text.charAt(text.length() - 1); + if (last != '{' && last != '[' && last != ':') + { + text.append(','); + } + } + } + + // captureDepth reaching zero means the close event just appended matched the container capture's own + // opening event, so the fully reconstructed subtree in `text` is ready to commit. + private void closeContainer( + int index) + { + captureDepth--; + if (captureDepth == 0) + { + captured.put(paths[index], text.toString()); + text.setLength(0); + awaiting = -1; + } + } + + private void appendEscaped( + CharSequence value) + { + for (int i = 0; i < value.length(); i++) + { + final char c = value.charAt(i); + switch (c) + { + case '"': + text.append("\\\""); + break; + case '\\': + text.append("\\\\"); + break; + case '\n': + text.append("\\n"); + break; + case '\r': + text.append("\\r"); + break; + case '\t': + text.append("\\t"); + break; + case '\b': + text.append("\\b"); + break; + case '\f': + text.append("\\f"); + break; + default: + if (c < 0x20) + { + text.append("\\u00") + .append(HEX_DIGITS[(c >> 4) & 0xf]) + .append(HEX_DIGITS[c & 0xf]); + } + else + { + text.append(c); + } + break; + } + } + } } diff --git a/runtime/binding-mcp-http/src/test/java/io/aklivity/zilla/runtime/binding/mcp/http/internal/stream/McpHttpClientIT.java b/runtime/binding-mcp-http/src/test/java/io/aklivity/zilla/runtime/binding/mcp/http/internal/stream/McpHttpClientIT.java index 43c0dd8f077..874f8c12851 100644 --- a/runtime/binding-mcp-http/src/test/java/io/aklivity/zilla/runtime/binding/mcp/http/internal/stream/McpHttpClientIT.java +++ b/runtime/binding-mcp-http/src/test/java/io/aklivity/zilla/runtime/binding/mcp/http/internal/stream/McpHttpClientIT.java @@ -376,7 +376,7 @@ public void shouldCallToolPing() throws Exception } // list_tags' output schema is array-rooted: structuredContent is the array itself, not an object - // wrapping it + // wrapping it; tool.summary's bare ${result} captures that same root array as compact JSON @Test @Configuration("client.yaml") @Specification({ @@ -399,6 +399,32 @@ public void shouldCallToolCountItems() throws Exception k3po.finish(); } + // get_status has no declared output schema at all, so the raw upstream body streams unprojected; + // tool.summary's bare ${result} reference (no path) binds to that response's own root scalar value + @Test + @Configuration("client.yaml") + @Specification({ + "${mcp}/get.status/client", + "${http}/get.status/server"}) + public void shouldCallToolGetStatus() throws Exception + { + k3po.finish(); + } + + // get_dashboard's tool.summary combines a container reference (${result.data}, a nested object + // re-serialized as compact JSON, including a quote character that must round-trip through JSON + // escaping) with bare scalar/null/boolean leaf references (${result.active}, ${result.disabled}, + // ${result.flag}) in the same template + @Test + @Configuration("client.yaml") + @Specification({ + "${mcp}/get.dashboard/client", + "${http}/get.dashboard/server"}) + public void shouldCallToolGetDashboard() throws Exception + { + k3po.finish(); + } + // a 12000-byte top-level argument value referenced by the route's :path template: proves // McpHttpArguments captures a value spanning multiple decode windows correctly (see the // mediating-transform rule / multi-window accumulation fix), not just short path arguments that diff --git a/runtime/binding-mcp-http/src/test/java/io/aklivity/zilla/runtime/binding/mcp/http/internal/stream/McpHttpProxyIT.java b/runtime/binding-mcp-http/src/test/java/io/aklivity/zilla/runtime/binding/mcp/http/internal/stream/McpHttpProxyIT.java index eba8dd415cd..02b85d2e4b9 100644 --- a/runtime/binding-mcp-http/src/test/java/io/aklivity/zilla/runtime/binding/mcp/http/internal/stream/McpHttpProxyIT.java +++ b/runtime/binding-mcp-http/src/test/java/io/aklivity/zilla/runtime/binding/mcp/http/internal/stream/McpHttpProxyIT.java @@ -374,7 +374,7 @@ public void shouldCallToolPing() throws Exception } // list_tags' output schema is array-rooted: structuredContent is the array itself, not an object - // wrapping it + // wrapping it; tool.summary's bare ${result} captures that same root array as compact JSON @Test @Configuration("proxy.yaml") @Specification({ @@ -397,6 +397,32 @@ public void shouldCallToolCountItems() throws Exception k3po.finish(); } + // get_status has no declared output schema at all, so the raw upstream body streams unprojected; + // tool.summary's bare ${result} reference (no path) binds to that response's own root scalar value + @Test + @Configuration("proxy.yaml") + @Specification({ + "${mcp}/get.status/client", + "${http}/get.status/server"}) + public void shouldCallToolGetStatus() throws Exception + { + k3po.finish(); + } + + // get_dashboard's tool.summary combines a container reference (${result.data}, a nested object + // re-serialized as compact JSON, including a quote character that must round-trip through JSON + // escaping) with bare scalar/null/boolean leaf references (${result.active}, ${result.disabled}, + // ${result.flag}) in the same template + @Test + @Configuration("proxy.yaml") + @Specification({ + "${mcp}/get.dashboard/client", + "${http}/get.dashboard/server"}) + public void shouldCallToolGetDashboard() throws Exception + { + k3po.finish(); + } + // a 12000-byte top-level argument value referenced by the route's :path template: proves // McpHttpArguments captures a value spanning multiple decode windows correctly (see the // mediating-transform rule / multi-window accumulation fix), not just short path arguments that diff --git a/runtime/binding-mcp-kafka-connect/src/main/resources/io/aklivity/zilla/runtime/binding/mcp/kafka/connect/internal/schema/kafka-connect.openapi.json b/runtime/binding-mcp-kafka-connect/src/main/resources/io/aklivity/zilla/runtime/binding/mcp/kafka/connect/internal/schema/kafka-connect.openapi.json index 9489d37af4e..a2d14c6130a 100644 --- a/runtime/binding-mcp-kafka-connect/src/main/resources/io/aklivity/zilla/runtime/binding/mcp/kafka/connect/internal/schema/kafka-connect.openapi.json +++ b/runtime/binding-mcp-kafka-connect/src/main/resources/io/aklivity/zilla/runtime/binding/mcp/kafka/connect/internal/schema/kafka-connect.openapi.json @@ -13,7 +13,7 @@ "/connectors": { "get": { "operationId": "list_connectors", - "summary": "Listed connectors", + "summary": "Connectors: ${result}", "responses": { "200": { "description": "ok" @@ -305,7 +305,7 @@ "/connectors/{connector}/tasks": { "get": { "operationId": "list_connector_tasks", - "summary": "Listed connector tasks", + "summary": "Tasks: ${result}", "parameters": [ { "name": "connector", @@ -447,7 +447,7 @@ "/connector-plugins": { "get": { "operationId": "list_connector_plugins", - "summary": "Listed connector plugins", + "summary": "Plugins: ${result}", "responses": { "200": { "description": "ok" diff --git a/runtime/binding-mcp-openapi/src/main/java/io/aklivity/zilla/runtime/binding/mcp/openapi/internal/config/composite/McpOpenapiCompositeGenerator.java b/runtime/binding-mcp-openapi/src/main/java/io/aklivity/zilla/runtime/binding/mcp/openapi/internal/config/composite/McpOpenapiCompositeGenerator.java index e73333dafc6..e930e223e8a 100644 --- a/runtime/binding-mcp-openapi/src/main/java/io/aklivity/zilla/runtime/binding/mcp/openapi/internal/config/composite/McpOpenapiCompositeGenerator.java +++ b/runtime/binding-mcp-openapi/src/main/java/io/aklivity/zilla/runtime/binding/mcp/openapi/internal/config/composite/McpOpenapiCompositeGenerator.java @@ -1067,12 +1067,13 @@ private static OpenapiResponseView successResponse( return result; } - // MCP's tool outputSchema (and the structuredContent it describes) must be a JSON object; an OpenAPI - // response whose own schema is array- or primitive-typed is wrapped as {"result": } instead - // of advertised as-is -- see wrapAsObjectSchema and McpHttpResultWrap, which wraps the real response - // body the same way so structuredContent still matches the advertised schema. A resource's own output - // schema has no such constraint (it is never advertised, only used to project the read body), so this - // check and the wrapping it drives applies to tools only + // MCP's structuredContent field must always be a JSON object on the wire, regardless of whether the + // OpenAPI operation declares a response schema at all -- an undeclared-schema response can still be a + // bare array or scalar at runtime, so a response whose own declared (or entirely absent) schema is not + // object-typed is wrapped as {"result": } instead of passed through as-is -- see wrapAsObjectSchema + // and McpHttpResultWrap, which wraps the real response body the same way so structuredContent still + // conforms. A resource's own output schema has no such constraint (it is never advertised, only used to + // project the read body), so this check and the wrapping it drives applies to tools only private static boolean hasObjectOutputSchema( OpenapiOperationView operation) { diff --git a/runtime/binding-mcp-openapi/src/test/java/io/aklivity/zilla/runtime/binding/mcp/openapi/internal/config/composite/McpOpenapiCompositeGeneratorTest.java b/runtime/binding-mcp-openapi/src/test/java/io/aklivity/zilla/runtime/binding/mcp/openapi/internal/config/composite/McpOpenapiCompositeGeneratorTest.java index 6c944d53dd3..e6bb5026a97 100644 --- a/runtime/binding-mcp-openapi/src/test/java/io/aklivity/zilla/runtime/binding/mcp/openapi/internal/config/composite/McpOpenapiCompositeGeneratorTest.java +++ b/runtime/binding-mcp-openapi/src/test/java/io/aklivity/zilla/runtime/binding/mcp/openapi/internal/config/composite/McpOpenapiCompositeGeneratorTest.java @@ -306,6 +306,15 @@ public class McpOpenapiCompositeGeneratorTest "items": { "type": "object", "properties": { "id": { "type": "integer" }, "name": { "type": "string" } } } } } } } } } + }, + "/pets/{id}": { + "delete": { + "operationId": "delete_pet", + "parameters": [ + { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } } + ], + "responses": { "200": { "description": "ok" } } + } } } } @@ -3011,6 +3020,56 @@ public void shouldNotWrapExplicitOutputOverrideEvenForArrayTypedResponse() assertThat(tool.outputMaybeWrapped, equalTo(false)); } + @Test + public void shouldWrapWhenNoOutputSchemaDeclared() + { + lenient().when(catalog.resolve(eq("petstore-api"), eq("latest"))).thenReturn(66); + lenient().when(catalog.resolve(eq(66))).thenReturn(PETSTORE_SPEC); + + BindingConfig binding = GenericBindingConfig.builder() + .namespace("test") + .name("mcp-openapi0") + .type("mcp-openapi") + .kind(CLIENT) + .options(McpOpenapiOptionsConfig.builder() + .spec() + .label("petstore") + .server("https://api.petstore.example.com") + .catalog() + .name("catalog0") + .subject("petstore-api") + .version("latest") + .build() + .build() + .build()) + .route() + .when(McpOpenapiConditionConfig.builder() + .tool("delete_pet") + .build()) + .with(McpOpenapiWithConfig.builder() + .spec("petstore") + .operation("delete_pet") + .build()) + .build() + .build(); + binding.resolveId = resolveId; + + McpOpenapiCompositeConfig composite = generator.generate(new McpOpenapiBindingConfig(context, binding)); + + BindingConfig mcpHttp = composite.namespaces.get(0).bindings.stream() + .filter(b -> "mcp-http0".equals(b.name)) + .findFirst() + .orElse(null); + McpHttpOptionsConfig mcpHttpOptions = (McpHttpOptionsConfig) mcpHttp.options; + McpHttpToolConfig tool = mcpHttpOptions.tools.stream() + .filter(t -> "delete_pet".equals(t.name)) + .findFirst() + .orElse(null); + + assertThat(tool, notNullValue()); + assertThat(tool.outputMaybeWrapped, equalTo(true)); + } + @Test public void shouldDeriveToolAnnotationsFromHttpMethod() { diff --git a/runtime/binding-mcp-schema-registry/src/main/resources/io/aklivity/zilla/runtime/binding/mcp/schema/registry/internal/schema/karapace-schema-registry.openapi.json b/runtime/binding-mcp-schema-registry/src/main/resources/io/aklivity/zilla/runtime/binding/mcp/schema/registry/internal/schema/karapace-schema-registry.openapi.json index bc58e092e03..b25223f0d49 100644 --- a/runtime/binding-mcp-schema-registry/src/main/resources/io/aklivity/zilla/runtime/binding/mcp/schema/registry/internal/schema/karapace-schema-registry.openapi.json +++ b/runtime/binding-mcp-schema-registry/src/main/resources/io/aklivity/zilla/runtime/binding/mcp/schema/registry/internal/schema/karapace-schema-registry.openapi.json @@ -1 +1 @@ -{"openapi":"3.0.1","info":{"title":"Schema Registry API","version":"1.0"},"servers":[{"url":"http://localhost:8080"}],"paths":{"/subjects":{"get":{"operationId":"list_subjects","description":"List all registered subjects in the schema registry.","summary":"List subjects","responses":{"200":{"description":"ok"}}}},"/subjects/{subject}/versions":{"get":{"operationId":"describe_subject","description":"List the schema version numbers registered for a subject.","summary":"List versions for a subject","parameters":[{"name":"subject","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"ok"}}},"post":{"operationId":"register_schema","description":"Register a new schema version under a subject.","summary":"Registered schema with id ${result.id}","parameters":[{"name":"subject","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"schema":{"type":"string"},"schemaType":{"type":"string"}}}}}},"responses":{"200":{"description":"ok"}}}},"/subjects/{subject}/versions/{version}":{"get":{"operationId":"get_schema","description":"Retrieve a specific registered schema version for a subject.","summary":"Retrieved schema id ${result.id}, version ${result.version}","parameters":[{"name":"subject","in":"path","required":true,"schema":{"type":"string"}},{"name":"version","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"ok"}}},"delete":{"operationId":"delete_schema_version","description":"Delete a specific registered schema version for a subject.","summary":"Delete a specific schema version","parameters":[{"name":"subject","in":"path","required":true,"schema":{"type":"string"}},{"name":"version","in":"path","required":true,"schema":{"type":"string"}},{"name":"permanent","in":"query","required":false,"schema":{"type":"boolean"}}],"responses":{"200":{"description":"ok"}}}},"/subjects/{subject}":{"delete":{"operationId":"delete_subject","description":"Delete a subject and all its registered schema versions.","summary":"Delete a subject and all its versions","parameters":[{"name":"subject","in":"path","required":true,"schema":{"type":"string"}},{"name":"permanent","in":"query","required":false,"schema":{"type":"boolean"}}],"responses":{"200":{"description":"ok"}}}},"/compatibility/subjects/{subject}/versions/{version}":{"post":{"operationId":"check_compatibility","description":"Check whether a given schema is compatible with the existing versions registered for a subject.","summary":"Compatibility check result: ${result.is_compatible}","parameters":[{"name":"subject","in":"path","required":true,"schema":{"type":"string"}},{"name":"version","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"schema":{"type":"string"},"schemaType":{"type":"string"}}}}}},"responses":{"200":{"description":"ok"}}}},"/config/{subject}":{"get":{"operationId":"get_compatibility","description":"Get the compatibility level configured for a subject.","summary":"Compatibility level is ${result.compatibilityLevel}","parameters":[{"name":"subject","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"ok"}}},"put":{"operationId":"set_compatibility","description":"Set the compatibility level for a subject.","summary":"Compatibility level set to ${result.compatibility}","parameters":[{"name":"subject","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"compatibility":{"type":"string"}}}}}},"responses":{"200":{"description":"ok"}}}}}} \ No newline at end of file +{"openapi":"3.0.1","info":{"title":"Schema Registry API","version":"1.0"},"servers":[{"url":"http://localhost:8080"}],"paths":{"/subjects":{"get":{"operationId":"list_subjects","description":"List all registered subjects in the schema registry.","summary":"Subjects: ${result}","responses":{"200":{"description":"ok"}}}},"/subjects/{subject}/versions":{"get":{"operationId":"describe_subject","description":"List the schema version numbers registered for a subject.","summary":"Versions: ${result}","parameters":[{"name":"subject","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"ok"}}},"post":{"operationId":"register_schema","description":"Register a new schema version under a subject.","summary":"Registered schema with id ${result.id}","parameters":[{"name":"subject","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"schema":{"type":"string"},"schemaType":{"type":"string"}}}}}},"responses":{"200":{"description":"ok"}}}},"/subjects/{subject}/versions/{version}":{"get":{"operationId":"get_schema","description":"Retrieve a specific registered schema version for a subject.","summary":"Retrieved schema id ${result.id}, version ${result.version}","parameters":[{"name":"subject","in":"path","required":true,"schema":{"type":"string"}},{"name":"version","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"ok"}}},"delete":{"operationId":"delete_schema_version","description":"Delete a specific registered schema version for a subject.","summary":"Deleted version ${result}","parameters":[{"name":"subject","in":"path","required":true,"schema":{"type":"string"}},{"name":"version","in":"path","required":true,"schema":{"type":"string"}},{"name":"permanent","in":"query","required":false,"schema":{"type":"boolean"}}],"responses":{"200":{"description":"ok"}}}},"/subjects/{subject}":{"delete":{"operationId":"delete_subject","description":"Delete a subject and all its registered schema versions.","summary":"Deleted versions: ${result}","parameters":[{"name":"subject","in":"path","required":true,"schema":{"type":"string"}},{"name":"permanent","in":"query","required":false,"schema":{"type":"boolean"}}],"responses":{"200":{"description":"ok"}}}},"/compatibility/subjects/{subject}/versions/{version}":{"post":{"operationId":"check_compatibility","description":"Check whether a given schema is compatible with the existing versions registered for a subject.","summary":"Compatibility check result: ${result.is_compatible}","parameters":[{"name":"subject","in":"path","required":true,"schema":{"type":"string"}},{"name":"version","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"schema":{"type":"string"},"schemaType":{"type":"string"}}}}}},"responses":{"200":{"description":"ok"}}}},"/config/{subject}":{"get":{"operationId":"get_compatibility","description":"Get the compatibility level configured for a subject.","summary":"Compatibility level is ${result.compatibilityLevel}","parameters":[{"name":"subject","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"ok"}}},"put":{"operationId":"set_compatibility","description":"Set the compatibility level for a subject.","summary":"Compatibility level set to ${result.compatibility}","parameters":[{"name":"subject","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"compatibility":{"type":"string"}}}}}},"responses":{"200":{"description":"ok"}}}}}} \ No newline at end of file diff --git a/specs/binding-mcp-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/http/config/client.yaml b/specs/binding-mcp-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/http/config/client.yaml index 0e4b356c146..fed0538b217 100644 --- a/specs/binding-mcp-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/http/config/client.yaml +++ b/specs/binding-mcp-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/http/config/client.yaml @@ -205,6 +205,20 @@ catalogs: { "type": "integer" } + get_status_params: + version: latest + schema: | + { + "type": "object", + "properties": {} + } + get_dashboard_params: + version: latest + schema: | + { + "type": "object", + "properties": {} + } echo_id_params: version: latest schema: | @@ -372,7 +386,7 @@ bindings: list_tags: description: > List repository tags. - summary: "done" + summary: "Tags: ${result}" schemas: input: model: json @@ -403,6 +417,31 @@ bindings: http_github0: - subject: count_items_result version: latest + get_status: + description: > + Get service status. A bare-scalar response with no declared output schema, covering + tool.summary's bare ${result} reference (the response's own root value, with no path). + summary: "Status: ${result}" + schemas: + input: + model: json + catalog: + http_github0: + - subject: get_status_params + version: latest + get_dashboard: + description: > + Get a status dashboard. A bare-object response with no declared output schema, covering + tool.summary's container capture: ${result.data} re-serializes a whole nested object as + compact JSON, alongside bare scalar/null/boolean leaf references. + summary: "Dashboard: ${result.data} active=${result.active} disabled=${result.disabled} flag=${result.flag}" + schemas: + input: + model: json + catalog: + http_github0: + - subject: get_dashboard_params + version: latest echo_id: description: > Echo the given identifier. @@ -573,6 +612,22 @@ bindings: ":scheme": https ":authority": api.github.com:443 ":path": /count + - when: + - tool: get_status + with: + headers: + ":method": GET + ":scheme": https + ":authority": api.github.com:443 + ":path": /status + - when: + - tool: get_dashboard + with: + headers: + ":method": GET + ":scheme": https + ":authority": api.github.com:443 + ":path": /dashboard - when: - tool: echo_id with: diff --git a/specs/binding-mcp-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/http/config/proxy.yaml b/specs/binding-mcp-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/http/config/proxy.yaml index bc4403404c7..8c605217fe2 100644 --- a/specs/binding-mcp-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/http/config/proxy.yaml +++ b/specs/binding-mcp-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/http/config/proxy.yaml @@ -205,6 +205,20 @@ catalogs: { "type": "integer" } + get_status_params: + version: latest + schema: | + { + "type": "object", + "properties": {} + } + get_dashboard_params: + version: latest + schema: | + { + "type": "object", + "properties": {} + } echo_id_params: version: latest schema: | @@ -372,7 +386,7 @@ bindings: list_tags: description: > List repository tags. - summary: "done" + summary: "Tags: ${result}" schemas: input: model: json @@ -403,6 +417,31 @@ bindings: http_github0: - subject: count_items_result version: latest + get_status: + description: > + Get service status. A bare-scalar response with no declared output schema, covering + tool.summary's bare ${result} reference (the response's own root value, with no path). + summary: "Status: ${result}" + schemas: + input: + model: json + catalog: + http_github0: + - subject: get_status_params + version: latest + get_dashboard: + description: > + Get a status dashboard. A bare-object response with no declared output schema, covering + tool.summary's container capture: ${result.data} re-serializes a whole nested object as + compact JSON, alongside bare scalar/null/boolean leaf references. + summary: "Dashboard: ${result.data} active=${result.active} disabled=${result.disabled} flag=${result.flag}" + schemas: + input: + model: json + catalog: + http_github0: + - subject: get_dashboard_params + version: latest echo_id: description: > Echo the given identifier. @@ -582,6 +621,24 @@ bindings: ":scheme": https ":authority": api.github.com:443 ":path": /count + - when: + - tool: get_status + exit: http0 + with: + headers: + ":method": GET + ":scheme": https + ":authority": api.github.com:443 + ":path": /status + - when: + - tool: get_dashboard + exit: http0 + with: + headers: + ":method": GET + ":scheme": https + ":authority": api.github.com:443 + ":path": /dashboard - when: - tool: echo_id exit: http0 diff --git a/specs/binding-mcp-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/http/streams/http/get.dashboard/client.rpt b/specs/binding-mcp-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/http/streams/http/get.dashboard/client.rpt new file mode 100644 index 00000000000..fc5d79e3d6c --- /dev/null +++ b/specs/binding-mcp-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/http/streams/http/get.dashboard/client.rpt @@ -0,0 +1,40 @@ +# +# Copyright 2021-2026 Aklivity Inc +# +# Licensed under the Aklivity Community License (the "License"); you may not use +# this file except in compliance with the License. You may obtain a copy of the +# License at +# +# https://www.aklivity.io/aklivity-community-license/ +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. +# + +connect "zilla://streams/http0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${http:beginEx() + .typeId(zilla:id("http")) + .header(":method", "GET") + .header(":scheme", "https") + .header(":authority", "api.github.com:443") + .header(":path", "/dashboard") + .build()} + +connected + +read zilla:begin.ext ${http:matchBeginEx() + .typeId(zilla:id("http")) + .header(":status", "200") + .header("content-type", "application/json") + .build()} + +read '{"active":true,"disabled":false,"flag":null,"data":{"name":"Ops \\"Team\\"",' + '"count":3,"verified":true,"archived":false,"note":null,"tags":["a","b"]}}' +read closed + +write close diff --git a/specs/binding-mcp-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/http/streams/http/get.dashboard/server.rpt b/specs/binding-mcp-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/http/streams/http/get.dashboard/server.rpt new file mode 100644 index 00000000000..e7973d941f3 --- /dev/null +++ b/specs/binding-mcp-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/http/streams/http/get.dashboard/server.rpt @@ -0,0 +1,47 @@ +# +# Copyright 2021-2026 Aklivity Inc +# +# Licensed under the Aklivity Community License (the "License"); you may not use +# this file except in compliance with the License. You may obtain a copy of the +# License at +# +# https://www.aklivity.io/aklivity-community-license/ +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. +# + +property serverAddress "zilla://streams/http0" + +accept ${serverAddress} + option zilla:window 8192 + option zilla:transmission "half-duplex" + +accepted + +read zilla:begin.ext ${http:matchBeginEx() + .typeId(zilla:id("http")) + .header(":method", "GET") + .header(":scheme", "https") + .header(":authority", "api.github.com:443") + .header(":path", "/dashboard") + .build()} + +connected + +write zilla:begin.ext ${http:beginEx() + .typeId(zilla:id("http")) + .header(":status", "200") + .header("content-type", "application/json") + .build()} +write flush + +write '{"active":true,"disabled":false,"flag":null,"data":{"name":"Ops \\"Team\\"",' + '"count":3,"verified":true,"archived":false,"note":null,"tags":["a","b"]}}' +write flush + +write close + +read closed diff --git a/specs/binding-mcp-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/http/streams/http/get.status/client.rpt b/specs/binding-mcp-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/http/streams/http/get.status/client.rpt new file mode 100644 index 00000000000..16c7c0d5af9 --- /dev/null +++ b/specs/binding-mcp-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/http/streams/http/get.status/client.rpt @@ -0,0 +1,39 @@ +# +# Copyright 2021-2026 Aklivity Inc +# +# Licensed under the Aklivity Community License (the "License"); you may not use +# this file except in compliance with the License. You may obtain a copy of the +# License at +# +# https://www.aklivity.io/aklivity-community-license/ +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. +# + +connect "zilla://streams/http0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${http:beginEx() + .typeId(zilla:id("http")) + .header(":method", "GET") + .header(":scheme", "https") + .header(":authority", "api.github.com:443") + .header(":path", "/status") + .build()} + +connected + +read zilla:begin.ext ${http:matchBeginEx() + .typeId(zilla:id("http")) + .header(":status", "200") + .header("content-type", "application/json") + .build()} + +read '"healthy"' +read closed + +write close diff --git a/specs/binding-mcp-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/http/streams/http/get.status/server.rpt b/specs/binding-mcp-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/http/streams/http/get.status/server.rpt new file mode 100644 index 00000000000..0a4bfbddc4d --- /dev/null +++ b/specs/binding-mcp-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/http/streams/http/get.status/server.rpt @@ -0,0 +1,46 @@ +# +# Copyright 2021-2026 Aklivity Inc +# +# Licensed under the Aklivity Community License (the "License"); you may not use +# this file except in compliance with the License. You may obtain a copy of the +# License at +# +# https://www.aklivity.io/aklivity-community-license/ +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. +# + +property serverAddress "zilla://streams/http0" + +accept ${serverAddress} + option zilla:window 8192 + option zilla:transmission "half-duplex" + +accepted + +read zilla:begin.ext ${http:matchBeginEx() + .typeId(zilla:id("http")) + .header(":method", "GET") + .header(":scheme", "https") + .header(":authority", "api.github.com:443") + .header(":path", "/status") + .build()} + +connected + +write zilla:begin.ext ${http:beginEx() + .typeId(zilla:id("http")) + .header(":status", "200") + .header("content-type", "application/json") + .build()} +write flush + +write '"healthy"' +write flush + +write close + +read closed diff --git a/specs/binding-mcp-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/http/streams/mcp/get.dashboard/client.rpt b/specs/binding-mcp-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/http/streams/mcp/get.dashboard/client.rpt new file mode 100644 index 00000000000..9dfc6925d22 --- /dev/null +++ b/specs/binding-mcp-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/http/streams/mcp/get.dashboard/client.rpt @@ -0,0 +1,64 @@ +# +# Copyright 2021-2026 Aklivity Inc +# +# Licensed under the Aklivity Community License (the "License"); you may not use +# this file except in compliance with the License. You may obtain a copy of the +# License at +# +# https://www.aklivity.io/aklivity-community-license/ +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. +# + +connect "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .build() + .build()} + +connected + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("5ca1ab1e-c0de-4a11-5e55-000100000000") + .build() + .build()} + +read notify LIFECYCLE_INITIALIZED + +connect await LIFECYCLE_INITIALIZED + "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .toolsCall() + .sessionId("5ca1ab1e-c0de-4a11-5e55-000100000000") + .name("get_dashboard") + .contentLength(39) + .build() + .build()} + +connected + +write '{"name":"get_dashboard","arguments":{}}' + +# tool has no output schema, so the raw upstream body streams through unprojected. tool.summary +# references ${result.data} (an object nested under a key, re-serialized as compact JSON by +# McpHttpResults' container capture, including a quote character that must round-trip through +# JSON escaping) alongside three top-level scalar leaf references -- ${result.active} (true), +# ${result.disabled} (false), and ${result.flag} (null) -- covering every scalar/container +# combination captureContainer and the leaf VALUE_TRUE/VALUE_FALSE/VALUE_NULL branches handle +read '{"structuredContent":{"active":true,"disabled":false,"flag":null,"data":{"name":"Ops \\"Team\\"","count":3,"verified":true,"archived":false,"note":null,"tags":["a","b"]}},"content":[{"type":"text","text":"Dashboard: {\\"name\\":\\"Ops \\\\\\"Team\\\\\\"\\",\\"count\\":3,\\"verified\\":true,\\"archived\\":false,\\"note\\":null,\\"tags\\":[\\"a\\",\\"b\\"]} active=true disabled=false flag=null"}],"isError":false}' +read closed + +write close diff --git a/specs/binding-mcp-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/http/streams/mcp/get.dashboard/server.rpt b/specs/binding-mcp-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/http/streams/mcp/get.dashboard/server.rpt new file mode 100644 index 00000000000..f4d7de1292d --- /dev/null +++ b/specs/binding-mcp-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/http/streams/mcp/get.dashboard/server.rpt @@ -0,0 +1,62 @@ +# +# Copyright 2021-2026 Aklivity Inc +# +# Licensed under the Aklivity Community License (the "License"); you may not use +# this file except in compliance with the License. You may obtain a copy of the +# License at +# +# https://www.aklivity.io/aklivity-community-license/ +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. +# + +property serverAddress "zilla://streams/app0" + +accept ${serverAddress} + option zilla:window 8192 + option zilla:transmission "half-duplex" + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .build() + .build()} + +connected + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("5ca1ab1e-c0de-4a11-5e55-000100000000") + .build() + .build()} +write flush + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .toolsCall() + .sessionId("5ca1ab1e-c0de-4a11-5e55-000100000000") + .name("get_dashboard") + .contentLength(39) + .build() + .build()} + +connected + +read '{"name":"get_dashboard","arguments":{}}' + +write flush + +write '{"structuredContent":{"active":true,"disabled":false,"flag":null,"data":{"name":"Ops \\"Team\\"","count":3,"verified":true,"archived":false,"note":null,"tags":["a","b"]}},"content":[{"type":"text","text":"Dashboard: {\\"name\\":\\"Ops \\\\\\"Team\\\\\\"\\",\\"count\\":3,\\"verified\\":true,\\"archived\\":false,\\"note\\":null,\\"tags\\":[\\"a\\",\\"b\\"]} active=true disabled=false flag=null"}],"isError":false}' +write flush + +write close + +read closed diff --git a/specs/binding-mcp-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/http/streams/mcp/get.status/client.rpt b/specs/binding-mcp-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/http/streams/mcp/get.status/client.rpt new file mode 100644 index 00000000000..ab8221be35b --- /dev/null +++ b/specs/binding-mcp-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/http/streams/mcp/get.status/client.rpt @@ -0,0 +1,65 @@ +# +# Copyright 2021-2026 Aklivity Inc +# +# Licensed under the Aklivity Community License (the "License"); you may not use +# this file except in compliance with the License. You may obtain a copy of the +# License at +# +# https://www.aklivity.io/aklivity-community-license/ +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. +# + +connect "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .build() + .build()} + +connected + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("5ca1ab1e-c0de-4a11-5e55-000100000000") + .build() + .build()} + +read notify LIFECYCLE_INITIALIZED + +connect await LIFECYCLE_INITIALIZED + "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .toolsCall() + .sessionId("5ca1ab1e-c0de-4a11-5e55-000100000000") + .name("get_status") + .contentLength(36) + .build() + .build()} + +connected + +write '{"name":"get_status","arguments":{}}' + +# tool has no output schema, so the raw upstream body (a bare JSON string, not an object) streams +# through unprojected; tool.summary's bare ${result} reference (no path) binds to that whole root +# scalar value, proving the reference works with no wrapping key to match against +read '{' + '"structuredContent":"healthy",' + '"content":[{"type":"text","text":"Status: healthy"}],' + '"isError":false' + '}' +read closed + +write close diff --git a/specs/binding-mcp-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/http/streams/mcp/get.status/server.rpt b/specs/binding-mcp-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/http/streams/mcp/get.status/server.rpt new file mode 100644 index 00000000000..2ec2721022f --- /dev/null +++ b/specs/binding-mcp-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/http/streams/mcp/get.status/server.rpt @@ -0,0 +1,66 @@ +# +# Copyright 2021-2026 Aklivity Inc +# +# Licensed under the Aklivity Community License (the "License"); you may not use +# this file except in compliance with the License. You may obtain a copy of the +# License at +# +# https://www.aklivity.io/aklivity-community-license/ +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. +# + +property serverAddress "zilla://streams/app0" + +accept ${serverAddress} + option zilla:window 8192 + option zilla:transmission "half-duplex" + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .build() + .build()} + +connected + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("5ca1ab1e-c0de-4a11-5e55-000100000000") + .build() + .build()} +write flush + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .toolsCall() + .sessionId("5ca1ab1e-c0de-4a11-5e55-000100000000") + .name("get_status") + .contentLength(36) + .build() + .build()} + +connected + +read '{"name":"get_status","arguments":{}}' + +write flush + +write '{' + '"structuredContent":"healthy",' + '"content":[{"type":"text","text":"Status: healthy"}],' + '"isError":false' + '}' +write flush + +write close + +read closed diff --git a/specs/binding-mcp-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/http/streams/mcp/list.tags/client.rpt b/specs/binding-mcp-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/http/streams/mcp/list.tags/client.rpt index c1d25631d72..7ba8649f417 100644 --- a/specs/binding-mcp-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/http/streams/mcp/list.tags/client.rpt +++ b/specs/binding-mcp-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/http/streams/mcp/list.tags/client.rpt @@ -53,10 +53,12 @@ connected write '{"name":"list_tags","arguments":{}}' # tool.output (list_tags_result) is an array-rooted schema: structuredContent is the array itself, not -# an object wrapping it — proves McpHttpToolResult's array-root envelope step +# an object wrapping it — proves McpHttpToolResult's array-root envelope step. tool.summary's bare +# ${result} (no path) captures that same root array, re-serialized as compact JSON, proving +# McpHttpResults' container capture alongside its existing scalar-only capture read '{' '"structuredContent":["v1","v2","v3"],' - '"content":[{"type":"text","text":"done"}],' + '"content":[{"type":"text","text":"Tags: [\\"v1\\",\\"v2\\",\\"v3\\"]"}],' '"isError":false' '}' read closed diff --git a/specs/binding-mcp-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/http/streams/mcp/list.tags/server.rpt b/specs/binding-mcp-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/http/streams/mcp/list.tags/server.rpt index 9f46ca514f8..fa87c22a777 100644 --- a/specs/binding-mcp-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/http/streams/mcp/list.tags/server.rpt +++ b/specs/binding-mcp-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/http/streams/mcp/list.tags/server.rpt @@ -56,7 +56,7 @@ write flush write '{' '"structuredContent":["v1","v2","v3"],' - '"content":[{"type":"text","text":"done"}],' + '"content":[{"type":"text","text":"Tags: [\\"v1\\",\\"v2\\",\\"v3\\"]"}],' '"isError":false' '}' write flush diff --git a/specs/binding-mcp-http.spec/src/test/java/io/aklivity/zilla/specs/binding/mcp/http/streams/HttpClientIT.java b/specs/binding-mcp-http.spec/src/test/java/io/aklivity/zilla/specs/binding/mcp/http/streams/HttpClientIT.java index 0af3d461b8d..36e66630dd8 100644 --- a/specs/binding-mcp-http.spec/src/test/java/io/aklivity/zilla/specs/binding/mcp/http/streams/HttpClientIT.java +++ b/specs/binding-mcp-http.spec/src/test/java/io/aklivity/zilla/specs/binding/mcp/http/streams/HttpClientIT.java @@ -225,6 +225,24 @@ public void shouldProxyCountItemsToHttp() throws Exception k3po.finish(); } + @Test + @Specification({ + "${http}/get.status/client", + "${http}/get.status/server"}) + public void shouldProxyGetStatusToHttp() throws Exception + { + k3po.finish(); + } + + @Test + @Specification({ + "${http}/get.dashboard/client", + "${http}/get.dashboard/server"}) + public void shouldProxyGetDashboardToHttp() throws Exception + { + k3po.finish(); + } + @Test @Specification({ "${http}/echo.id.large/client", diff --git a/specs/binding-mcp-http.spec/src/test/java/io/aklivity/zilla/specs/binding/mcp/http/streams/McpServerIT.java b/specs/binding-mcp-http.spec/src/test/java/io/aklivity/zilla/specs/binding/mcp/http/streams/McpServerIT.java index 22c12da3029..476b495f706 100644 --- a/specs/binding-mcp-http.spec/src/test/java/io/aklivity/zilla/specs/binding/mcp/http/streams/McpServerIT.java +++ b/specs/binding-mcp-http.spec/src/test/java/io/aklivity/zilla/specs/binding/mcp/http/streams/McpServerIT.java @@ -315,6 +315,24 @@ public void shouldCallToolCountItems() throws Exception k3po.finish(); } + @Test + @Specification({ + "${mcp}/get.status/client", + "${mcp}/get.status/server"}) + public void shouldCallToolGetStatus() throws Exception + { + k3po.finish(); + } + + @Test + @Specification({ + "${mcp}/get.dashboard/client", + "${mcp}/get.dashboard/server"}) + public void shouldCallToolGetDashboard() throws Exception + { + k3po.finish(); + } + @Test @Specification({ "${mcp}/echo.id.large/client", diff --git a/specs/binding-mcp-kafka-connect.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/kafka/connect/streams/mcp/list.connector.plugins/client.rpt b/specs/binding-mcp-kafka-connect.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/kafka/connect/streams/mcp/list.connector.plugins/client.rpt index 03455802cf4..df078ad1bd6 100644 --- a/specs/binding-mcp-kafka-connect.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/kafka/connect/streams/mcp/list.connector.plugins/client.rpt +++ b/specs/binding-mcp-kafka-connect.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/kafka/connect/streams/mcp/list.connector.plugins/client.rpt @@ -52,7 +52,7 @@ connected write '{"name":"list_connector_plugins","arguments":{}}' -read '{"structuredContent":{"result":[{"class":"org.apache.kafka.connect.file.FileStreamSourceConnector","type":"source","version":"1.0"}]},"content":[{"type":"text","text":"Listed connector plugins"}],"isError":false}' +read '{"structuredContent":{"result":[{"class":"org.apache.kafka.connect.file.FileStreamSourceConnector","type":"source","version":"1.0"}]},"content":[{"type":"text","text":"Plugins: [{\\"class\\":\\"org.apache.kafka.connect.file.FileStreamSourceConnector\\",\\"type\\":\\"source\\",\\"version\\":\\"1.0\\"}]"}],"isError":false}' read closed write close diff --git a/specs/binding-mcp-kafka-connect.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/kafka/connect/streams/mcp/list.connector.plugins/server.rpt b/specs/binding-mcp-kafka-connect.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/kafka/connect/streams/mcp/list.connector.plugins/server.rpt index 6fe3179fec6..9d9a5b0b88f 100644 --- a/specs/binding-mcp-kafka-connect.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/kafka/connect/streams/mcp/list.connector.plugins/server.rpt +++ b/specs/binding-mcp-kafka-connect.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/kafka/connect/streams/mcp/list.connector.plugins/server.rpt @@ -54,7 +54,7 @@ read '{"name":"list_connector_plugins","arguments":{}}' write flush -write '{"structuredContent":{"result":[{"class":"org.apache.kafka.connect.file.FileStreamSourceConnector","type":"source","version":"1.0"}]},"content":[{"type":"text","text":"Listed connector plugins"}],"isError":false}' +write '{"structuredContent":{"result":[{"class":"org.apache.kafka.connect.file.FileStreamSourceConnector","type":"source","version":"1.0"}]},"content":[{"type":"text","text":"Plugins: [{\\"class\\":\\"org.apache.kafka.connect.file.FileStreamSourceConnector\\",\\"type\\":\\"source\\",\\"version\\":\\"1.0\\"}]"}],"isError":false}' write flush write close diff --git a/specs/binding-mcp-kafka-connect.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/kafka/connect/streams/mcp/list.connector.tasks/client.rpt b/specs/binding-mcp-kafka-connect.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/kafka/connect/streams/mcp/list.connector.tasks/client.rpt index eb2d1f7572d..91cd3044ad4 100644 --- a/specs/binding-mcp-kafka-connect.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/kafka/connect/streams/mcp/list.connector.tasks/client.rpt +++ b/specs/binding-mcp-kafka-connect.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/kafka/connect/streams/mcp/list.connector.tasks/client.rpt @@ -52,7 +52,7 @@ connected write '{"name":"list_connector_tasks","arguments":{"connector":"connector1"}}' -read '{"structuredContent":{"result":[{"id":{"connector":"connector1","task":0},"config":{"task.class":"FileStreamSourceTask"}}]},"content":[{"type":"text","text":"Listed connector tasks"}],"isError":false}' +read '{"structuredContent":{"result":[{"id":{"connector":"connector1","task":0},"config":{"task.class":"FileStreamSourceTask"}}]},"content":[{"type":"text","text":"Tasks: [{\\"id\\":{\\"connector\\":\\"connector1\\",\\"task\\":0},\\"config\\":{\\"task.class\\":\\"FileStreamSourceTask\\"}}]"}],"isError":false}' read closed write close diff --git a/specs/binding-mcp-kafka-connect.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/kafka/connect/streams/mcp/list.connector.tasks/server.rpt b/specs/binding-mcp-kafka-connect.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/kafka/connect/streams/mcp/list.connector.tasks/server.rpt index 676a40a394e..439a71558e4 100644 --- a/specs/binding-mcp-kafka-connect.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/kafka/connect/streams/mcp/list.connector.tasks/server.rpt +++ b/specs/binding-mcp-kafka-connect.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/kafka/connect/streams/mcp/list.connector.tasks/server.rpt @@ -54,7 +54,7 @@ read '{"name":"list_connector_tasks","arguments":{"connector":"connector1"}}' write flush -write '{"structuredContent":{"result":[{"id":{"connector":"connector1","task":0},"config":{"task.class":"FileStreamSourceTask"}}]},"content":[{"type":"text","text":"Listed connector tasks"}],"isError":false}' +write '{"structuredContent":{"result":[{"id":{"connector":"connector1","task":0},"config":{"task.class":"FileStreamSourceTask"}}]},"content":[{"type":"text","text":"Tasks: [{\\"id\\":{\\"connector\\":\\"connector1\\",\\"task\\":0},\\"config\\":{\\"task.class\\":\\"FileStreamSourceTask\\"}}]"}],"isError":false}' write flush write close diff --git a/specs/binding-mcp-kafka-connect.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/kafka/connect/streams/mcp/list.connectors/client.rpt b/specs/binding-mcp-kafka-connect.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/kafka/connect/streams/mcp/list.connectors/client.rpt index 73fa6ff803e..5a5b8b949c6 100644 --- a/specs/binding-mcp-kafka-connect.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/kafka/connect/streams/mcp/list.connectors/client.rpt +++ b/specs/binding-mcp-kafka-connect.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/kafka/connect/streams/mcp/list.connectors/client.rpt @@ -52,7 +52,7 @@ connected write '{"name":"list_connectors","arguments":{}}' -read '{"structuredContent":{"result":["connector1","connector2"]},"content":[{"type":"text","text":"Listed connectors"}],"isError":false}' +read '{"structuredContent":{"result":["connector1","connector2"]},"content":[{"type":"text","text":"Connectors: [\\"connector1\\",\\"connector2\\"]"}],"isError":false}' read closed write close diff --git a/specs/binding-mcp-kafka-connect.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/kafka/connect/streams/mcp/list.connectors/server.rpt b/specs/binding-mcp-kafka-connect.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/kafka/connect/streams/mcp/list.connectors/server.rpt index 5f86ee7e5a2..404a2e346e0 100644 --- a/specs/binding-mcp-kafka-connect.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/kafka/connect/streams/mcp/list.connectors/server.rpt +++ b/specs/binding-mcp-kafka-connect.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/kafka/connect/streams/mcp/list.connectors/server.rpt @@ -54,7 +54,7 @@ read '{"name":"list_connectors","arguments":{}}' write flush -write '{"structuredContent":{"result":["connector1","connector2"]},"content":[{"type":"text","text":"Listed connectors"}],"isError":false}' +write '{"structuredContent":{"result":["connector1","connector2"]},"content":[{"type":"text","text":"Connectors: [\\"connector1\\",\\"connector2\\"]"}],"isError":false}' write flush write close diff --git a/specs/binding-mcp-schema-registry.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/schema/registry/streams/mcp/delete.schema.version.permanent/client.rpt b/specs/binding-mcp-schema-registry.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/schema/registry/streams/mcp/delete.schema.version.permanent/client.rpt index 96d4ed083bb..5718fd7d689 100644 --- a/specs/binding-mcp-schema-registry.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/schema/registry/streams/mcp/delete.schema.version.permanent/client.rpt +++ b/specs/binding-mcp-schema-registry.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/schema/registry/streams/mcp/delete.schema.version.permanent/client.rpt @@ -52,7 +52,7 @@ connected write '{"name":"delete_schema_version","arguments":{"subject":"orders-value","version":"1","permanent":true}}' -read '{"structuredContent":{"result":1},"content":[{"type":"text","text":"Delete a specific schema version"}],"isError":false}' +read '{"structuredContent":{"result":1},"content":[{"type":"text","text":"Deleted version 1"}],"isError":false}' read closed write close diff --git a/specs/binding-mcp-schema-registry.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/schema/registry/streams/mcp/delete.schema.version.permanent/server.rpt b/specs/binding-mcp-schema-registry.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/schema/registry/streams/mcp/delete.schema.version.permanent/server.rpt index 76b979e4e43..8aec7caffde 100644 --- a/specs/binding-mcp-schema-registry.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/schema/registry/streams/mcp/delete.schema.version.permanent/server.rpt +++ b/specs/binding-mcp-schema-registry.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/schema/registry/streams/mcp/delete.schema.version.permanent/server.rpt @@ -54,7 +54,7 @@ read '{"name":"delete_schema_version","arguments":{"subject":"orders-value","ve write flush -write '{"structuredContent":{"result":1},"content":[{"type":"text","text":"Delete a specific schema version"}],"isError":false}' +write '{"structuredContent":{"result":1},"content":[{"type":"text","text":"Deleted version 1"}],"isError":false}' write flush write close diff --git a/specs/binding-mcp-schema-registry.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/schema/registry/streams/mcp/delete.schema.version/client.rpt b/specs/binding-mcp-schema-registry.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/schema/registry/streams/mcp/delete.schema.version/client.rpt index 373dfdd82de..e87867e55ee 100644 --- a/specs/binding-mcp-schema-registry.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/schema/registry/streams/mcp/delete.schema.version/client.rpt +++ b/specs/binding-mcp-schema-registry.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/schema/registry/streams/mcp/delete.schema.version/client.rpt @@ -52,7 +52,7 @@ connected write '{"name":"delete_schema_version","arguments":{"subject":"orders-value","version":"1"}}' -read '{"structuredContent":{"result":1},"content":[{"type":"text","text":"Delete a specific schema version"}],"isError":false}' +read '{"structuredContent":{"result":1},"content":[{"type":"text","text":"Deleted version 1"}],"isError":false}' read closed write close diff --git a/specs/binding-mcp-schema-registry.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/schema/registry/streams/mcp/delete.schema.version/server.rpt b/specs/binding-mcp-schema-registry.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/schema/registry/streams/mcp/delete.schema.version/server.rpt index 2e81a1af9db..1a3a92ca872 100644 --- a/specs/binding-mcp-schema-registry.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/schema/registry/streams/mcp/delete.schema.version/server.rpt +++ b/specs/binding-mcp-schema-registry.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/schema/registry/streams/mcp/delete.schema.version/server.rpt @@ -54,7 +54,7 @@ read '{"name":"delete_schema_version","arguments":{"subject":"orders-value","ve write flush -write '{"structuredContent":{"result":1},"content":[{"type":"text","text":"Delete a specific schema version"}],"isError":false}' +write '{"structuredContent":{"result":1},"content":[{"type":"text","text":"Deleted version 1"}],"isError":false}' write flush write close diff --git a/specs/binding-mcp-schema-registry.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/schema/registry/streams/mcp/delete.subject.permanent/client.rpt b/specs/binding-mcp-schema-registry.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/schema/registry/streams/mcp/delete.subject.permanent/client.rpt index 434a84bd7e9..11b40886e57 100644 --- a/specs/binding-mcp-schema-registry.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/schema/registry/streams/mcp/delete.subject.permanent/client.rpt +++ b/specs/binding-mcp-schema-registry.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/schema/registry/streams/mcp/delete.subject.permanent/client.rpt @@ -52,7 +52,7 @@ connected write '{"name":"delete_subject","arguments":{"subject":"orders-value","permanent":true}}' -read '{"structuredContent":{"result":[1,2]},"content":[{"type":"text","text":"Delete a subject and all its versions"}],"isError":false}' +read '{"structuredContent":{"result":[1,2]},"content":[{"type":"text","text":"Deleted versions: [1,2]"}],"isError":false}' read closed write close diff --git a/specs/binding-mcp-schema-registry.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/schema/registry/streams/mcp/delete.subject.permanent/server.rpt b/specs/binding-mcp-schema-registry.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/schema/registry/streams/mcp/delete.subject.permanent/server.rpt index ebcaf0f7f96..00dede49b5f 100644 --- a/specs/binding-mcp-schema-registry.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/schema/registry/streams/mcp/delete.subject.permanent/server.rpt +++ b/specs/binding-mcp-schema-registry.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/schema/registry/streams/mcp/delete.subject.permanent/server.rpt @@ -54,7 +54,7 @@ read '{"name":"delete_subject","arguments":{"subject":"orders-value","permanent write flush -write '{"structuredContent":{"result":[1,2]},"content":[{"type":"text","text":"Delete a subject and all its versions"}],"isError":false}' +write '{"structuredContent":{"result":[1,2]},"content":[{"type":"text","text":"Deleted versions: [1,2]"}],"isError":false}' write flush write close diff --git a/specs/binding-mcp-schema-registry.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/schema/registry/streams/mcp/delete.subject/client.rpt b/specs/binding-mcp-schema-registry.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/schema/registry/streams/mcp/delete.subject/client.rpt index d2e4dc942c5..15f6ebc06b9 100644 --- a/specs/binding-mcp-schema-registry.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/schema/registry/streams/mcp/delete.subject/client.rpt +++ b/specs/binding-mcp-schema-registry.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/schema/registry/streams/mcp/delete.subject/client.rpt @@ -52,7 +52,7 @@ connected write '{"name":"delete_subject","arguments":{"subject":"orders-value"}}' -read '{"structuredContent":{"result":[1,2]},"content":[{"type":"text","text":"Delete a subject and all its versions"}],"isError":false}' +read '{"structuredContent":{"result":[1,2]},"content":[{"type":"text","text":"Deleted versions: [1,2]"}],"isError":false}' read closed write close diff --git a/specs/binding-mcp-schema-registry.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/schema/registry/streams/mcp/delete.subject/server.rpt b/specs/binding-mcp-schema-registry.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/schema/registry/streams/mcp/delete.subject/server.rpt index 4eaa11b0eb4..68ea3b7d3b0 100644 --- a/specs/binding-mcp-schema-registry.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/schema/registry/streams/mcp/delete.subject/server.rpt +++ b/specs/binding-mcp-schema-registry.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/schema/registry/streams/mcp/delete.subject/server.rpt @@ -54,7 +54,7 @@ read '{"name":"delete_subject","arguments":{"subject":"orders-value"}}' write flush -write '{"structuredContent":{"result":[1,2]},"content":[{"type":"text","text":"Delete a subject and all its versions"}],"isError":false}' +write '{"structuredContent":{"result":[1,2]},"content":[{"type":"text","text":"Deleted versions: [1,2]"}],"isError":false}' write flush write close diff --git a/specs/binding-mcp-schema-registry.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/schema/registry/streams/mcp/describe.subject/client.rpt b/specs/binding-mcp-schema-registry.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/schema/registry/streams/mcp/describe.subject/client.rpt index ed2f6518fd8..ca57e7194b8 100644 --- a/specs/binding-mcp-schema-registry.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/schema/registry/streams/mcp/describe.subject/client.rpt +++ b/specs/binding-mcp-schema-registry.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/schema/registry/streams/mcp/describe.subject/client.rpt @@ -52,7 +52,7 @@ connected write '{"name":"describe_subject","arguments":{"subject":"orders-value"}}' -read '{"structuredContent":{"result":[1,2]},"content":[{"type":"text","text":"List versions for a subject"}],"isError":false}' +read '{"structuredContent":{"result":[1,2]},"content":[{"type":"text","text":"Versions: [1,2]"}],"isError":false}' read closed write close diff --git a/specs/binding-mcp-schema-registry.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/schema/registry/streams/mcp/describe.subject/server.rpt b/specs/binding-mcp-schema-registry.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/schema/registry/streams/mcp/describe.subject/server.rpt index c22c98f668c..99cabff3f86 100644 --- a/specs/binding-mcp-schema-registry.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/schema/registry/streams/mcp/describe.subject/server.rpt +++ b/specs/binding-mcp-schema-registry.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/schema/registry/streams/mcp/describe.subject/server.rpt @@ -54,7 +54,7 @@ read '{"name":"describe_subject","arguments":{"subject":"orders-value"}}' write flush -write '{"structuredContent":{"result":[1,2]},"content":[{"type":"text","text":"List versions for a subject"}],"isError":false}' +write '{"structuredContent":{"result":[1,2]},"content":[{"type":"text","text":"Versions: [1,2]"}],"isError":false}' write flush write close diff --git a/specs/binding-mcp-schema-registry.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/schema/registry/streams/mcp/list.subjects/client.rpt b/specs/binding-mcp-schema-registry.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/schema/registry/streams/mcp/list.subjects/client.rpt index b8393c61fba..12dbf7d3727 100644 --- a/specs/binding-mcp-schema-registry.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/schema/registry/streams/mcp/list.subjects/client.rpt +++ b/specs/binding-mcp-schema-registry.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/schema/registry/streams/mcp/list.subjects/client.rpt @@ -52,7 +52,7 @@ connected write '{"name":"list_subjects","arguments":{}}' -read '{"structuredContent":{"result":["orders-value","payments-value"]},"content":[{"type":"text","text":"List subjects"}],"isError":false}' +read '{"structuredContent":{"result":["orders-value","payments-value"]},"content":[{"type":"text","text":"Subjects: [\\"orders-value\\",\\"payments-value\\"]"}],"isError":false}' read closed write close diff --git a/specs/binding-mcp-schema-registry.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/schema/registry/streams/mcp/list.subjects/server.rpt b/specs/binding-mcp-schema-registry.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/schema/registry/streams/mcp/list.subjects/server.rpt index c8ccfec8e6b..975de2d6c32 100644 --- a/specs/binding-mcp-schema-registry.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/schema/registry/streams/mcp/list.subjects/server.rpt +++ b/specs/binding-mcp-schema-registry.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/schema/registry/streams/mcp/list.subjects/server.rpt @@ -54,7 +54,7 @@ read '{"name":"list_subjects","arguments":{}}' write flush -write '{"structuredContent":{"result":["orders-value","payments-value"]},"content":[{"type":"text","text":"List subjects"}],"isError":false}' +write '{"structuredContent":{"result":["orders-value","payments-value"]},"content":[{"type":"text","text":"Subjects: [\\"orders-value\\",\\"payments-value\\"]"}],"isError":false}' write flush write close