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 e930e223e8..61a8c88cf9 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 @@ -1117,6 +1117,7 @@ private static String inputSchema( final JsonObjectBuilder properties = Json.createObjectBuilder(); final List required = new LinkedList<>(); final List propertyNames = new LinkedList<>(); + JsonValue additionalProperties = null; if (operation.parameters != null) { @@ -1161,6 +1162,11 @@ private static String inputSchema( } } } + if (schema != null && schema.model.additionalProperties != null && + !JsonValue.FALSE.equals(schema.model.additionalProperties)) + { + additionalProperties = schema.model.additionalProperties; + } break; } } @@ -1174,6 +1180,10 @@ private static String inputSchema( required.forEach(requiredArray::add); object.add("required", requiredArray); } + if (additionalProperties != null) + { + object.add("additionalProperties", additionalProperties); + } return object.build().toString(); } 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 e6bb5026a9..9b0de35789 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 @@ -838,6 +838,86 @@ public void shouldDenyPathParameterInOpenBodySchema() assertThat(bodySchemaObject.getBoolean("additionalProperties"), equalTo(true)); } + @Test + public void shouldAdvertiseAdditionalPropertiesInInputSchema() + { + String spec = + """ + { + "openapi": "3.0.1", + "info": { "title": "things", "version": "1.0" }, + "servers": [ { "url": "https://api.example.com" } ], + "paths": { + "/things/{id}": { + "put": { + "operationId": "update_thing", + "parameters": [ + { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } } + ], + "requestBody": { + "required": true, + "content": { "application/json": { "schema": { + "type": "object", + "properties": { "name": { "type": "string" } }, + "additionalProperties": true } } } + }, + "responses": { "200": { "description": "ok" } } + } + } + } + } + """; + lenient().when(catalog.resolve(eq("things-api"), eq("latest"))).thenReturn(77); + lenient().when(catalog.resolve(eq(77))).thenReturn(spec); + + BindingConfig binding = GenericBindingConfig.builder() + .namespace("test") + .name("mcp-openapi0") + .type("mcp-openapi") + .kind(CLIENT) + .options(McpOpenapiOptionsConfig.builder() + .spec() + .label("openapi_things0") + .server("https://api.example.com") + .catalog() + .name("catalog0") + .subject("things-api") + .version("latest") + .build() + .build() + .build()) + .route() + .when(McpOpenapiConditionConfig.builder() + .tool("update_thing") + .build()) + .with(McpOpenapiWithConfig.builder() + .spec("openapi_things0") + .operation("update_thing") + .build()) + .build() + .build(); + binding.resolveId = resolveId; + + McpOpenapiCompositeConfig composite = generator.generate(new McpOpenapiBindingConfig(context, binding)); + + NamespaceConfig namespace = composite.namespaces.get(0); + String inputSchema = namespace.catalogs.stream() + .map(c -> c.options) + .filter(InlineOptionsConfig.class::isInstance) + .map(InlineOptionsConfig.class::cast) + .flatMap(o -> o.subjects.stream()) + .filter(s -> "update_thing-input".equals(s.subject)) + .map(s -> s.schema) + .findFirst() + .orElse(null); + + assertThat(inputSchema, notNullValue()); + JsonObject inputSchemaObject = Json.createReader(new StringReader(inputSchema)).readObject(); + assertThat(inputSchemaObject.getJsonObject("properties").containsKey("id"), equalTo(true)); + assertThat(inputSchemaObject.getJsonObject("properties").containsKey("name"), equalTo(true)); + assertThat(inputSchemaObject.getBoolean("additionalProperties"), equalTo(true)); + } + @Test public void shouldOverrideOutputSchema() { diff --git a/specs/binding-mcp-kafka-connect.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/kafka/connect/streams/mcp/tools.list/client.rpt b/specs/binding-mcp-kafka-connect.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/kafka/connect/streams/mcp/tools.list/client.rpt index 3bc8dbc53a..1cf39a3198 100644 --- a/specs/binding-mcp-kafka-connect.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/kafka/connect/streams/mcp/tools.list/client.rpt +++ b/specs/binding-mcp-kafka-connect.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/kafka/connect/streams/mcp/tools.list/client.rpt @@ -56,8 +56,8 @@ read '{"tools":[{"name":"list_connectors","title":"List Connectors","descriptio '{"name":"describe_connector","title":"Describe Connector","description":"Read the configuration and task list of a connector.","inputSchema":{"type":"object","properties":{"connector":{"type":"string"}},"required":["connector"]},"annotations":{"readOnlyHint":true,"destructiveHint":false,"idempotentHint":true,"openWorldHint":false}},' '{"name":"delete_connector","title":"Delete Connector","description":"Delete a connector.","inputSchema":{"type":"object","properties":{"connector":{"type":"string"}},"required":["connector"]},"annotations":{"readOnlyHint":false,"destructiveHint":true,"idempotentHint":true,"openWorldHint":false}},' '{"name":"describe_connector_config","title":"Describe Connector Config","description":"Read the effective configuration of a connector.","inputSchema":{"type":"object","properties":{"connector":{"type":"string"}},"required":["connector"]},"outputSchema":{"type":"object"},"annotations":{"readOnlyHint":true,"destructiveHint":false,"idempotentHint":true,"openWorldHint":false}},' - '{"name":"update_connector_config","title":"Update Connector Config","description":"Create or update a connector by setting its full configuration.","inputSchema":{"type":"object","properties":{"connector":{"type":"string"},"connector.class":{"type":"string"},"tasks.max":{"type":"string"}},"required":["connector"]},"annotations":{"readOnlyHint":false,"destructiveHint":false,"idempotentHint":true,"openWorldHint":false}},' - '{"name":"validate_connector_config","title":"Validate Connector Config","description":"Validate a connector configuration against a plugin configuration definition, without creating or updating any connector.","inputSchema":{"type":"object","properties":{"pluginName":{"type":"string"},"connector.class":{"type":"string"},"tasks.max":{"type":"string"}},"required":["pluginName"]},"annotations":{"readOnlyHint":true,"destructiveHint":false,"idempotentHint":true,"openWorldHint":false}},' + '{"name":"update_connector_config","title":"Update Connector Config","description":"Create or update a connector by setting its full configuration.","inputSchema":{"type":"object","properties":{"connector":{"type":"string"},"connector.class":{"type":"string"},"tasks.max":{"type":"string"}},"required":["connector"],"additionalProperties":true},"annotations":{"readOnlyHint":false,"destructiveHint":false,"idempotentHint":true,"openWorldHint":false}},' + '{"name":"validate_connector_config","title":"Validate Connector Config","description":"Validate a connector configuration against a plugin configuration definition, without creating or updating any connector.","inputSchema":{"type":"object","properties":{"pluginName":{"type":"string"},"connector.class":{"type":"string"},"tasks.max":{"type":"string"}},"required":["pluginName"],"additionalProperties":true},"annotations":{"readOnlyHint":true,"destructiveHint":false,"idempotentHint":true,"openWorldHint":false}},' '{"name":"describe_connector_status","title":"Describe Connector Status","description":"Read the current state of a connector and the state of each of its tasks.","inputSchema":{"type":"object","properties":{"connector":{"type":"string"}},"required":["connector"]},"annotations":{"readOnlyHint":true,"destructiveHint":false,"idempotentHint":true,"openWorldHint":false}},' '{"name":"restart_connector","title":"Restart Connector","description":"Restart a connector.","inputSchema":{"type":"object","properties":{"connector":{"type":"string"}},"required":["connector"]},"annotations":{"readOnlyHint":false,"destructiveHint":false,"idempotentHint":false,"openWorldHint":false}},' '{"name":"pause_connector","title":"Pause Connector","description":"Pause a connector and all of its tasks.","inputSchema":{"type":"object","properties":{"connector":{"type":"string"}},"required":["connector"]},"annotations":{"readOnlyHint":false,"destructiveHint":false,"idempotentHint":true,"openWorldHint":false}},'