diff --git a/.agents/skills/naftiko-capability/SKILL.md b/.agents/skills/naftiko-capability/SKILL.md index a9ef3014..cc257d20 100644 --- a/.agents/skills/naftiko-capability/SKILL.md +++ b/.agents/skills/naftiko-capability/SKILL.md @@ -50,6 +50,7 @@ for *how*. | "I want to proxy an API today and encapsulate it incrementally" | Read `references/proxy-then-customize.md` | | "I want to chain multiple HTTP calls to consumed APIs and expose the result into a single REST operation" | Read `references/chain-api-calls.md` | | "I need to go from local test credentials to production secrets" | Read `references/dev-to-production.md` | +| "I want to prototype a tool or endpoint before the backend exists" or "I want to return static or dynamic mock data" | Read `references/mock-capability.md` | | "I want to build a full-featured capability that does all of the above" | Read all stories in order, then use `assets/capability-example.yml` as structural reference | | "I have a YAML validation error" | Run `scripts/lint-capability.sh` — see **Lint workflow** below | | "I'm done writing — what should I check before shipping?" | Read `references/design-guidelines.md`, then run lint | @@ -95,20 +96,30 @@ Specification directly. For the full rule list, read the Spectral ruleset file directly. 3. Fix and re-lint. Repeat until clean. -## Hard Constraints - -1. The root field `naftiko` must be `"1.0.0-alpha1"` — no other version string is - valid for this spec revision. -2. Every `consumes` entry must have both `baseUri` and `namespace`. -3. Every `exposes` entry must have a `namespace`. -4. Namespaces must be unique across all `exposes`, `consumes` and `binds` objects. -5. At least one of `exposes` or `consumes` must be present in `capability`. -6. `call` references must follow `{namespace}.{operationName}` and point - to a valid consumed operation. -7. `{exposeNamespace}.{paramName}` is the only syntax for referencing - exposed input parameters in `with` injectors — do not invent alternatives. -8. `variable` expressions resolve from `binds` keys. -9. `ForwardConfig` requires `targetNamespace` (single string, not array) +### Mock a Capability + +Use mock mode when the upstream API does not exist yet, or you want to +prototype a contract-first design. Read `references/mock-capability.md` +before writing any mock output parameters. + +**Key rules:** + +1. Omit `call`, `steps`, and the entire `consumes` block — they are not + needed for a pure mock capability. +2. Use `value` on `MappedOutputParameter` for static strings + (`value: "Hello"`) or Mustache templates (`value: "Hello, {{name}}!"`). + Do NOT use `const` — it is schema-only and is never used at runtime. +3. Mustache placeholders in `value` are resolved against the tool's or + endpoint's input parameters by name. Only top-level input parameter + names are in scope — no nesting, no `with` remapping. +4. `value` and `mapping` are mutually exclusive on a scalar output + parameter — never set both. +5. Object and array type output parameters in mock mode must carry + `value` on each leaf scalar descendant; the container itself has + no `value`. +6. When the mock is ready to be wired to a real API, add `consumes`, + replace `value` with `mapping`, and add `call` or `steps` — the + exposed contract does not change. and `trustedHeaders` (at least one entry). 10. MCP tools must have `name` and `description`. MCP tool input parameters must have `name`, `type`, and `description`. Tools may declare optional @@ -129,4 +140,10 @@ Specification directly. outputs are dead declarations that add noise without value. 16. Do not prefix variable names with the capability, namespace, or resource name — variables are already scoped to their context. - Redundant prefixes reduce readability without adding disambiguation. \ No newline at end of file + Redundant prefixes reduce readability without adding disambiguation. +17. In mock mode, use `value` on output parameters — never `const`. + `const` is a JSON Schema keyword retained for validation and linting + only; it has no effect at runtime. +18. In mock mode, Mustache templates in `value` fields resolve only against + top-level input parameter names. Do not reference `with`-remapped + consumed parameter names — those are not in scope for output resolution. \ No newline at end of file diff --git a/.agents/skills/naftiko-capability/references/mock-capability.md b/.agents/skills/naftiko-capability/references/mock-capability.md new file mode 100644 index 00000000..4b78455d --- /dev/null +++ b/.agents/skills/naftiko-capability/references/mock-capability.md @@ -0,0 +1,149 @@ +# Story: Mock a Capability + +## User problem + +The backend API doesn't exist yet, or you want to prototype a contract-first +design — defining the exposed shape before any upstream service is available. +You need the MCP tool or REST endpoint to return realistic, shaped data so +consumers (agents, developers, tests) can start integrating immediately. + +## Naftiko pattern + +A **mock capability** is a capability that omits `consumes`, `call`, and +`steps`. Output parameters carry a `value` field instead of a `mapping` field. +The runtime returns the value directly without making any HTTP call. + +`value` accepts: +- A **static string**: `value: "Hello, World!"` +- A **Mustache template**: `value: "Hello, {{name}}!"` — resolved against the + tool's or endpoint's input parameters by name at request time. + +Both REST and MCP adapters support mock mode identically — the same output +parameter shape works for both adapter types. + +## When to use each + +| Situation | `value` | +|---|---| +| Fixed stub data, no inputs needed | Static string | +| Response echoes or interpolates an input | Mustache template `{{paramName}}` | +| Nested object with static leaves | Static string on each leaf scalar | +| Nested object with dynamic leaves | Mustache template on each leaf scalar | + +## Minimal MCP mock example + +```yaml +naftiko: "1.0.0-alpha1" + +capability: + exposes: + - type: mcp + port: 3001 + namespace: mock-tools + description: Mock MCP server for prototyping + tools: + - name: say-hello + description: Returns a greeting using the provided name + inputParameters: + - name: name + type: string + required: true + description: Name to greet + outputParameters: + - name: message + type: string + value: "Hello, {{name}}! Welcome aboard." +``` + +## Minimal REST mock example + +```yaml +naftiko: "1.0.0-alpha1" + +capability: + exposes: + - type: rest + address: localhost + port: 8080 + namespace: mock-api + resources: + - path: /greet + operations: + - method: GET + inputParameters: + - name: name + in: query + type: string + required: true + outputParameters: + - name: message + type: string + value: "Hello, {{name}}!" +``` + +## Nested object mock + +Nest `type: object` with scalar children carrying `value`. The container +itself has no `value` — only scalar leaves do. + +```yaml +outputParameters: + - name: user + type: object + properties: + - id: + type: string + value: "usr-001" + - displayName: + type: string + value: "{{name}}" + - role: + type: string + value: "viewer" +``` + +## Array mock + +Use `type: array` with a single `items` descriptor. The runtime returns +one representative item. + +```yaml +outputParameters: + - name: results + type: array + items: + type: object + properties: + - id: + type: string + value: "item-001" + - label: + type: string + value: "Sample item for {{query}}" +``` + +## Migrating from mock to real + +When the upstream API is ready: + +1. Add a `consumes` block with `baseUri`, `namespace`, resources, and operations. +2. Replace `value` with `mapping` on each output parameter (JSONPath expression). +3. Add `call: {namespace}.{operationName}` to the exposed tool or operation. +4. Remove any inputs that were only needed for Mustache interpolation if they + are now resolved from the upstream response instead. + +The exposed contract (tool/resource names, input parameters, output parameter +names and types) does not need to change — consumers see no difference. + +## Hard constraints for mock mode + +- Use `value`, not `const`. `const` is a JSON Schema keyword for validation; + it has no runtime effect. +- `value` and `mapping` are mutually exclusive on a scalar output parameter. +- Mustache placeholders resolve against top-level input parameter names only. + Names remapped via `with` are not in scope for output value resolution. +- Object and array output parameters in mock mode must NOT carry `value` on + the container — only on the scalar leaf descendants. +- If `call` or `steps` is present alongside output `value` fields and an + upstream response is available, the upstream response takes precedence; + `value` is the fallback when no response body exists. diff --git a/src/main/java/io/naftiko/engine/Resolver.java b/src/main/java/io/naftiko/engine/Resolver.java index 1924f4ad..e459292c 100644 --- a/src/main/java/io/naftiko/engine/Resolver.java +++ b/src/main/java/io/naftiko/engine/Resolver.java @@ -97,9 +97,9 @@ public static Object resolveInputParameterFromRequest(InputParameterSpec spec, R return null; } - // constant takes precedence - if (spec.getConstant() != null) { - return spec.getConstant(); + // value provides a static override when not a JSONPath expression + if (spec.getValue() != null && !spec.getValue().trim().startsWith("$")) { + return spec.getValue(); } String in = spec.getIn() == null ? "body" : spec.getIn(); @@ -182,10 +182,9 @@ public static Object resolveInputParameterFromRequest(InputParameterSpec spec, R * * Resolution priority for parameter values: * 1. 'value' field - resolved with Mustache template syntax ({{paramName}}) for dynamic resolution - * 2. 'const' field - used as-is, no template resolution applied - * 3. 'template' field - resolved with Mustache syntax - * 4. Parameters map - direct lookup by parameter name - * 5. Environment variables - for 'environment' location + * 2. 'template' field - resolved with Mustache syntax + * 3. Parameters map - direct lookup by parameter name + * 4. Environment variables - for 'environment' location */ public static void resolveInputParametersToRequest(Request clientRequest, List specs, Map parameters) { @@ -202,9 +201,6 @@ public static void resolveInputParametersToRequest(Request clientRequest, if (spec.getValue() != null) { // Resolve Mustache templates in value, allowing dynamic parameter resolution val = Resolver.resolveMustacheTemplate(spec.getValue(), parameters); - } else if (spec.getConstant() != null) { - // Use constant value as-is (no template resolution) - val = spec.getConstant(); } else if (spec.getTemplate() != null) { val = Resolver.resolveMustacheTemplate(spec.getTemplate(), parameters); } else if (parameters != null && parameters.containsKey(spec.getName())) { @@ -245,15 +241,27 @@ public static void resolveInputParametersToRequest(Request clientRequest, */ public static JsonNode resolveOutputMappings(OutputParameterSpec spec, JsonNode clientRoot, ObjectMapper mapper) { + return resolveOutputMappings(spec, clientRoot, mapper, null); + } + + /** + * Build a mapped JSON node from the output parameter specification and the client response + * root, optionally resolving Mustache templates in {@code value} fields. + * + * @param parameters input parameters for Mustache resolution (may be null) + */ + public static JsonNode resolveOutputMappings(OutputParameterSpec spec, JsonNode clientRoot, + ObjectMapper mapper, Map parameters) { if (spec == null) { return NullNode.instance; } String type = spec.getType(); - // constant value takes precedence - if (spec.getConstant() != null) { - return mapper.getNodeFactory().textNode(spec.getConstant()); + // value takes precedence — used for static/mock values + if (spec.getValue() != null) { + String resolved = resolveMustacheTemplate(spec.getValue(), parameters); + return mapper.getNodeFactory().textNode(resolved); } if ("array".equalsIgnoreCase(type)) { @@ -357,5 +365,84 @@ public static JsonNode resolveOutputMappings(OutputParameterSpec spec, JsonNode } } + /** + * Build a mock JSON object from output parameter {@code value} fields. + * Mustache templates in values are resolved against the given parameters. + * + * @param outputParameters the output parameter specs + * @param mapper Jackson mapper + * @param parameters input parameters for Mustache resolution (may be null) + * @return a JSON object, or {@code null} if no values could be built + */ + public static JsonNode buildMockData(List outputParameters, + ObjectMapper mapper, Map parameters) { + if (outputParameters == null || outputParameters.isEmpty()) { + return null; + } + + ObjectNode result = mapper.createObjectNode(); + + for (OutputParameterSpec param : outputParameters) { + JsonNode paramValue = buildMockValue(param, mapper, parameters); + if (paramValue != null && !(paramValue instanceof NullNode)) { + String fieldName = param.getName() != null ? param.getName() : "value"; + result.set(fieldName, paramValue); + } + } + + return result.size() > 0 ? result : null; + } + + /** + * Build a mock JSON node for a single output parameter. Resolves Mustache templates + * in {@code value} fields and recurses into nested objects and arrays. + * + * @param param the output parameter spec + * @param mapper Jackson mapper + * @param parameters input parameters for Mustache resolution (may be null) + * @return the mock JSON node, or {@link NullNode} if no value could be built + */ + public static JsonNode buildMockValue(OutputParameterSpec param, ObjectMapper mapper, + Map parameters) { + if (param == null) { + return NullNode.instance; + } + + if (param.getValue() != null) { + String resolved = resolveMustacheTemplate(param.getValue(), parameters); + return mapper.getNodeFactory().textNode(resolved); + } + + String type = param.getType(); + + if ("array".equalsIgnoreCase(type)) { + ArrayNode arrayNode = mapper.createArrayNode(); + OutputParameterSpec items = param.getItems(); + if (items != null) { + JsonNode itemValue = buildMockValue(items, mapper, parameters); + if (itemValue != null && !(itemValue instanceof NullNode)) { + arrayNode.add(itemValue); + } + } + return arrayNode; + } + + if ("object".equalsIgnoreCase(type)) { + ObjectNode objectNode = mapper.createObjectNode(); + if (param.getProperties() != null) { + for (OutputParameterSpec prop : param.getProperties()) { + JsonNode propValue = buildMockValue(prop, mapper, parameters); + if (propValue != null && !(propValue instanceof NullNode)) { + String propName = prop.getName() != null ? prop.getName() : "property"; + objectNode.set(propName, propValue); + } + } + } + return objectNode.size() > 0 ? objectNode : NullNode.instance; + } + + return NullNode.instance; + } + } diff --git a/src/main/java/io/naftiko/engine/consumes/http/HttpClientAdapter.java b/src/main/java/io/naftiko/engine/consumes/http/HttpClientAdapter.java index 19dee9ee..af6eca39 100644 --- a/src/main/java/io/naftiko/engine/consumes/http/HttpClientAdapter.java +++ b/src/main/java/io/naftiko/engine/consumes/http/HttpClientAdapter.java @@ -74,8 +74,8 @@ public HttpClientOperationSpec getOperationSpec(String operationName) { public void setHeaders(Request request) { // Set any default headers from the input parameters for (InputParameterSpec param : getHttpClientSpec().getInputParameters()) { - if ("header".equalsIgnoreCase(param.getIn()) && param.getConstant() != null) { - request.getHeaders().set(param.getName(), param.getConstant()); + if ("header".equalsIgnoreCase(param.getIn()) && param.getValue() != null) { + request.getHeaders().set(param.getName(), param.getValue()); } } } diff --git a/src/main/java/io/naftiko/engine/exposes/mcp/ToolHandler.java b/src/main/java/io/naftiko/engine/exposes/mcp/ToolHandler.java index 9f33d402..9936b375 100644 --- a/src/main/java/io/naftiko/engine/exposes/mcp/ToolHandler.java +++ b/src/main/java/io/naftiko/engine/exposes/mcp/ToolHandler.java @@ -24,6 +24,8 @@ import io.naftiko.engine.Resolver; import io.naftiko.engine.exposes.OperationStepExecutor; import io.naftiko.spec.exposes.McpServerToolSpec; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; /** * Handles MCP tool calls by delegating to consumed HTTP operations. @@ -98,6 +100,12 @@ public McpSchema.CallToolResult handleToolCall(String toolName, Map parameters) throws IOException { + ObjectMapper mapper = new ObjectMapper(); + JsonNode mockRoot = Resolver.buildMockData(toolSpec.getOutputParameters(), mapper, + parameters); + + String json = mapper.writeValueAsString(mockRoot != null ? mockRoot : mapper.createObjectNode()); + return new McpSchema.CallToolResult( + List.of(new McpSchema.TextContent(json)), false, null, null); + } + /** * Build an MCP CallToolResult from the HTTP client response. */ diff --git a/src/main/java/io/naftiko/engine/exposes/rest/ResourceRestlet.java b/src/main/java/io/naftiko/engine/exposes/rest/ResourceRestlet.java index 100bf12f..23ee5b51 100644 --- a/src/main/java/io/naftiko/engine/exposes/rest/ResourceRestlet.java +++ b/src/main/java/io/naftiko/engine/exposes/rest/ResourceRestlet.java @@ -122,8 +122,8 @@ private boolean handleFromOperationSpec(Request request, Response response) { sendResponse(serverOp, response, found); return true; } else if (canBuildMockResponse(serverOp)) { - // No HTTP client adapter found, use mock mode with const values - sendMockResponse(serverOp, response); + // No HTTP client adapter found, use mock mode with static values + sendMockResponse(serverOp, response, inputParameters); return true; } else { response.setStatus(Status.CLIENT_ERROR_BAD_REQUEST); @@ -179,8 +179,8 @@ private boolean handleFromOperationSpec(Request request, Response response) { sendResponse(serverOp, response, found); return true; } else if (canBuildMockResponse(serverOp)) { - // No HTTP client adapter found, use mock mode with const values - sendMockResponse(serverOp, response); + // No HTTP client adapter found, use mock mode with static values + sendMockResponse(serverOp, response, inputParameters); return true; } } @@ -191,29 +191,29 @@ private boolean handleFromOperationSpec(Request request, Response response) { } /** - * Check if an operation can build a mock response using const values from outputParameters. - * Returns true if the operation has at least one outputParameter with a const value. + * Check if an operation can build a mock response using static values from outputParameters. + * Returns true if the operation has at least one outputParameter with a value. */ boolean canBuildMockResponse(RestServerOperationSpec serverOp) { if (serverOp.getOutputParameters() == null || serverOp.getOutputParameters().isEmpty()) { return false; } - // Check if at least one output parameter has a const value + // Check if at least one output parameter has a static value for (OutputParameterSpec param : serverOp.getOutputParameters()) { - if (param.getConstant() != null) { + if (param.getValue() != null) { return true; } - // Check nested properties for const values + // Check nested properties for static values if (param.getProperties() != null && !param.getProperties().isEmpty()) { for (OutputParameterSpec prop : param.getProperties()) { - if (hasConstValue(prop)) { + if (hasStaticValue(prop)) { return true; } } } - // Check items for const values - if (param.getItems() != null && hasConstValue(param.getItems())) { + // Check items for static values + if (param.getItems() != null && hasStaticValue(param.getItems())) { return true; } } @@ -222,41 +222,43 @@ boolean canBuildMockResponse(RestServerOperationSpec serverOp) { } /** - * Recursively check if a parameter or its nested structure has any const values. + * Recursively check if a parameter or its nested structure has any static values. */ - private boolean hasConstValue(OutputParameterSpec param) { + private boolean hasStaticValue(OutputParameterSpec param) { if (param == null) { return false; } - if (param.getConstant() != null) { + if (param.getValue() != null) { return true; } if (param.getProperties() != null) { for (OutputParameterSpec prop : param.getProperties()) { - if (hasConstValue(prop)) { + if (hasStaticValue(prop)) { return true; } } } if (param.getItems() != null) { - return hasConstValue(param.getItems()); + return hasStaticValue(param.getItems()); } return false; } /** - * Send a mock response using const values from outputParameters. + * Send a mock response using static or templated values from outputParameters. */ - void sendMockResponse(RestServerOperationSpec serverOp, Response response) { + void sendMockResponse(RestServerOperationSpec serverOp, Response response, + Map inputParameters) { try { ObjectMapper mapper = new ObjectMapper(); - // Build a JSON response using const values from outputParameters - JsonNode mockRoot = buildMockData(serverOp, mapper); + // Build a JSON response using static/templated values from outputParameters + JsonNode mockRoot = Resolver.buildMockData(serverOp.getOutputParameters(), mapper, + inputParameters); if (mockRoot != null) { response.setStatus(Status.SUCCESS_OK); @@ -276,80 +278,6 @@ void sendMockResponse(RestServerOperationSpec serverOp, Response response) { response.commit(); } - /** - * Build a JSON object with mock data from outputParameters const values. - */ - private JsonNode buildMockData(RestServerOperationSpec serverOp, ObjectMapper mapper) { - if (serverOp.getOutputParameters() == null || serverOp.getOutputParameters().isEmpty()) { - return null; - } - - com.fasterxml.jackson.databind.node.ObjectNode result = mapper.createObjectNode(); - - for (OutputParameterSpec param : serverOp.getOutputParameters()) { - JsonNode paramValue = buildParameterValue(param, mapper); - if (paramValue != null && !(paramValue instanceof NullNode)) { - // Use the parameter name if available, otherwise use "value" - String fieldName = param.getName() != null ? param.getName() : "value"; - result.set(fieldName, paramValue); - } - } - - return result.size() > 0 ? result : null; - } - - /** - * Build a JSON node for a single parameter, using const values or structures. - */ - JsonNode buildParameterValue(OutputParameterSpec param, ObjectMapper mapper) { - if (param == null) { - return NullNode.instance; - } - - // Handle const values directly - if (param.getConstant() != null) { - return mapper.getNodeFactory().textNode(param.getConstant()); - } - - String type = param.getType(); - - // Handle array types - if ("array".equalsIgnoreCase(type)) { - com.fasterxml.jackson.databind.node.ArrayNode arrayNode = mapper.createArrayNode(); - OutputParameterSpec items = param.getItems(); - - if (items != null) { - // Create one mock item to demonstrate the structure - JsonNode itemValue = buildParameterValue(items, mapper); - if (itemValue != null && !(itemValue instanceof NullNode)) { - arrayNode.add(itemValue); - } - } - - return arrayNode; - } - - // Handle object types - if ("object".equalsIgnoreCase(type)) { - com.fasterxml.jackson.databind.node.ObjectNode objectNode = mapper.createObjectNode(); - - if (param.getProperties() != null) { - for (OutputParameterSpec prop : param.getProperties()) { - JsonNode propValue = buildParameterValue(prop, mapper); - if (propValue != null && !(propValue instanceof NullNode)) { - String propName = prop.getName() != null ? prop.getName() : "property"; - objectNode.set(propName, propValue); - } - } - } - - return objectNode.size() > 0 ? objectNode : NullNode.instance; - } - - // For other types without const values, return null - return NullNode.instance; - } - void sendResponse(RestServerOperationSpec serverOp, Response response, OperationStepExecutor.HandlingContext found) { // Apply output mappings if present or forward the raw entity diff --git a/src/main/java/io/naftiko/spec/OutputParameterDeserializer.java b/src/main/java/io/naftiko/spec/OutputParameterDeserializer.java index ec57c193..42306773 100644 --- a/src/main/java/io/naftiko/spec/OutputParameterDeserializer.java +++ b/src/main/java/io/naftiko/spec/OutputParameterDeserializer.java @@ -22,8 +22,8 @@ import com.fasterxml.jackson.databind.node.ObjectNode; /** - * Custom deserializer for OutputParameterSpec that handles nested structure definitions - * including properties, items, and values in a polymorphic manner. + * Custom deserializer for OutputParameterSpec that handles nested structure definitions including + * properties, items, and values in a polymorphic manner. */ public class OutputParameterDeserializer extends JsonDeserializer { @@ -51,11 +51,16 @@ private OutputParameterSpec deserializeNode(JsonNode node, DeserializationContex if (node.has("mapping")) { spec.setMapping(node.get("mapping").asText()); - } else if (node.has("value")) { - // Backward compatibility: allow "value" as an alias of "mapping" + } else if (node.has("value") && node.has("name")) { + // ConsumedOutputParameter uses "value" for JsonPath extraction (has name + value) spec.setMapping(node.get("value").asText()); } + if (node.has("value") && !node.has("name")) { + // MappedOutputParameter uses "value" for static runtime values (no name) + spec.setValue(node.get("value").asText()); + } + if (node.has("const")) { spec.setConstant(node.get("const").asText()); } @@ -95,16 +100,17 @@ private OutputParameterSpec deserializeNode(JsonNode node, DeserializationContex try { String propName = entry.getKey(); JsonNode propNode = entry.getValue(); - - // Inject the property name into the property spec - if (propNode.isObject()) { - ((ObjectNode) propNode).put("name", propName); - } - + OutputParameterSpec propSpec = deserializeNode(propNode, ctxt); + // Set the name from the property key AFTER deserialization + // so the dispatch logic sees the original node content + if (propSpec.getName() == null) { + propSpec.setName(propName); + } spec.getProperties().add(propSpec); } catch (Exception e) { - throw new RuntimeException("Error deserializing property: " + entry.getKey(), e); + throw new RuntimeException( + "Error deserializing property: " + entry.getKey(), e); } }); } diff --git a/src/main/java/io/naftiko/spec/OutputParameterSerializer.java b/src/main/java/io/naftiko/spec/OutputParameterSerializer.java index 3a58e33a..eaff56a9 100644 --- a/src/main/java/io/naftiko/spec/OutputParameterSerializer.java +++ b/src/main/java/io/naftiko/spec/OutputParameterSerializer.java @@ -64,6 +64,11 @@ private void serializeSpec(OutputParameterSpec spec, ObjectNode node) { } } + // Static runtime value (MappedOutputParameter) + if (spec.getValue() != null && (spec.getName() == null || spec.getName().isBlank())) { + node.put("value", spec.getValue()); + } + if (spec.getConstant() != null) { node.put("const", spec.getConstant()); } @@ -174,6 +179,10 @@ private void serializeSpecWithoutName(OutputParameterSpec spec, ObjectNode node) node.put("mapping", spec.getMapping()); } + if (spec.getValue() != null) { + node.put("value", spec.getValue()); + } + if (spec.getConstant() != null) { node.put("const", spec.getConstant()); } diff --git a/src/main/java/io/naftiko/spec/OutputParameterSpec.java b/src/main/java/io/naftiko/spec/OutputParameterSpec.java index 6f626137..2164f7bf 100644 --- a/src/main/java/io/naftiko/spec/OutputParameterSpec.java +++ b/src/main/java/io/naftiko/spec/OutputParameterSpec.java @@ -29,6 +29,12 @@ public class OutputParameterSpec extends StructureSpec { @JsonProperty("mapping") private volatile String mapping; + /** + * Static value for mock responses. When provided, the runtime uses this value + * as-is instead of resolving via mapping from an external API response. + */ + private volatile String value; + public OutputParameterSpec() { super(); } @@ -65,4 +71,12 @@ public void setMapping(String mapping) { this.mapping = mapping; } + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + } diff --git a/src/main/resources/schemas/examples/cir.yml b/src/main/resources/schemas/examples/cir.yml deleted file mode 100644 index 967bf90e..00000000 --- a/src/main/resources/schemas/examples/cir.yml +++ /dev/null @@ -1,187 +0,0 @@ -# yaml-language-server: $schema=../naftiko-schema.json ---- -naftiko: "1.0.0-alpha1" - -info: - label: "CIR Time Export" - description: "Extracts CIR-eligible time tracking data from Notion for a given date range, with breakdown by person, project, and activity. Designed for automated CIR (Crédit d'Impôt Recherche) reporting." - tags: - - cir - - time-tracking - - notion - - reporting - created: "2026-02-19" - modified: "2026-02-19" - stakeholders: - - role: "owner" - fullName: "Thomas Eskenazi" - email: "thomaseskenazi@naftiko.io" - -binds: - - namespace: "notion-env" - description: "Notion API credentials for accessing pre-release participant data stored in Notion." - location: "file:///path/to/notion_env.json" - keys: - NOTION_TOKEN: "NOTION_INTEGRATION_TOKEN" - NOTION_PROJECTS_DB_ID: "PROJECTS_DATABASE_ID" - NOTION_TIME_TRACKER_DB_ID: "TIME_TRACKER_DATABASE_ID" - -capability: - exposes: - - type: "rest" - address: "localhost" - port: 8090 - namespace: "cir" - resources: - - path: "/time-report" - name: "time-report" - label: "CIR Time Report" - description: "API endpoint for retrieving CIR-eligible time tracking data with breakdown by person, project, and activity." - inputParameters: - - name: "start_date" - description: "Start date for the time report (inclusive), in YYYY-MM-DD format" - in: "query" - type: "string" - pattern: "^\\d{4}-\\d{2}-\\d{2}$" - - name: "end_date" - description: "End date for the time report (inclusive), in YYYY-MM-DD format" - in: "query" - type: "string" - pattern: "^\\d{4}-\\d{2}-\\d{2}$" - operations: - - name: "get-time-report" - method: "GET" - label: "Get CIR Time Report" - outputParameters: - - name: "entries" - type: "array" - items: - - name: "date" - type: "string" - - name: "project_morning" - type: "string" - - name: "activity_morning" - type: "string" - - name: "project_afternoon" - type: "string" - - name: "activity_afternoon" - type: "string" - - name: "person" - type: "string" - steps: - - type: "call" - name: "fetch-projects" - call: "notion.query-projects" - - type: "call" - name: "fetch-entries" - call: "notion.query-entries" - with: - start_date: "$this.cir.start_date" - end_date: "$this.cir.end_date" - - type: "lookup" - name: "morning-project" - index: "fetch-projects" - match: "ids" - lookupValue: "$.fetch-entries.morning-ids" - outputParameters: - - "projects" - - "activities" - - type: "lookup" - name: "afternoon-project" - index: "fetch-projects" - match: "ids" - lookupValue: "$.fetch-entries.afternoon-ids" - outputParameters: - - "projects" - - "activities" - mappings: - - targetName: "date" - value: "$.fetch-entries.dates" - - targetName: "project_morning" - value: "$.morning-project.projects" - - targetName: "activity_morning" - value: "$.morning-project.activities" - - targetName: "project_afternoon" - value: "$.afternoon-project.projects" - - targetName: "activity_afternoon" - value: "$.afternoon-project.activities" - - targetName: "person" - value: "$.fetch-entries.persons" - - consumes: - - type: "http" - description: "Notion API integration for accessing project and time tracking data relevant to CIR reporting." - namespace: "notion" - baseUri: "https://api.notion.com/v1" - authentication: - type: "bearer" - token: "{{NOTION_TOKEN}}" - inputParameters: - - name: "Notion-Version" - in: "header" - value: "2022-06-28" - - name: "Content-Type" - in: "header" - value: "application/json" - resources: - - name: "projects-db" - label: "Projects Database" - path: "/databases/{{NOTION_PROJECTS_DB_ID}}/query" - operations: - - name: "query-projects" - method: "POST" - label: "Query CIR-eligible Projects" - body: - type: json - data: - filter: - property: "CIR Scope" - select: - equals: "Yes" - outputParameters: - - name: "ids" - type: "array" - value: "$.results[*].id" - - name: "projects" - type: "array" - value: "$.results[*].properties.Project.select.name" - - name: "activities" - type: "array" - value: "$.results[*].properties.Activity.select.name" - - - name: "time-tracker-db" - label: "Time Tracker Database" - path: "/databases/{{NOTION_TIME_TRACKER_DB_ID}}/query" - operations: - - name: "query-entries" - method: "POST" - label: "Query Time Entries for Date Range" - inputParameters: - - name: "start_date" - in: "body" - - name: "end_date" - in: "body" - body: - type: json - data: - filter: - and: - - property: "Day" - date: - on_or_after: "{{start_date}}" - - property: "Day" - date: - before: "{{end_date}}" - outputParameters: - - name: "dates" - type: "array" - value: "$.results[*].properties.Day.date.start" - - name: "persons" - type: "array" - value: "$.results[*].properties.Person.people[0].id" - - name: "morning-ids" - type: "array" - value: "$.results[*].properties.Morning.relation[0].id" - - name: "afternoon-ids" - type: "array" - value: "$.results[*].properties.Afternoon.relation[0].id" diff --git a/src/main/resources/schemas/examples/html-consumes-rest-adapter.yml b/src/main/resources/schemas/examples/html-consumes-rest-adapter.yml deleted file mode 100644 index 22b4f2d5..00000000 --- a/src/main/resources/schemas/examples/html-consumes-rest-adapter.yml +++ /dev/null @@ -1,44 +0,0 @@ -# yaml-language-server: $schema=../naftiko-schema.json ---- -naftiko: "1.0.0-alpha1" -info: - label: "HTML Catalog Reader" - description: "Extracts product rows from an HTML table" - -capability: - exposes: - - type: "rest" - address: "localhost" - port: 9090 - namespace: "catalog" - resources: - - path: "/products" - name: "products" - operations: - - method: "GET" - call: "vendor.get-products" - outputParameters: - - type: "array" - mapping: "$.tables[0]" - items: - type: "object" - properties: - name: - type: "string" - mapping: "$.Name" - price: - type: "string" - mapping: "$.Price" - - consumes: - - type: "http" - namespace: "vendor" - baseUri: "https://vendor.example.com" - resources: - - path: "/catalog" - name: "catalog" - operations: - - method: "GET" - name: "get-products" - outputRawFormat: "html" - outputSchema: "table.products" diff --git a/src/main/resources/schemas/examples/markdown-consumes-rest-adapter.yml b/src/main/resources/schemas/examples/markdown-consumes-rest-adapter.yml deleted file mode 100644 index 997c4de1..00000000 --- a/src/main/resources/schemas/examples/markdown-consumes-rest-adapter.yml +++ /dev/null @@ -1,44 +0,0 @@ -# yaml-language-server: $schema=../naftiko-schema.json ---- -naftiko: "1.0.0-alpha1" -info: - label: "Markdown Feature Reader" - description: "Extracts feature rows from a markdown table" - -capability: - exposes: - - type: "rest" - address: "localhost" - port: 9090 - namespace: "readme" - resources: - - path: "/features" - name: "features" - operations: - - method: "GET" - call: "docs.get-readme" - outputParameters: - - type: "array" - mapping: "$.tables[0]" - items: - type: "object" - properties: - feature: - type: "string" - mapping: "$.Feature" - status: - type: "string" - mapping: "$.Status" - - consumes: - - type: "http" - namespace: "docs" - baseUri: "https://raw.githubusercontent.com" - resources: - - path: "/naftiko/framework/main/README.md" - name: "readme" - operations: - - method: "GET" - name: "get-readme" - outputRawFormat: "markdown" - outputSchema: "Overview" diff --git a/src/main/resources/schemas/examples/multi-consumes-rest-adapter.yml b/src/main/resources/schemas/examples/multi-consumes-rest-adapter.yml deleted file mode 100644 index ce0bc9da..00000000 --- a/src/main/resources/schemas/examples/multi-consumes-rest-adapter.yml +++ /dev/null @@ -1,149 +0,0 @@ -# yaml-language-server: $schema=../naftiko-schema.json ---- -naftiko: "1.0.0-alpha1" -info: - label: "Sample Capability" - description: "This is a sample capability specification to demonstrate the features of Naftiko" - tags: - - Naftiko - - Sample - created: "2026-01-01" - modified: "2026-02-12" - stakeholders: - - role: "editor" - fullName: "John Doe" - email: "john.doe@example.com" - -capability: - exposes: - - type: "rest" - address: "localhost" - port: 9090 - namespace: "sample" - resources: - - path: "/users/{{username}}" - name: "user" - label: "User resource" - description: "This is a resource to retrieve user information" - operations: - - method: "GET" - name: "get-user" - label: "Get User" - inputParameters: - - name: "user_name" - in: "path" - type: "string" - description: "Username to retrieve information for" - steps: - - call: "github.get-user" - name: "fetch-user" - type: "call" - with: - "username": "$this.sample.{{user_name}}" - - - - - path: "/databases/{{database_id}}" - name: "db" - label: "Database resource" - description: "This is a resource to retrieve and update information about a database" - inputParameters: - - name: "database_id" - type: "string" - description: "ID of the database to retrieve information for" - in: "path" - operations: - - method: "GET" - name: "get-db" - label: "Get Database" - outputParameters: - - name: "Api-Version" - type: string - - name: "db_name" - type: "string" - steps: - - call: "notion.get-database" - type: "call" - name: "fetch-db" - with: - "database_id": "$this.sample.{{database_id}}" - mappings: - - targetName: "db_name" - value: "$.{{dbName}}" - - targetName: "Api-Version" - value: "v1" - - path: "/notion/{{path}}" - label: "Notion API Pass-thru Proxy" - description: "A proxy to forward requests to the Notion API while the capability is being configured" - forward: - targetNamespace: "notion" - trustedHeaders: - - "Notion-Version" - - path: "/github/{{path}}" - label: "GitHub API Pass-thru Proxy" - description: "A proxy to forward requests to the GitHub API while the capability is being configured" - forward: - targetNamespace: "github" - trustedHeaders: - - "Notion-Version" - - consumes: - - type: "http" - description: "HTTP API integration for accessing external services." - namespace: "notion" - baseUri: "https://api.notion.com/v1" - inputParameters: - - name: "Notion-Version" - in: "header" - value: "2022-06-28" - authentication: - type: "basic" - username: "scott" - password: "tiger" - resources: - - path: "/databases/{{database_id}}" - name: "db" - label: "Database resource" - description: "This is a resource to retrieve and update information about a database" - inputParameters: - - name: "database_id" - in: "path" - value: "1234" - operations: - - method: "GET" - name: "get-database" - label: "Get Database" - outputRawFormat: "xml" - outputParameters: - - name: "dbName" - type: "string" - value: "$.title[0].text.content" - - name: "dbId" - type: "string" - value: "$.id" - - method: "PUT" - name: "update-database" - label: "Update Database" - - method: "DELETE" - name: "delete-database" - label: "Delete Database" - - path: "/databases/{{database_id}}/query" - name: "query-db" - label: "Query database resource" - operations: - - method: "POST" - name: "query-database" - label: "Query Database" - - - type: "http" - description: "HTTP API integration for accessing external services." - namespace: "github" - baseUri: "https://api.github.com/v1" - resources: - - path: "/users/{{username}}" - name: "user" - label: "User resource" - operations: - - method: "GET" - name: "get-user" - label: "Get User" diff --git a/src/main/resources/schemas/examples/multi-consumes-with-auth.yml b/src/main/resources/schemas/examples/multi-consumes-with-auth.yml deleted file mode 100644 index 2ed6b2e4..00000000 --- a/src/main/resources/schemas/examples/multi-consumes-with-auth.yml +++ /dev/null @@ -1,64 +0,0 @@ -# yaml-language-server: $schema=../naftiko-schema.json ---- -naftiko: "1.0.0-alpha1" -info: - label: passtru - description: Example pass tru capability -capability: - exposes: - - namespace: "local-api" - type: rest - address: "localhost" - port: 8080 - resources: - - path: "/users" - name: "users" - label: "Users API" - description: "API to retrieve user information" - operations: - - method: "GET" - name: "get-users" - label: "Get Users" - steps: - - call: "github.get-users" - type: call - name: "fetch-users" - - path: "/notion" - label: "Notion API Pass-thru Proxy" - description: "A proxy to forward requests to the Notion API while the capability is being configured" - forward: - targetNamespace: "notion" - trustedHeaders: - - "Notion-Version" - consumes: - - namespace: "notion" - description: "Notion API integration for accessing project and time tracking data relevant to CIR reporting." - type: http - baseUri: "https://api.notion.com/v1" - resources: - - path: "/mock" - name: "notion" - label: "Notion API" - description: "This is a pass-thru resource to forward requests to the Notion API" - operations: - - name: "get-mock" - label: "Mock operation for example" - method: GET - outputRawFormat: json - authentication: - type: "basic" - username: "scott" - password: "tiger" - - namespace: "github" - description: "GitHub API integration for accessing user data." - type: http - baseUri: "https://api.github.com/v1" - resources: - - name: "github" - path: "/users" - label: "GitHub API" - description: "This is a pass-thru resource to forward requests to the GitHub API" - operations: - - name: "get-users" - label: "Mock operation for example" - method: GET diff --git a/src/main/resources/schemas/examples/no-adapter.yml b/src/main/resources/schemas/examples/no-adapter.yml deleted file mode 100644 index 59870d2e..00000000 --- a/src/main/resources/schemas/examples/no-adapter.yml +++ /dev/null @@ -1,57 +0,0 @@ -# yaml-language-server: $schema=../naftiko-schema.json ---- -naftiko: "1.0.0-alpha1" -info: - label: "Sample Capability" - description: "This is a sample capability specification to demonstrate the features of Naftiko" - tags: - - Naftiko - - Sample - created: "2026-01-01" - modified: "2026-02-12" - stakeholders: - - role: "editor" - fullName: "John Doe" - email: "john.doe@example.com" - -capability: - exposes: - - type: "rest" - address: "localhost" - port: 9090 - namespace: "sample" - - resources: - - path: "/notion/pre-release" - name: "pr" - label: "Pre-release resource" - description: "This is a resource to retrieve the list of pre-release participants stored in Notion" - operations: - - method: "GET" - outputParameters: - - type: "array" - mapping: "$.results" - items: - type: "object" - properties: - name: - type: "string" - const: "John Doe" - company: - type: "string" - const: "Example Corp" - title: - type: "string" - const: "Persona" - location: - type: "string" - const: "Earth" - owner: - type: "string" - const: "Naftiko" - participation_status: - type: "string" - const: "Committed" - comments: - type: "string" - const: "For mocking purposes only" diff --git a/src/main/resources/schemas/examples/notion-sandbox.yml b/src/main/resources/schemas/examples/notion-sandbox.yml deleted file mode 100644 index 8982c078..00000000 --- a/src/main/resources/schemas/examples/notion-sandbox.yml +++ /dev/null @@ -1,36 +0,0 @@ -# yaml-language-server: $schema=../naftiko-schema.json ---- -naftiko: "1.0.0-alpha1" -info: - label: "Notion capability" - description: "This is a capability enabling consumption of Notion API." -capability: - exposes: - - type: "rest" - address: "localhost" - namespace: "notion-api-proxy" - port: 8080 - resources: - - path: "/notion/{{path}}" - label: "Notion API Pass-thru Proxy" - description: "A proxy to forward requests to the Notion API while the capability is being configured" - forward: - trustedHeaders: - - "Notion-Version" - targetNamespace: "notion" - consumes: - - namespace: "notion" - description: "Notion API integration for accessing data stored in Notion." - type: http - baseUri: "https://api.notion.com/v1" - authentication: - type: "basic" - username: "scott" - password: "tiger" - resources: - - name: "db" - path: "databases/{{database_id}}" - operations: - - name: get-database - method: GET - diff --git a/src/main/resources/schemas/examples/notion.yml b/src/main/resources/schemas/examples/notion.yml deleted file mode 100644 index a3d30c76..00000000 --- a/src/main/resources/schemas/examples/notion.yml +++ /dev/null @@ -1,142 +0,0 @@ -# yaml-language-server: $schema=../naftiko-schema.json ---- -naftiko: "1.0.0-alpha1" - -info: - label: "Sample Capability" - description: "This is a sample capability specification to demonstrate the features of Naftiko" - tags: - - Naftiko - - Sample - created: "2026-01-01" - modified: "2026-02-12" - stakeholders: - - role: "editor" - fullName: "John Doe" - email: "john.doe@example.com" - -binds: - - namespace: "notion-env" - description: "Notion API credentials for accessing pre-release participant data stored in Notion." - location: "file:///path/to/notion_env.json" - keys: - NOTION_TOKEN: "NOTION_API_TOKEN" - NOTION_PROJECTS_DB_ID: "PROJECTS_DATABASE_ID" - -capability: - exposes: - - type: "rest" - address: "localhost" - port: 9090 - namespace: "sample" - - resources: - - path: "/notion/pre-release" - name: "pr" - label: "Pre-release resource" - description: "This is a resource to retrieve the list of pre-release participants stored in Notion" - operations: - - method: "GET" - call: "notion.query-db" - with: - datasource_id: "2fe4adce-3d02-8028-bec8-000bfb5cafa2" - outputParameters: - - type: "array" - mapping: "$.results" - items: - type: "object" - properties: - name: - type: "string" - mapping: "$.properties.Name.title[0].text.content" - company: - type: "string" - mapping: "$.properties.Company.rich_text[0].text.content" - title: - type: "string" - mapping: "$.properties.Title.rich_text[0].text.content" - location: - type: "string" - mapping: "$.properties.Location.rich_text[0].text.content" - owner: - type: "string" - mapping: "$.properties.Owner.people[0].name" - participation_status: - type: "string" - mapping: "$.properties.Participation Status.select.name" - comments: - type: "string" - mapping: "$.properties.Comments.rich_text[0].text.content" - - - type: "mcp" - address: "localhost" - port: 9091 - namespace: "notion-mcp" - description: "MCP server exposing Notion database query capabilities for pre-release participant management." - - tools: - - name: "query-database" - description: "Query the Notion pre-release participants database to retrieve committed participants with their name, company, title, location, owner, participation status and comments." - call: "notion.query-db" - with: - datasource_id: "2fe4adce-3d02-8028-bec8-000bfb5cafa2" - outputParameters: - - type: "array" - mapping: "$.results" - items: - type: "object" - properties: - name: - type: "string" - mapping: "$.properties.Name.title[0].text.content" - company: - type: "string" - mapping: "$.properties.Company.rich_text[0].text.content" - title: - type: "string" - mapping: "$.properties.Title.rich_text[0].text.content" - location: - type: "string" - mapping: "$.properties.Location.rich_text[0].text.content" - owner: - type: "string" - mapping: "$.properties.Owner.people[0].name" - participation_status: - type: "string" - mapping: "$.properties.Participation Status.select.name" - comments: - type: "string" - mapping: "$.properties.Comments.rich_text[0].text.content" - - consumes: - - type: "http" - description: "Notion API integration for accessing pre-release participant data stored in Notion." - namespace: "notion" - baseUri: "https://api.notion.com/v1" - authentication: - type: "bearer" - token: "{{notion_api_key}}" - inputParameters: - - name: "notion_api_key" - in: "environment" - - name: "Notion-Version" - in: "header" - value: "2025-09-03" - - resources: - - path: "/data_sources/{{datasource_id}}/query" - name: "query" - label: "Query database resource" - operations: - - method: "POST" - name: "query-db" - label: "Query Database" - body: | - { - "filter": { - "property": "Participation Status", - "select": { - "equals": "Committed" - } - } - } diff --git a/src/main/resources/schemas/examples/skill-adapter.yml b/src/main/resources/schemas/examples/skill-adapter.yml deleted file mode 100644 index 6f756556..00000000 --- a/src/main/resources/schemas/examples/skill-adapter.yml +++ /dev/null @@ -1,121 +0,0 @@ -# yaml-language-server: $schema=../naftiko-schema.json ---- -naftiko: "1.0.0-alpha1" -info: - label: "Weather Forecast Capability" - description: "Exposes weather data via API, MCP, and skill catalog adapters" - -capability: - exposes: - - type: rest - port: 3000 - namespace: weather-api - resources: - - path: /weather/current - description: "Current weather conditions" - operations: - - name: get-current - method: GET - call: open-meteo.get-current - outputParameters: - - type: object - mapping: $.current - properties: - temperature: { type: number, mapping: $.temperature_2m } - windspeed: { type: number, mapping: $.windspeed_10m } - weathercode: { type: number, mapping: $.weathercode } - - - path: /weather/alerts - description: "Active weather alerts" - operations: - - name: list-alerts - method: GET - call: open-meteo.list-alerts - outputParameters: - - type: array - mapping: $.alerts - items: - type: "object" - properties: - title: { type: string, mapping: $.title } - severity: { type: string, mapping: $.severity } - - - type: mcp - port: 3001 - namespace: weather-mcp - description: "Weather tools for AI agents" - tools: - - name: get-current-weather - description: "Retrieve current weather conditions for a location" - hints: - readOnly: true - openWorld: true - inputParameters: - - name: location - type: string - description: "City name or coordinates (lat,lon)" - call: open-meteo.get-current - with: - location: "$this.weather-mcp.location" - outputParameters: - - type: object - mapping: $.current - properties: - temperature: { type: number, mapping: $.temperature_2m } - windspeed: { type: number, mapping: $.windspeed_10m } - weathercode: { type: number, mapping: $.weathercode } - - - type: skill - port: 4000 - namespace: weather-skills - description: "Weather forecast and climate analysis skill catalog" - skills: - - name: weather-forecast - description: "Real-time weather data and forecasting" - license: Apache-2.0 - compatibility: "claude-3-5-sonnet,gpt-4o" - argument-hint: "Use when the user asks about weather, forecast, temperature, or climate" - location: "file:///opt/skills/weather-forecast" - tools: - - name: current-conditions - description: "Get current weather conditions for a specific location" - from: - sourceNamespace: weather-api - action: get-current - - name: climate-guide - description: "Reference guide for interpreting climate data and weather patterns" - instruction: "climate-interpretation-guide.md" - - - name: alert-monitoring - description: "Severe weather alerts and monitoring guidance" - argument-hint: "Use when the user needs to check or monitor weather alerts" - location: "file:///opt/skills/alert-monitoring" - tools: - - name: active-alerts - description: "List active severe weather alerts for a region" - from: - sourceNamespace: weather-mcp - action: get-current-weather - - name: alert-response-guide - description: "Guide for responding to severe weather alerts" - instruction: "alert-response-guide.md" - - consumes: - - type: http - namespace: open-meteo - description: "Open-Meteo weather API for current conditions and alerts." - baseUri: "https://api.open-meteo.com/v1" - resources: - - name: forecast - path: /forecast - operations: - - name: get-current - method: GET - label: "Get Current Weather" - - - name: alerts - path: /alerts - operations: - - name: list-alerts - method: GET - label: "List Active Alerts" diff --git a/src/main/resources/schemas/naftiko-schema-v0.2.json b/src/main/resources/schemas/naftiko-schema-v0.2.json deleted file mode 100644 index fab263ac..00000000 --- a/src/main/resources/schemas/naftiko-schema-v0.2.json +++ /dev/null @@ -1,277 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://naftiko.io/schemas/v0.2/capability.json", - "title": "Naftiko Capability Specification", - "description": "This Schema should be used to describe and validate Naftiko Capabilities", - "type": "object", - "properties": { - "naftiko": { - "type": "string", - "description": "Version of the Naftiko schema", - "const": "0.2" - }, - "info": { - "$ref": "#/$defs/Info" - }, - "capability": { - "$ref": "#/$defs/Capability" - } - }, - "required": [ - "naftiko", - "info", - "capability" - ], - "additionalProperties": false, - "$defs": { - "Address": { - "type": "string", - "description": "hostname or IP for the exposition", - "anyOf": [ - { - "format": "hostname" - }, - { - "format": "ipv4" - }, - { - "format": "ipv6" - } - ] - }, - "Protocol": { - "type": "string", - "description": "Protocol/format used", - "enum": [ - "rest/json", - "rest/xml", - "grpc", - "graphql", - "soap" - ], - "default": "rest/json" - }, - "Info": { - "type": "object", - "description": "Information about the capability", - "properties": { - "name": { - "type": "string", - "description": "The name of the capability" - }, - "description": { - "type": "string", - "description": "A description of the capability (the more meaningful, the easier for agents discovery)" - } - }, - "required": [ - "name", - "description" - ], - "additionalProperties": false - }, - "Capability": { - "type": "object", - "description": "Capability technical configuration", - "properties": { - "exposes": { - "type": "array", - "items": { - "$ref": "#/$defs/Exposes" - }, - "minItems": 1 - }, - "consumes": { - "type": "array", - "items": { - "$ref": "#/$defs/Consumes" - }, - "minItems": 1, - "if": { - "minItems": 2 - }, - "then": { - "items": { - "required": [ - "targetUri", - "expositionSuffix" - ] - } - } - } - }, - "required": [ - "exposes", - "consumes" - ], - "additionalProperties": false - }, - "Exposes": { - "type": "object", - "description": "Exposition endpoint configuration", - "properties": { - "address": { - "$ref": "#/$defs/Address" - }, - "port": { - "type": "integer", - "description": "Port number for the Exposition endpoint", - "minimum": 1, - "maximum": 65535 - }, - "protocol": { - "$ref": "#/$defs/Protocol" - }, - "authentication": { - "$ref": "#/$defs/Authentication", - "default": "inherit" - } - }, - "required": [ - "port" - ], - "additionalProperties": false - }, - "Consumes": { - "type": "object", - "description": "configuration for consuming multiple entries (requires routing suffix)", - "properties": { - "expositionSuffix": { - "type": "string", - "description": "Path suffix used to route to this consumes at the exposes level", - "pattern": "^[a-zA-Z0-9_-]+$" - }, - "targetUri": { - "type": "string", - "description": "Target URI for the consumes API (supports {{path}} placeholder)", - "format": "uri-template", - "pattern": "^.*\\{\\{path\\}\\}.*$" - }, - "protocol": { - "$ref": "#/$defs/Protocol" - }, - "authentication": { - "$ref": "#/$defs/Authentication", - "default": "inherit" - } - }, - "required": [ - "targetUri" - ], - "additionalProperties": false - }, - "Authentication": { - "description": "Authentication", - "oneOf": [ - { - "$ref": "#/$defs/AuthBasic" - }, - { - "$ref": "#/$defs/AuthApiKey" - }, - { - "$ref": "#/$defs/AuthBearer" - }, - { - "$ref": "#/$defs/AuthDigest" - }, - { - "type": "string", - "const": "inherit" - } - ] - }, - "AuthBasic": { - "type": "object", - "description": "Basic authentication", - "properties": { - "type": { - "type": "string", - "const": "basic" - }, - "username": { - "type": "string", - "description": "Username for basic auth" - }, - "password": { - "type": "string", - "description": "Password for basic auth" - } - }, - "required": [ - "type" - ], - "additionalProperties": false - }, - "AuthApiKey": { - "type": "object", - "description": "API Key authentication", - "properties": { - "type": { - "type": "string", - "const": "apikey" - }, - "key": { - "type": "string", - "description": "API key name" - }, - "value": { - "type": "string", - "description": "API key value" - }, - "placement": { - "type": "string", - "enum": [ - "header", - "query" - ], - "description": "Where to place the API key" - } - }, - "required": [ - "type" - ], - "additionalProperties": false - }, - "AuthBearer": { - "type": "object", - "description": "Bearer token authentication", - "properties": { - "type": { - "type": "string", - "const": "bearer" - }, - "token": { - "type": "string", - "description": "Bearer token" - } - }, - "required": [ - "type" - ], - "additionalProperties": false - }, - "AuthDigest": { - "type": "object", - "description": "Digest authentication", - "properties": { - "type": { - "type": "string", - "const": "digest" - }, - "username": { - "type": "string", - "description": "Username for digest auth" - }, - "password": { - "type": "string", - "description": "Password for digest auth" - } - }, - "required": [ - "type" - ], - "additionalProperties": false - } - } -} diff --git a/src/main/resources/schemas/naftiko-schema-v0.3.json b/src/main/resources/schemas/naftiko-schema-v0.3.json deleted file mode 100644 index b382c63c..00000000 --- a/src/main/resources/schemas/naftiko-schema-v0.3.json +++ /dev/null @@ -1,829 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://naftiko.io/schemas/v0.3/capability.json", - "name": "Naftiko Capability Specification", - "description": "This Schema should be used to describe and validate Naftiko Capabilities", - "type": "object", - "properties": { - "naftiko": { - "type": "string", - "description": "Version of the Naftiko schema", - "const": "0.3" - }, - "info": { - "$ref": "#/$defs/Info" - }, - "capability": { - "$ref": "#/$defs/Capability" - } - }, - "required": [ - "naftiko", - "info", - "capability" - ], - "additionalProperties": false, - "$defs": { - "InputParameter": { - "type": "object", - "description": "Describes a single function parameter. A unique parameter is defined by a combination of a name and location.", - "properties": { - "name": { - "type": "string", - "description": "Name of the parameter", - "pattern": "^[a-zA-Z0-9-_]+$" - }, - "in": { - "type": "string", - "description": "Location of the parameter", - "enum": [ - "query", - "header", - "path", - "cookie", - "body" - ] - }, - "value": { - "type": "string", - "description": "Value or JSONPath reference" - } - }, - "required": [ - "name", - "in" - ], - "additionalProperties": false - }, - "OutputParameter": { - "type": "object", - "description": "Describes a single function output parameter. It is referenced by and ID for later usage in the Exposes layer to define how the output of a consumed function is mapped.", - "properties": { - "name": { - "type": "string", - "description": "The name of the output parameter", - "pattern": "^[a-zA-Z0-9-_]+$" - }, - "value": { - "type": "string", - "description": "The value of the output parameter. It is retrived by reference to the consumed function response using JsonPath syntax. E.g. $response.data.user.id" - } - }, - "required": [ - "name" - ], - "additionalProperties": false - }, - "Hostname": { - "type": "string", - "description": "Valid hostname", - "pattern": "^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)(\\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$" - }, - "IPv4": { - "type": "string", - "description": "Valid IPv4 address", - "pattern": "^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$" - }, - "IPv6": { - "type": "string", - "description": "Valid IPv6 address (simplified)", - "pattern": "^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$" - }, - "Address": { - "description": "hostname or IP for the exposition", - "anyOf": [ - { - "$ref": "#/$defs/Hostname" - }, - { - "$ref": "#/$defs/IPv4" - }, - { - "$ref": "#/$defs/IPv6" - } - ] - }, - "Info": { - "type": "object", - "description": "Information about the capability", - "properties": { - "label": { - "type": "string", - "description": "The display name of the capability" - }, - "description": { - "type": "string", - "description": "A description of the capability (the more meaningful, the easier for agents discovery)" - }, - "tags": { - "type": "array", - "description": "List of tags to help categorize the capability. It can be used for discovery and filtering purposes.", - "items": { - "type": "string" - } - }, - "created": { - "type": "string", - "pattern": "^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])$", - "description": "The date and time when the capability was created" - }, - "modified": { - "type": "string", - "pattern": "^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])$", - "description": "The date and time when the capability was last modified" - }, - "stakeholders": { - "type": "array", - "description": "List of stakeholders related to this capability. It can be used for discovery and filtering purposes.", - "items": { - "$ref": "#/$defs/Person" - } - } - }, - "required": [ - "label", - "description" - ], - "additionalProperties": false - }, - "Person": { - "type": "object", - "description": "A person related to the capability", - "properties": { - "role": { - "type": "string", - "description": "The role of the person in relation to the capability. E.g. owner, editor, viewer" - }, - "fullName": { - "type": "string", - "description": "The full name of the person" - }, - "email": { - "type": "string", - "pattern": "^[a-zA-Z0-9._%+\\-]+@[a-zA-Z0-9.\\-]+\\.[a-zA-Z]{2,}$", - "description": "The email address of the person" - } - }, - "required": [ - "role", - "fullName" - ], - "additionalProperties": false - }, - "Capability": { - "type": "object", - "description": "Capability technical configuration", - "properties": { - "exposes": { - "type": "array", - "description": "List of exposed server adapters", - "items": { - "$ref": "#/$defs/ExposesApi" - }, - "minItems": 1 - }, - "consumes": { - "type": "array", - "description": "Consumed client adapters", - "items": { - "$ref": "#/$defs/ConsumesHttp" - }, - "minItems": 1 - } - }, - "required": [ - "exposes", - "consumes" - ], - "additionalProperties": false - }, - "ExposesApi": { - "type": "object", - "description": "API exposition configuration", - "properties": { - "type": { - "type": "string", - "const": "api" - }, - "address": { - "$ref": "#/$defs/Address" - }, - "port": { - "type": "integer", - "minimum": 1, - "maximum": 65535 - }, - "namespace": { - "type": "string", - "pattern": "^[a-zA-Z0-9-]+$", - "description": "Unique identifier for this exposed API" - }, - "authentication": { - "$ref": "#/$defs/Authentication" - }, - "resources": { - "type": "array", - "description": "List of exposed resources", - "items": { - "$ref": "#/$defs/ExposedResource" - }, - "minItems": 1 - } - }, - "required": [ - "type", - "port", - "namespace", - "resources" - ], - "additionalProperties": false - }, - "ExposedResource": { - "type": "object", - "description": "An exposed resource", - "properties": { - "label": { - "type": "string", - "description": "Display name for the resource. It will likely be used in UIs." - }, - "description": { - "type": "string", - "description": "Used to provide *meaningul* infimration about the resource. Remember, in a world of agents, context is king." - }, - "path": { - "type": "string", - "description": "Path of the resource (supports {{param}} placeholders)" - }, - "name": { - "type": "string", - "description": "Technicnal name for the resource. It will likely be used for reference", - "pattern": "^[a-zA-Z0-9-]+$" - }, - "inputParameters": { - "type": "array", - "items": { - "$ref": "#/$defs/InputParameter" - } - }, - "operations": { - "type": "array", - "description": "Operations available on this resource", - "items": { - "$ref": "#/$defs/ExposedOperation" - } - }, - "forward": { - "$ref": "#/$defs/ForwardConfig" - } - }, - "required": [ - "description", - "path" - ], - "oneOf": [ - { - "required": [ - "operations" - ] - }, - { - "required": [ - "forward" - ] - } - ], - "additionalProperties": false - }, - "ExposedOperation": { - "type": "object", - "description": "An operation exposed on a resource", - "properties": { - "name": { - "type": "string", - "description": "Technicnal name for the resource. It will likely be used for reference", - "pattern": "^[a-zA-Z0-9-]+$" - }, - "label": { - "type": "string", - "description": "Display name for the resource. It will likely be used in UIs." - }, - "method": { - "type": "string", - "enum": [ - "GET", - "POST", - "PUT", - "PATCH", - "DELETE" - ], - "default": "GET" - }, - "inputParameters": { - "type": "array", - "items": { - "$ref": "#/$defs/InputParameter" - } - }, - "outputParameters": { - "type": "array", - "items": { - "$ref": "#/$defs/OutputParameter" - } - }, - "steps": { - "type": "array", - "items": { - "$ref": "#/$defs/OperationStep" - }, - "minItems": 1 - } - }, - "required": [ - "method", - "name", - "steps" - ], - "additionalProperties": false - }, - "ConsumesHttp": { - "type": "object", - "description": "HTTP API consumption configuration", - "properties": { - "namespace": { - "type": "string", - "pattern": "^[a-zA-Z0-9-]+$", - "description": "Unique identifier for this consumed API" - }, - "type": { - "type": "string", - "const": "http" - }, - "baseUri": { - "type": "string", - "description": "Target URI for the consumes API", - "pattern": "^https?://[^/]+(/[^{]*)?$" - }, - "inputParameters": { - "type": "array", - "items": { - "$ref": "#/$defs/InputParameter" - } - }, - "authentication": { - "$ref": "#/$defs/Authentication" - }, - "headers": { - "type": "array", - "items": { - "type": "string", - "pattern": "^[^:]+:\\s*.+$" - } - }, - "resources": { - "type": "array", - "description": "List of consumed resources", - "items": { - "$ref": "#/$defs/ConsumedHttpResource" - }, - "minItems": 1 - } - }, - "required": [ - "namespace", - "type", - "baseUri", - "resources" - ], - "additionalProperties": false - }, - "ConsumedHttpResource": { - "type": "object", - "description": "A consumed HTTP resource", - "properties": { - "name": { - "type": "string", - "pattern": "^[a-zA-Z0-9-]+$" - }, - "label": { - "type": "string" - }, - "description": { - "type": "string" - }, - "path": { - "type": "string" - }, - "inputParameters": { - "type": "array", - "items": { - "$ref": "#/$defs/InputParameter" - } - }, - "operations": { - "type": "array", - "description": "Operations on this resource", - "items": { - "$ref": "#/$defs/ConsumedHttpOperation" - }, - "minItems": 1 - } - }, - "required": [ - "name", - "path", - "operations" - ], - "additionalProperties": false - }, - "ConsumedHttpOperation": { - "type": "object", - "description": "An operation on a consumed HTTP resource", - "properties": { - "name": { - "type": "string", - "pattern": "^[a-zA-Z0-9-]+$" - }, - "label": { - "type": "string" - }, - "method": { - "type": "string", - "enum": [ - "GET", - "POST", - "PUT", - "PATCH", - "DELETE" - ], - "default": "GET" - }, - "inputParameters": { - "type": "array", - "items": { - "$ref": "#/$defs/InputParameter" - } - }, - "outputRawFormat": { - "type": "string", - "enum": [ - "json", - "XML" - ], - "default": "json", - "description": "The raw format of the response from the consumed API. It is used to determine how to parse the response and extract the output parameters. Defualt value is json." - }, - "outputParameters": { - "type": "array", - "items": { - "$ref": "#/$defs/OutputParameter" - } - }, - "body": { - "$ref": "#/$defs/RequestBody" - } - }, - "required": [ - "name", - "method" - ], - "additionalProperties": false - }, - "OperationStep": { - "type": "object", - "description": "Describes a single sequence step called in a sequence of calls in order to execute a request", - "properties": { - "call": { - "type": "string", - "description": "The reference of the source where the consumed request lies. It should follow the syntax {{namespace}}.{{operationId}}. E.g. : notion.get-database or github.get-user", - "pattern": "^[a-zA-Z0-9-]+\\.[a-zA-Z0-9-]+$" - }, - "inputParameters": { - "type": "array", - "items": { - "$ref": "#/$defs/OperationStepParameter" - } - }, - "mappings": { - "type": "array", - "description": "Defines how to map the output parameters of this step to the input parameters of the next steps or to the output parameters of the exposed operation. It is an array of mapping objects with targetName and value properties. The targetName is the name of the parameter to map to, and the value is a JsonPath reference to the value to map from. E.g. {targetName: 'database_id', value: '$.get-database.database_id'}", - "items": { - "$ref": "#/$defs/StepOutputMapping" - } - } - }, - "required": [ - "call" - ], - "additionalProperties": false - }, - "StepOutputMapping": { - "type": "object", - "description": "Describes how to map the output of an operation step to the input of another step or to the output of the exposed operation", - "properties": { - "targetName": { - "type": "string", - "description": "The name of the parameter to map to. It can be an input parameter of a next step or an output parameter of the exposed operation" - }, - "value": { - "type": "string", - "description": "A JsonPath reference to the value to map from. E.g. $.get-database.database_id" - } - }, - "required": [ - "targetName", - "value" - ], - "additionalProperties": false - }, - "OperationStepParameter": { - "type": "object", - "description": "Describes a single parameter in an operation step", - "properties": { - "name": { - "type": "string", - "description": "The name of the parameter in the called operation" - }, - "value": { - "type": "string", - "description": "The value of the parameter. It can be a static value or a reference to a previous step output using JsonPath syntax. E.g. $this.step1.output.data.id" - } - }, - "required": [ - "name", - "value" - ], - "additionalProperties": false - }, - "RequestBodyJson": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "json" - }, - "data": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "object" - }, - { - "type": "array" - } - ] - } - }, - "required": [ - "type", - "data" - ], - "additionalProperties": false - }, - "RequestBodyText": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "text", - "xml", - "sparql" - ] - }, - "data": { - "type": "string" - } - }, - "required": [ - "type", - "data" - ], - "additionalProperties": false - }, - "RequestBodyFormUrlEncoded": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "formUrlEncoded" - }, - "data": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - ] - } - }, - "required": [ - "type", - "data" - ], - "additionalProperties": false - }, - "RequestBodyMultipartFormPart": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "value": { - "type": "string" - }, - "filename": { - "type": "string" - }, - "contentType": { - "type": "string" - } - }, - "required": [ - "name", - "value" - ], - "additionalProperties": false - }, - "RequestBodyMultipartForm": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "multipartForm" - }, - "data": { - "type": "array", - "items": { - "$ref": "#/$defs/RequestBodyMultipartFormPart" - } - } - }, - "required": [ - "type", - "data" - ], - "additionalProperties": false - }, - "RequestBody": { - "description": "Request body configuration", - "oneOf": [ - { - "$ref": "#/$defs/RequestBodyJson" - }, - { - "$ref": "#/$defs/RequestBodyText" - }, - { - "$ref": "#/$defs/RequestBodyFormUrlEncoded" - }, - { - "$ref": "#/$defs/RequestBodyMultipartForm" - } - ] - }, - "ForwardConfig": { - "type": "object", - "description": "Configuration for forwarding requests to a consumed namespace", - "properties": { - "targetNamespace": { - "type": "string", - "pattern": "^[a-zA-Z0-9-]+$", - "description": "The namespace to forward requests to" - }, - "trustedHeaders": { - "type": "array", - "items": { - "type": "string" - }, - "description": "List of headers that can be forwarded", - "minItems": 1 - } - }, - "required": [ - "targetNamespace", - "trustedHeaders" - ], - "additionalProperties": false - }, - "Authentication": { - "description": "Authentication", - "oneOf": [ - { - "$ref": "#/$defs/AuthBasic" - }, - { - "$ref": "#/$defs/AuthApiKey" - }, - { - "$ref": "#/$defs/AuthBearer" - }, - { - "$ref": "#/$defs/AuthDigest" - }, - { - "type": "string", - "const": "inherit" - } - ] - }, - "AuthBasic": { - "type": "object", - "description": "Basic authentication", - "properties": { - "type": { - "type": "string", - "const": "basic" - }, - "username": { - "type": "string", - "description": "Username for basic auth" - }, - "password": { - "type": "string", - "description": "Password for basic auth" - } - }, - "required": [ - "type" - ], - "additionalProperties": false - }, - "AuthApiKey": { - "type": "object", - "description": "API Key authentication", - "properties": { - "type": { - "type": "string", - "const": "apikey" - }, - "key": { - "type": "string", - "description": "API key name" - }, - "value": { - "type": "string", - "description": "API key value" - }, - "placement": { - "type": "string", - "enum": [ - "header", - "query" - ], - "description": "Where to place the API key" - } - }, - "required": [ - "type" - ], - "additionalProperties": false - }, - "AuthBearer": { - "type": "object", - "description": "Bearer token authentication", - "properties": { - "type": { - "type": "string", - "const": "bearer" - }, - "token": { - "type": "string", - "description": "Bearer token" - } - }, - "required": [ - "type" - ], - "additionalProperties": false - }, - "AuthDigest": { - "type": "object", - "description": "Digest authentication", - "properties": { - "type": { - "type": "string", - "const": "digest" - }, - "username": { - "type": "string", - "description": "Username for digest auth" - }, - "password": { - "type": "string", - "description": "Password for digest auth" - } - }, - "required": [ - "type" - ], - "additionalProperties": false - } - } -} diff --git a/src/main/resources/schemas/naftiko-schema-v0.4.json b/src/main/resources/schemas/naftiko-schema-v0.4.json deleted file mode 100644 index e2cd3a7e..00000000 --- a/src/main/resources/schemas/naftiko-schema-v0.4.json +++ /dev/null @@ -1,1632 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://naftiko.io/schemas/v0.4/capability.json", - "name": "Naftiko Capability Specification", - "description": "This Schema should be used to describe and validate Naftiko Capabilities", - "type": "object", - "properties": { - "naftiko": { - "type": "string", - "description": "Version of the Naftiko schema", - "const": "0.4" - }, - "info": { - "$ref": "#/$defs/Info" - }, - "externalRefs": { - "type": "array", - "items": { - "$ref": "#/$defs/ExternalRef" - }, - "minItems": 1, - "description": "List of external references for variable injection and resource linking." - }, - "capability": { - "$ref": "#/$defs/Capability" - } - }, - "required": [ - "naftiko", - "capability" - ], - "additionalProperties": false, - "$defs": { - "ExternalRef": { - "oneOf": [ - { - "type": "object", - "description": "File-resolved external reference", - "properties": { - "name": { - "$ref": "#/$defs/IdentifierKebab", - "description": "A unique identifier for this external reference." - }, - "description": { - "type": "string", - "description": "Used to provide *meaningful* information about the external reference. Remember, in a world of agents, context is king." - }, - "type": { - "type": "string", - "enum": [ - "environment" - ] - }, - "resolution": { - "type": "string", - "description": "The resolution strategy for this external reference. 'file' means that the reference will be resolved from a file at the specified URI during capability loading, and the extracted variables will be injected for use in the capability definition. 'runtime' means that the reference will be resolved at runtime from the execution context (e.g. environment variables, secrets manager, or any other context store), and the extracted variables will be injected for use in the capability definition. Default value is 'runtime'.", - "const": "file" - }, - "uri": { - "type": "string" - }, - "keys": { - "$ref": "#/$defs/ExternalRefKeys", - "description": "Map of key-value pairs that define the variables to be injected from the resolved external reference. Each key is the variable name used for injection, each value is the corresponding key in the resolved file content. E.g. {\"notion_token\": \"NOTION_INTEGRATION_TOKEN\"} means that the value of the NOTION_INTEGRATION_TOKEN field in the resolved JSON file will be injected as {{notion_token}} in the capability definition." - } - }, - "required": [ - "name", - "type", - "resolution", - "uri", - "keys" - ], - "additionalProperties": false - }, - { - "type": "object", - "description": "Runtime-resolved external reference (default)", - "properties": { - "name": { - "$ref": "#/$defs/IdentifierKebab", - "description": "A unique identifier for this external reference." - }, - "type": { - "type": "string", - "enum": [ - "variables" - ] - }, - "resolution": { - "type": "string", - "description": "The resolution strategy for this external reference. 'file' means that the reference will be resolved from a file at the specified URI during capability loading, and the extracted variables will be injected for use in the capability definition. 'runtime' means that the reference will be resolved at runtime from the execution context (e.g. environment variables, secrets manager, or any other context store), and the extracted variables will be injected for use in the capability definition. Default value is 'runtime'.", - "const": "runtime" - }, - "keys": { - "$ref": "#/$defs/ExternalRefKeys", - "description": "Map of key-value pairs that define the variables to be injected from the external reference at runtime. Each key is the variable name used for injection, each value is the corresponding key in the runtime context. E.g. {\"notion_token\": \"NOTION_INTEGRATION_TOKEN\"} means that the value of the NOTION_INTEGRATION_TOKEN field in the runtime context will be injected as {{notion_token}} in the capability definition." - } - }, - "required": [ - "name", - "type", - "resolution", - "keys" - ], - "additionalProperties": false - } - ] - }, - "ExternalRefKeys": { - "type": "object", - "description": "Map of key-value pairs that define the variables to be injected from the external reference. Each key is the variable name used for injection, each value is the corresponding key in the resolved file content or runtime context. E.g. {\"notion_token\": \"NOTION_INTEGRATION_TOKEN\"} means that the value of the NOTION_INTEGRATION_TOKEN field in the resolved JSON file or runtime context will be injected as {{notion_token}} in the capability definition.", - "propertyNames": { - "$ref": "#/$defs/IdentifierExtended" - }, - "additionalProperties": { - "$ref": "#/$defs/IdentifierExtended" - } - }, - "IdentifierKebab": { - "type": "string", - "description": "Kebab-case identifier (alphanumeric and hyphens).", - "pattern": "^[a-zA-Z0-9-]+$" - }, - "IdentifierExtended": { - "type": "string", - "description": "Extended identifier (alphanumeric, hyphens, underscores, and wildcard).", - "pattern": "^[a-zA-Z0-9-_*]+$" - }, - "ConsumedInputParameter": { - "type": "object", - "description": "Describes a single function parameter. A unique parameter is defined by a combination of a name and location.", - "properties": { - "name": { - "$ref": "#/$defs/IdentifierExtended", - "description": "Name of the parameter" - }, - "in": { - "type": "string", - "description": "Location of the parameter", - "enum": [ - "query", - "header", - "path", - "cookie", - "body", - "environment" - ] - }, - "value": { - "type": "string", - "description": "Value of the parameter. Can be a static value or an expression resolved from the execution context (e.g. $this.namespace.param)" - } - }, - "required": [ - "name", - "in" - ], - "additionalProperties": false - }, - "ExposedInputParameter": { - "type": "object", - "description": "Declares an input parameter for an exposed resource or operation. Type and description are mandatory since the framework cannot infer them.", - "properties": { - "name": { - "$ref": "#/$defs/IdentifierExtended", - "description": "Name of the parameter." - }, - "in": { - "type": "string", - "description": "Location of the parameter.", - "enum": [ - "query", - "header", - "path", - "cookie", - "body" - ] - }, - "type": { - "type": "string", - "description": "The expected data type of the parameter.", - "enum": [ - "string", - "number", - "integer", - "boolean", - "array", - "object" - ] - }, - "description": { - "type": "string", - "description": "A meaningful description of the parameter. Used for agent discovery and human documentation." - }, - "pattern": { - "type": "string", - "description": "Optional regex pattern for validation. Only applicable when type is 'string'." - }, - "value": { - "type": "string", - "description": "Static value or expression resolved from the execution context." - } - }, - "required": [ - "name", - "in", - "type", - "description" - ], - "additionalProperties": false - }, - "ConsumedOutputParameter": { - "type": "object", - "description": "Output parameter of a consumed operation. Extracts a named value from the raw API response via JsonPath, for use in orchestration steps.", - "properties": { - "name": { - "$ref": "#/$defs/IdentifierExtended", - "description": "Name of the output parameter. It will be used as a reference in the mapping definitions of the steps to indicate where to map values to." - }, - "type": { - "type": "string", - "enum": [ - "string", - "number", - "boolean", - "object", - "array" - ] - }, - "value": { - "type": "string", - "description": "JsonPath expression against the raw response payload. E.g. $.results[*].id" - } - }, - "required": [ - "name", - "type", - "value" - ], - "unevaluatedProperties": false - }, - "MappedOutputParameterBase": { - "type": "object", - "description": "Base for an inline-mapped output parameter with recursive typing and JsonPath extraction.", - "properties": { - "type": { - "type": "string", - "enum": [ - "string", - "number", - "boolean", - "object", - "array" - ] - }, - "mapping": { - "type": "string", - "description": "JsonPath expression to extract the value. E.g. $.properties.Name.title[0].text.content" - }, - "const": { - "description": "Optional constant value. If provided, it will override the mapping and the value will be injected as-is.", - "oneOf": [ - { - "type": "string" - }, - { - "type": "number" - }, - { - "type": "boolean" - }, - { - "type": "array" - }, - { - "type": "object" - } - ] - } - }, - "required": [ - "type" - ] - }, - "MappedOutputParameterString": { - "allOf": [ - { - "$ref": "#/$defs/MappedOutputParameterBase" - }, - { - "properties": { - "type": { - "const": "string" - }, - "mapping": true, - "const": true - }, - "oneOf": [ - { - "required": [ - "mapping" - ] - }, - { - "required": [ - "const" - ] - } - ], - "unevaluatedProperties": false - } - ] - }, - "MappedOutputParameterNumber": { - "allOf": [ - { - "$ref": "#/$defs/MappedOutputParameterBase" - }, - { - "properties": { - "type": { - "const": "number" - }, - "mapping": true, - "const": true - }, - "oneOf": [ - { - "required": [ - "mapping" - ] - }, - { - "required": [ - "const" - ] - } - ], - "unevaluatedProperties": false - } - ] - }, - "MappedOutputParameterBoolean": { - "allOf": [ - { - "$ref": "#/$defs/MappedOutputParameterBase" - }, - { - "properties": { - "type": { - "const": "boolean" - }, - "mapping": true, - "const": true - }, - "oneOf": [ - { - "required": [ - "mapping" - ] - }, - { - "required": [ - "const" - ] - } - ], - "unevaluatedProperties": false - } - ] - }, - "MappedOutputParameterObject": { - "allOf": [ - { - "$ref": "#/$defs/MappedOutputParameterBase" - }, - { - "properties": { - "type": { - "const": "object" - }, - "mapping": true, - "const": true, - "properties": { - "type": "object", - "description": "A map of named properties. Each key is a property name, each value is a typed output parameter.", - "additionalProperties": { - "$ref": "#/$defs/MappedOutputParameter" - } - } - }, - "required": [ - "properties" - ], - "unevaluatedProperties": false - } - ] - }, - "MappedOutputParameterArray": { - "allOf": [ - { - "$ref": "#/$defs/MappedOutputParameterBase" - }, - { - "properties": { - "type": { - "const": "array" - }, - "mapping": true, - "const": true, - "items": { - "$ref": "#/$defs/MappedItemArray" - } - }, - "required": [ - "items" - ], - "unevaluatedProperties": false - } - ] - }, - "MappedItemArray": { - "type": "array", - "description": "Describes the shape of items inside an array output parameter.", - "items": { - "$ref": "#/$defs/MappedOutputParameter" - }, - "minItems": 1 - }, - "MappedOutputParameter": { - "description": "Inline-mapped output parameter (simple mode). Used when the exposed operation has a single call + with.", - "oneOf": [ - { - "$ref": "#/$defs/MappedOutputParameterString" - }, - { - "$ref": "#/$defs/MappedOutputParameterNumber" - }, - { - "$ref": "#/$defs/MappedOutputParameterBoolean" - }, - { - "$ref": "#/$defs/MappedOutputParameterObject" - }, - { - "$ref": "#/$defs/MappedOutputParameterArray" - } - ] - }, - "OrchestratedOutputParameterBase": { - "type": "object", - "description": "Base for a named output parameter whose value is populated by step mappings during orchestration.", - "properties": { - "name": { - "$ref": "#/$defs/IdentifierExtended", - "description": "Name of the output parameter. It will be used as a reference in the mapping definitions of the steps to indicate where to map values to." - }, - "type": { - "type": "string", - "enum": [ - "string", - "number", - "boolean", - "object", - "array" - ] - } - }, - "required": [ - "name", - "type" - ] - }, - "OrchestratedOutputParameterScalar": { - "allOf": [ - { - "$ref": "#/$defs/OrchestratedOutputParameterBase" - }, - { - "properties": { - "type": { - "enum": [ - "string", - "number", - "boolean" - ] - }, - "name": true - }, - "unevaluatedProperties": false - } - ] - }, - "OrchestratedOutputParameterArray": { - "allOf": [ - { - "$ref": "#/$defs/OrchestratedOutputParameterBase" - }, - { - "properties": { - "type": { - "const": "array" - }, - "name": true, - "items": { - "type": "array", - "items": { - "$ref": "#/$defs/OrchestratedOutputParameter" - }, - "minItems": 1 - } - }, - "required": [ - "items" - ], - "unevaluatedProperties": false - } - ] - }, - "OrchestratedOutputParameterObject": { - "allOf": [ - { - "$ref": "#/$defs/OrchestratedOutputParameterBase" - }, - { - "properties": { - "type": { - "const": "object" - }, - "name": true, - "properties": { - "type": "object", - "additionalProperties": { - "$ref": "#/$defs/OrchestratedOutputParameter" - } - } - }, - "required": [ - "properties" - ], - "unevaluatedProperties": false - } - ] - }, - "OrchestratedOutputParameter": { - "description": "Named output parameter (orchestrated mode). Used when the exposed operation has steps with mappings/lookups.", - "oneOf": [ - { - "$ref": "#/$defs/OrchestratedOutputParameterScalar" - }, - { - "$ref": "#/$defs/OrchestratedOutputParameterArray" - }, - { - "$ref": "#/$defs/OrchestratedOutputParameterObject" - } - ] - }, - "Hostname": { - "type": "string", - "description": "Valid hostname", - "pattern": "^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)(\\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$" - }, - "IPv4": { - "type": "string", - "description": "Valid IPv4 address", - "pattern": "^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$" - }, - "IPv6": { - "type": "string", - "description": "Valid IPv6 address (simplified)", - "pattern": "^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$" - }, - "Address": { - "description": "hostname or IP for the exposition", - "anyOf": [ - { - "$ref": "#/$defs/Hostname" - }, - { - "$ref": "#/$defs/IPv4" - }, - { - "$ref": "#/$defs/IPv6" - } - ] - }, - "Info": { - "type": "object", - "description": "Information about the capability", - "properties": { - "label": { - "type": "string", - "description": "The display name of the capability" - }, - "description": { - "type": "string", - "description": "A description of the capability (the more meaningful, the easier for agents discovery)" - }, - "tags": { - "type": "array", - "description": "List of tags to help categorize the capability. It can be used for discovery and filtering purposes.", - "items": { - "type": "string" - } - }, - "created": { - "type": "string", - "pattern": "^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])$", - "description": "The date when the capability was created" - }, - "modified": { - "type": "string", - "pattern": "^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])$", - "description": "The date when the capability was last modified" - }, - "stakeholders": { - "type": "array", - "description": "List of stakeholders related to this capability. It can be used for discovery and filtering purposes.", - "items": { - "$ref": "#/$defs/Person" - } - } - }, - "required": [ - "label", - "description" - ], - "additionalProperties": false - }, - "Person": { - "type": "object", - "description": "A person related to the capability", - "properties": { - "role": { - "type": "string", - "description": "The role of the person in relation to the capability. E.g. owner, editor, viewer" - }, - "fullName": { - "type": "string", - "description": "The full name of the person" - }, - "email": { - "type": "string", - "pattern": "^[a-zA-Z0-9._%+\\-]+@[a-zA-Z0-9.\\-]+\\.[a-zA-Z]{2,}$", - "description": "The email address of the person" - } - }, - "required": [ - "role", - "fullName" - ], - "additionalProperties": false - }, - "Capability": { - "type": "object", - "description": "Capability technical configuration", - "properties": { - "exposes": { - "type": "array", - "description": "List of exposed server adapters", - "items": { - "oneOf": [ - { - "$ref": "#/$defs/ExposesApi" - }, - { - "$ref": "#/$defs/ExposesMcp" - } - ] - }, - "minItems": 1 - }, - "consumes": { - "type": "array", - "description": "Consumed client adapters", - "items": { - "$ref": "#/$defs/ConsumesHttp" - }, - "minItems": 1 - } - }, - "anyOf": [ - { - "required": [ - "exposes" - ] - }, - { - "required": [ - "consumes" - ] - }, - { - "required": [ - "exposes", - "consumes" - ] - } - ], - "additionalProperties": false - }, - "ExposesApi": { - "type": "object", - "description": "API exposition configuration", - "properties": { - "type": { - "type": "string", - "const": "api" - }, - "address": { - "$ref": "#/$defs/Address" - }, - "port": { - "type": "integer", - "minimum": 1, - "maximum": 65535 - }, - "namespace": { - "$ref": "#/$defs/IdentifierKebab", - "description": "Unique identifier for this exposed API" - }, - "authentication": { - "$ref": "#/$defs/Authentication" - }, - "resources": { - "type": "array", - "description": "List of exposed resources", - "items": { - "$ref": "#/$defs/ExposedResource" - }, - "minItems": 1 - } - }, - "required": [ - "type", - "port", - "namespace" - ], - "additionalProperties": false - }, - "ExposesMcp": { - "type": "object", - "description": "MCP Server exposition configuration. Exposes tools over MCP transport (Streamable HTTP or stdio).", - "properties": { - "type": { - "type": "string", - "const": "mcp" - }, - "transport": { - "type": "string", - "enum": [ - "http", - "stdio" - ], - "default": "http", - "description": "The MCP transport to use. 'http' (default) exposes a Streamable HTTP server; 'stdio' uses stdin/stdout JSON-RPC for local IDE integration." - }, - "address": { - "$ref": "#/$defs/Address" - }, - "port": { - "type": "integer", - "minimum": 1, - "maximum": 65535 - }, - "namespace": { - "$ref": "#/$defs/IdentifierKebab", - "description": "Unique identifier for this exposed MCP server" - }, - "description": { - "type": "string", - "description": "A meaningful description of this MCP server's purpose. Used as the server instructions sent during MCP initialization." - }, - "tools": { - "type": "array", - "description": "List of MCP tools exposed by this server", - "items": { - "$ref": "#/$defs/McpTool" - }, - "minItems": 1 - } - }, - "required": [ - "type", - "namespace", - "tools" - ], - "oneOf": [ - { - "properties": { - "transport": { - "const": "stdio" - } - }, - "required": [ - "transport" - ], - "not": { - "required": [ - "port" - ] - } - }, - { - "properties": { - "transport": { - "const": "http" - } - }, - "required": [ - "port" - ] - } - ], - "additionalProperties": false - }, - "McpTool": { - "type": "object", - "description": "An MCP tool definition. Each tool maps to one or more consumed HTTP operations.", - "properties": { - "name": { - "$ref": "#/$defs/IdentifierKebab", - "description": "Technical name for the tool. Used as the MCP tool name." - }, - "description": { - "type": "string", - "description": "A meaningful description of the tool. Used for agent discovery. Remember, in a world of agents, context is king." - }, - "inputParameters": { - "type": "array", - "description": "Tool input parameters. These become the MCP tool's input schema (JSON Schema).", - "items": { - "$ref": "#/$defs/McpToolInputParameter" - } - }, - "call": { - "type": "string", - "description": "For simple cases, the reference to the consumed operation. Format: {namespace}.{operationId}.", - "pattern": "^[a-zA-Z0-9-]+\\.[a-zA-Z0-9-]+$" - }, - "with": { - "$ref": "#/$defs/WithInjector" - }, - "steps": { - "type": "array", - "items": { - "$ref": "#/$defs/OperationStep" - }, - "minItems": 1 - }, - "mappings": { - "type": "array", - "description": "Maps step outputs to the tool's output parameters.", - "items": { - "$ref": "#/$defs/StepOutputMapping" - } - }, - "outputParameters": { - "type": "array" - } - }, - "required": [ - "name", - "description" - ], - "anyOf": [ - { - "required": [ - "call" - ], - "type": "object", - "properties": { - "outputParameters": { - "type": "array", - "items": { - "$ref": "#/$defs/MappedOutputParameter" - } - } - } - }, - { - "required": [ - "steps" - ], - "type": "object", - "properties": { - "mappings": true, - "outputParameters": { - "type": "array", - "items": { - "$ref": "#/$defs/OrchestratedOutputParameter" - } - } - } - } - ], - "additionalProperties": false - }, - "McpToolInputParameter": { - "type": "object", - "description": "Declares an input parameter for an MCP tool. These become properties in the tool's JSON Schema input definition.", - "properties": { - "name": { - "$ref": "#/$defs/IdentifierExtended", - "description": "Name of the parameter. Becomes a property name in the tool's input schema." - }, - "type": { - "type": "string", - "description": "The expected data type of the parameter.", - "enum": [ - "string", - "number", - "integer", - "boolean", - "array", - "object" - ] - }, - "description": { - "type": "string", - "description": "A meaningful description of the parameter. Used for agent discovery and tool documentation." - }, - "required": { - "type": "boolean", - "description": "Whether the parameter is required. Defaults to true.", - "default": true - } - }, - "required": [ - "name", - "type", - "description" - ], - "additionalProperties": false - }, - "ExposedResource": { - "type": "object", - "description": "An exposed resource", - "properties": { - "label": { - "type": "string", - "description": "Display name for the resource. It will likely be used in UIs." - }, - "description": { - "type": "string", - "description": "Used to provide *meaningful* information about the resource. Remember, in a world of agents, context is king." - }, - "path": { - "type": "string", - "description": "Path of the resource (supports {{param}} placeholders)" - }, - "name": { - "$ref": "#/$defs/IdentifierKebab", - "description": "Technical name for the resource. It will likely be used for reference" - }, - "inputParameters": { - "type": "array", - "items": { - "$ref": "#/$defs/ExposedInputParameter" - } - }, - "operations": { - "type": "array", - "description": "Operations available on this resource", - "items": { - "$ref": "#/$defs/ExposedOperation" - } - }, - "forward": { - "$ref": "#/$defs/ForwardConfig" - } - }, - "required": [ - "path" - ], - "anyOf": [ - { - "required": [ - "operations" - ] - }, - { - "required": [ - "forward" - ] - } - ], - "additionalProperties": false - }, - "ExposedOperation": { - "type": "object", - "description": "An operation exposed on a resource.", - "properties": { - "name": { - "$ref": "#/$defs/IdentifierKebab", - "description": "Technical name for the operation. Used for reference." - }, - "label": { - "type": "string", - "description": "Display name for the operation. Used in UIs." - }, - "description": { - "type": "string", - "description": "Used to provide *meaningful* information about the operation. Remember, in a world of agents, context is king." - }, - "method": { - "type": "string", - "enum": [ - "GET", - "POST", - "PUT", - "PATCH", - "DELETE" - ], - "default": "GET" - }, - "inputParameters": { - "type": "array", - "items": { - "$ref": "#/$defs/ExposedInputParameter" - } - }, - "call": { - "type": "string", - "description": "For simple cases, the reference to the consumed operation. Format: {namespace}.{operationId}. E.g.: notion.get-database or github.get-user", - "pattern": "^[a-zA-Z0-9-]+\\.[a-zA-Z0-9-]+$" - }, - "with": { - "$ref": "#/$defs/WithInjector" - }, - "outputParameters": { - "type": "array" - }, - "steps": { - "type": "array", - "items": { - "$ref": "#/$defs/OperationStep" - }, - "minItems": 1 - }, - "mappings": { - "type": "array", - "description": "Maps step outputs to the operation's output parameters.", - "items": { - "$ref": "#/$defs/StepOutputMapping" - } - } - }, - "required": [ - "method" - ], - "anyOf": [ - { - "properties": { - "outputParameters": { - "items": { - "$ref": "#/$defs/MappedOutputParameter" - } - } - } - }, - { - "required": [ - "steps", - "name" - ], - "properties": { - "mappings": true, - "outputParameters": { - "items": { - "$ref": "#/$defs/OrchestratedOutputParameter" - } - } - } - } - ], - "additionalProperties": false - }, - "WithInjector": { - "type": "object", - "description": "A map of key-value pairs that binds input parameters of the called operation to values or expressions resolved from the execution context.", - "additionalProperties": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "number" - } - ] - } - }, - "ConsumesHttp": { - "type": "object", - "description": "HTTP API consumption configuration", - "properties": { - "namespace": { - "$ref": "#/$defs/IdentifierKebab", - "description": "Unique identifier for this consumed API" - }, - "type": { - "type": "string", - "const": "http" - }, - "description": { - "type": "string", - "description": "Used to provide *meaningful* information about the operation. Remember, in a world of agents, context is king." - }, - "baseUri": { - "type": "string", - "description": "Target URI for the consumes API", - "pattern": "^https?://[^/]+(/[^{]*)?$" - }, - "inputParameters": { - "type": "array", - "items": { - "$ref": "#/$defs/ConsumedInputParameter" - } - }, - "authentication": { - "$ref": "#/$defs/Authentication" - }, - "resources": { - "type": "array", - "description": "List of consumed resources", - "items": { - "$ref": "#/$defs/ConsumedHttpResource" - }, - "minItems": 1 - } - }, - "required": [ - "namespace", - "type", - "baseUri" - ], - "additionalProperties": false - }, - "ConsumedHttpResource": { - "type": "object", - "description": "A consumed HTTP resource", - "properties": { - "name": { - "$ref": "#/$defs/IdentifierKebab", - "description": "Technical name for the resource. It will likely be used for reference" - }, - "label": { - "type": "string" - }, - "description": { - "type": "string" - }, - "path": { - "type": "string" - }, - "inputParameters": { - "type": "array", - "items": { - "$ref": "#/$defs/ConsumedInputParameter" - } - }, - "operations": { - "type": "array", - "description": "Operations on this resource", - "items": { - "$ref": "#/$defs/ConsumedHttpOperation" - }, - "minItems": 1 - } - }, - "required": [ - "name", - "path", - "operations" - ], - "additionalProperties": false - }, - "ConsumedHttpOperation": { - "type": "object", - "description": "An operation on a consumed HTTP resource", - "properties": { - "name": { - "$ref": "#/$defs/IdentifierKebab", - "description": "Technical name for the operation. Used for reference." - }, - "label": { - "type": "string" - }, - "description": { - "type": "string", - "description": "Used to provide *meaningful* information about the operation. Remember, in a world of agents, context is king." - }, - "method": { - "type": "string", - "enum": [ - "GET", - "POST", - "PUT", - "PATCH", - "DELETE" - ], - "default": "GET" - }, - "inputParameters": { - "type": "array", - "items": { - "$ref": "#/$defs/ConsumedInputParameter" - } - }, - "outputRawFormat": { - "type": "string", - "enum": [ - "json", - "xml", - "avro", - "protobuf", - "csv", - "yaml" - ], - "default": "json", - "description": "The raw format of the response from the consumed API. It is used to determine how to parse the response and extract the output parameters. Default value is json." - }, - "outputParameters": { - "type": "array", - "items": { - "$ref": "#/$defs/ConsumedOutputParameter" - } - }, - "body": { - "$ref": "#/$defs/RequestBody" - } - }, - "required": [ - "name", - "method" - ], - "additionalProperties": false - }, - "OperationStepBase": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "call", - "lookup" - ] - }, - "name": { - "$ref": "#/$defs/IdentifierKebab", - "description": "Technical name for the step. Used as namespace for referencing step outputs in mappings and expressions." - } - }, - "required": [ - "type", - "name" - ] - }, - "OperationStepCall": { - "allOf": [ - { - "$ref": "#/$defs/OperationStepBase" - }, - { - "properties": { - "type": { - "const": "call" - }, - "name": true, - "call": { - "type": "string", - "description": "Reference to the consumed operation. Format: {namespace}.{operationId}.", - "pattern": "^[a-zA-Z0-9-]+\\.[a-zA-Z0-9-]+$" - }, - "with": { - "$ref": "#/$defs/WithInjector" - } - }, - "required": [ - "call" - ], - "unevaluatedProperties": false - } - ] - }, - "OperationStepLookup": { - "allOf": [ - { - "$ref": "#/$defs/OperationStepBase" - }, - { - "properties": { - "type": { - "const": "lookup" - }, - "name": true, - "index": { - "$ref": "#/$defs/IdentifierKebab", - "description": "Name of a previous call step whose output serves as the lookup table." - }, - "match": { - "$ref": "#/$defs/IdentifierKebab", - "description": "Name of the key field in the index to match against." - }, - "lookupValue": { - "type": "string", - "description": "JsonPath expression resolving to the value(s) to look up." - }, - "outputParameters": { - "type": "array", - "description": "List of field names to extract from the matched index entries.", - "items": { - "type": "string" - }, - "minItems": 1 - } - }, - "required": [ - "index", - "match", - "lookupValue", - "outputParameters" - ], - "unevaluatedProperties": false - } - ] - }, - "OperationStep": { - "oneOf": [ - { - "$ref": "#/$defs/OperationStepCall" - }, - { - "$ref": "#/$defs/OperationStepLookup" - } - ] - }, - "StepOutputMapping": { - "type": "object", - "description": "Describes how to map the output of an operation step to the input of another step or to the output of the exposed operation", - "properties": { - "targetName": { - "$ref": "#/$defs/IdentifierExtended", - "description": "The name of the parameter to map to. It can be an input parameter of a next step or an output parameter of the exposed operation" - }, - "value": { - "type": "string", - "description": "A JsonPath reference to the value to map from. E.g. $.get-database.database_id" - } - }, - "required": [ - "targetName", - "value" - ], - "additionalProperties": false - }, - "RequestBodyJson": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "json" - }, - "data": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "object" - }, - { - "type": "array" - } - ] - } - }, - "required": [ - "type", - "data" - ], - "additionalProperties": false - }, - "RequestBodyText": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "text", - "xml", - "sparql" - ] - }, - "data": { - "type": "string" - } - }, - "required": [ - "type", - "data" - ], - "additionalProperties": false - }, - "RequestBodyFormUrlEncoded": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "formUrlEncoded" - }, - "data": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - ] - } - }, - "required": [ - "type", - "data" - ], - "additionalProperties": false - }, - "RequestBodyMultipartFormPart": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "value": { - "type": "string" - }, - "filename": { - "type": "string" - }, - "contentType": { - "type": "string" - } - }, - "required": [ - "name", - "value" - ], - "additionalProperties": false - }, - "RequestBodyMultipartForm": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "multipartForm" - }, - "data": { - "type": "array", - "items": { - "$ref": "#/$defs/RequestBodyMultipartFormPart" - } - } - }, - "required": [ - "type", - "data" - ], - "additionalProperties": false - }, - "RequestBodyRaw": { - "type": "string", - "description": "A raw body string. When used with a YAML block scalar (|), the string is sent as-is. Interpreted as JSON by default." - }, - "RequestBody": { - "description": "Request body configuration", - "oneOf": [ - { - "$ref": "#/$defs/RequestBodyJson" - }, - { - "$ref": "#/$defs/RequestBodyText" - }, - { - "$ref": "#/$defs/RequestBodyFormUrlEncoded" - }, - { - "$ref": "#/$defs/RequestBodyMultipartForm" - }, - { - "$ref": "#/$defs/RequestBodyRaw" - } - ] - }, - "ForwardConfig": { - "type": "object", - "description": "Configuration for forwarding requests to a consumed namespace", - "properties": { - "targetNamespace": { - "$ref": "#/$defs/IdentifierKebab", - "description": "The namespace to forward requests to" - }, - "trustedHeaders": { - "type": "array", - "items": { - "type": "string" - }, - "description": "List of headers that can be forwarded", - "minItems": 1 - } - }, - "required": [ - "targetNamespace" - ], - "additionalProperties": false - }, - "Authentication": { - "description": "Authentication", - "oneOf": [ - { - "$ref": "#/$defs/AuthBasic" - }, - { - "$ref": "#/$defs/AuthApiKey" - }, - { - "$ref": "#/$defs/AuthBearer" - }, - { - "$ref": "#/$defs/AuthDigest" - } - ] - }, - "AuthBasic": { - "type": "object", - "description": "Basic authentication", - "properties": { - "type": { - "type": "string", - "const": "basic" - }, - "username": { - "type": "string", - "description": "Username for basic auth" - }, - "password": { - "type": "string", - "description": "Password for basic auth" - } - }, - "required": [ - "type" - ], - "additionalProperties": false - }, - "AuthApiKey": { - "type": "object", - "description": "API Key authentication", - "properties": { - "type": { - "type": "string", - "const": "apikey" - }, - "key": { - "type": "string", - "description": "API key name" - }, - "value": { - "type": "string", - "description": "API key value" - }, - "placement": { - "type": "string", - "enum": [ - "header", - "query" - ], - "description": "Where to place the API key" - } - }, - "required": [ - "type" - ], - "additionalProperties": false - }, - "AuthBearer": { - "type": "object", - "description": "Bearer token authentication", - "properties": { - "type": { - "type": "string", - "const": "bearer" - }, - "token": { - "type": "string", - "description": "Bearer token" - } - }, - "required": [ - "type" - ], - "additionalProperties": false - }, - "AuthDigest": { - "type": "object", - "description": "Digest authentication", - "properties": { - "type": { - "type": "string", - "const": "digest" - }, - "username": { - "type": "string", - "description": "Username for digest auth" - }, - "password": { - "type": "string", - "description": "Password for digest auth" - } - }, - "required": [ - "type" - ], - "additionalProperties": false - } - } -} \ No newline at end of file diff --git a/src/main/resources/schemas/naftiko-schema.json b/src/main/resources/schemas/naftiko-schema.json index ab859a10..f22505ef 100644 --- a/src/main/resources/schemas/naftiko-schema.json +++ b/src/main/resources/schemas/naftiko-schema.json @@ -267,7 +267,7 @@ "description": "JsonPath expression to extract the value. E.g. $.properties.Name.title[0].text.content" }, "const": { - "description": "Optional constant value. If provided, it will override the mapping and the value will be injected as-is.", + "description": "Schema-level constant for validation and linting. Not used at runtime for value injection — use 'value' instead.", "oneOf": [ { "type": "string" @@ -285,6 +285,10 @@ "type": "object" } ] + }, + "value": { + "type": "string", + "description": "Static value injected at runtime. When provided, takes precedence over mapping." } }, "required": [ @@ -302,7 +306,8 @@ "const": "string" }, "mapping": true, - "const": true + "const": true, + "value": true }, "oneOf": [ { @@ -312,7 +317,7 @@ }, { "required": [ - "const" + "value" ] } ], @@ -331,7 +336,8 @@ "const": "number" }, "mapping": true, - "const": true + "const": true, + "value": true }, "oneOf": [ { @@ -341,7 +347,7 @@ }, { "required": [ - "const" + "value" ] } ], @@ -360,7 +366,8 @@ "const": "boolean" }, "mapping": true, - "const": true + "const": true, + "value": true }, "oneOf": [ { @@ -370,7 +377,7 @@ }, { "required": [ - "const" + "value" ] } ], @@ -390,6 +397,7 @@ }, "mapping": true, "const": true, + "value": true, "properties": { "type": "object", "description": "A map of named properties. Each key is a property name, each value is a typed output parameter.", @@ -417,6 +425,7 @@ }, "mapping": true, "const": true, + "value": true, "items": { "$ref": "#/$defs/MappedOutputParameter" } diff --git a/src/main/resources/wiki/FAQ.md b/src/main/resources/wiki/FAQ.md index d726e8f5..5b4f0941 100644 --- a/src/main/resources/wiki/FAQ.md +++ b/src/main/resources/wiki/FAQ.md @@ -446,7 +446,7 @@ exposes: MCP clients can then discover and use these resources dynamically. ### Q: Can I create MCP tools that return static mock data? -**A:** Yes, starting in version 1.0.0 Alpha 2. Define `outputParameters` with `const` values and omit `call` and `steps`. The tool serves a fixed JSON response — no `consumes` block is needed: +**A:** Yes, starting in version 1.0.0 Alpha 2. Define `outputParameters` with `value` fields and omit `call` and `steps`. The tool serves a fixed JSON response — no `consumes` block is needed. Values can be static strings or Mustache templates resolved against input parameters: ```yaml exposes: @@ -465,7 +465,7 @@ exposes: outputParameters: - name: message type: string - const: "Hello, World!" + value: "Hello, {{name}}!" ``` This mirrors the REST mock pattern (`no-adapter.yml`) and is useful for prototyping, demos, and contract-first development. diff --git "a/src/main/resources/wiki/Guide-\342\200\220-Use-Cases.md" "b/src/main/resources/wiki/Guide-\342\200\220-Use-Cases.md" index 63fbd1c6..4805972a 100644 --- "a/src/main/resources/wiki/Guide-\342\200\220-Use-Cases.md" +++ "b/src/main/resources/wiki/Guide-\342\200\220-Use-Cases.md" @@ -23,7 +23,7 @@ How Naftiko achieves this technically: - [x] REST resource exposure for conventional clients - [x] Output shaping with typed parameters and JSONPath mapping - [x] Nested object and array output parameters - - [x] Const values for computed fields + - [x] Static values for computed fields - [x] Externalized secrets via `binds` (file, vault, environment) ## 2. Rightsize AI context @@ -45,7 +45,7 @@ How Naftiko achieves this technically: - [x] MCP for Standard IO (for local MCP clients) - [x] Output shaping with typed parameters and JSONPath mapping - [x] Fine-grained field selection and nested object mapping - - [x] Const values to inject static context + - [x] Static values to inject context - [x] Typed MCP tool input parameters with descriptions - [x] Required/optional parameter declarations for agent discovery @@ -183,7 +183,8 @@ How Naftiko achieves this technically: - [x] MCP for Streaming HTTP (for remote MCP clients) - [x] MCP for Standard IO (for local MCP clients) - [x] Mock mode for MCP tools - - [x] Static const-valued outputs without consuming an API + - [x] Static value outputs without consuming an API + - [x] Dynamic mock values using Mustache templates from input parameters - [x] Multi-step orchestration wired to consumed operations - [x] Call and lookup steps with cross-step output bridging diff --git a/src/main/resources/wiki/Installation.md b/src/main/resources/wiki/Installation.md index 1d1c870b..425260a7 100644 --- a/src/main/resources/wiki/Installation.md +++ b/src/main/resources/wiki/Installation.md @@ -31,7 +31,7 @@ To use Naftiko Framework, you must install and then run its engine. * If your capability refers to some local hosts, be careful to not use 'localhost', but 'host.docker.internal' instead. This is because your capability will run into an isolated docker container, so 'localhost' will refer to the container and not your local machine.\ For example: ```bash - baseUri: "http://host.docker.internal:8080/api/" + baseUri: "http://host.docker.internal:8080/api" ``` * In the same way, if your capability expose a local host, be careful to not use 'localhost', but '0.0.0.0' instead. Else requests to localhost coming from outside of the container won't succeed.\ For example: diff --git "a/src/main/resources/wiki/Specification-\342\200\220-Schema.md" "b/src/main/resources/wiki/Specification-\342\200\220-Schema.md" index 41a380dd..7fecdb93 100644 --- "a/src/main/resources/wiki/Specification-\342\200\220-Schema.md" +++ "b/src/main/resources/wiki/Specification-\342\200\220-Schema.md" @@ -1295,24 +1295,25 @@ outputParameters: #### 3.12.2 MappedOutputParameter Object -Used in **simple mode** exposed operations. Maps a value from the consumed response using `type` and `mapping`. +Used in **simple mode** exposed operations. Maps a value from the consumed response using `type` and `mapping`, or provides a static value using `type` and `value`. **Fixed Fields:** | Field Name | Type | Description | | --- | --- | --- | | **type** | `string` | **REQUIRED**. Data type. One of: `string`, `number`, `boolean`, `object`, `array`. | -| **mapping** | `string` | `object` | **REQUIRED**. For scalar types (`string`, `number`, `boolean`): a JsonPath string. For `object`: an object with `properties` (recursive MappedOutputParameter map). For `array`: an object with `items` (recursive MappedOutputParameter). | +| **mapping** | `string` \| `object` | **Conditionally required**. JsonPath expression (scalar types) or recursive structure (`object`/`array`). Required unless `value` is provided. | +| **value** | `string` | **Conditionally required**. Static value or Mustache template injected at runtime. Required unless `mapping` is provided. Supports `{{paramName}}` placeholders resolved against input parameters. Used for mock responses and prototyping without a `consumes` block. | **Subtypes by type:** -- **`string`**, **`number`**, **`boolean`**: `mapping` is a JSONPath string (e.g. `$.login`) +- **`string`**, **`number`**, **`boolean`**: `mapping` is a JSONPath string (e.g. `$.login`), or `value` is a static string or Mustache template (e.g. `"Hello, {{name}}!"`) - **`object`**: `mapping` is `{ properties: { key: MappedOutputParameter, ... } }` — recursive - **`array`**: `mapping` is `{ items: MappedOutputParameter }` — recursive **Rules:** -- Both `type` and `mapping` are mandatory. +- `type` is mandatory. Either `mapping` or `value` must be provided, but not both. - No additional properties are allowed. **MappedOutputParameter Examples:** @@ -1325,6 +1326,16 @@ outputParameters: - type: number mapping: $.id +# Static value (mock / prototyping) +outputParameters: + - type: string + value: "Hello, World!" + +# Dynamic value using Mustache template +outputParameters: + - type: string + value: "Hello, {{name}}!" + # Object mapping (recursive) outputParameters: - type: object diff --git a/src/test/java/io/naftiko/engine/ResolverTest.java b/src/test/java/io/naftiko/engine/ResolverTest.java index db45417f..7567b6c1 100644 --- a/src/test/java/io/naftiko/engine/ResolverTest.java +++ b/src/test/java/io/naftiko/engine/ResolverTest.java @@ -159,7 +159,7 @@ public void resolveInputParameterFromRequestShouldSupportConstantAndBodyFallback InputParameterSpec constant = new InputParameterSpec(); constant.setName("x"); - constant.setConstant("fixed"); + constant.setValue("fixed"); constant.setIn("query"); InputParameterSpec missingBodyPath = new InputParameterSpec(); @@ -196,7 +196,7 @@ public void resolveInputParametersToRequestShouldApplyHeaderQueryAndTemplateReso InputParameterSpec constant = new InputParameterSpec(); constant.setName("X-Mode"); constant.setIn("header"); - constant.setConstant("strict"); + constant.setValue("strict"); Resolver.resolveInputParametersToRequest(clientRequest, List.of(header, query, constant), parameters); @@ -295,6 +295,27 @@ public void resolveOutputMappingsShouldHandleObjectValuesAndPrimitiveFallback() assertEquals("xyz", primitiveValue.asText()); } + @Test + public void resolveOutputMappingsShouldResolveMustacheTemplatesInValue() { + OutputParameterSpec spec = new OutputParameterSpec(); + spec.setType("string"); + spec.setValue("Hello, {{name}}!"); + + Map params = Map.of("name", "Voyager"); + JsonNode result = Resolver.resolveOutputMappings(spec, null, MAPPER, params); + assertEquals("Hello, Voyager!", result.asText()); + } + + @Test + public void resolveOutputMappingsShouldReturnRawValueWhenNoParameters() { + OutputParameterSpec spec = new OutputParameterSpec(); + spec.setType("string"); + spec.setValue("static-text"); + + JsonNode result = Resolver.resolveOutputMappings(spec, null, MAPPER, null); + assertEquals("static-text", result.asText()); + } + /** * Regression test for #213: array parameters in Mustache body templates must be * JSON-serialized, not converted via toString(). diff --git a/src/test/java/io/naftiko/engine/exposes/OutputMappingExtensionTest.java b/src/test/java/io/naftiko/engine/exposes/OutputMappingExtensionTest.java index 53c3fa57..df3262dc 100644 --- a/src/test/java/io/naftiko/engine/exposes/OutputMappingExtensionTest.java +++ b/src/test/java/io/naftiko/engine/exposes/OutputMappingExtensionTest.java @@ -36,7 +36,7 @@ private JsonNode invokeBuild(OutputParameterSpec spec, JsonNode root) throws Exc public void testConstTakesPrecedence() throws Exception { OutputParameterSpec spec = new OutputParameterSpec(); spec.setType("string"); - spec.setConstant("CONST_VAL"); + spec.setValue("CONST_VAL"); JsonNode result = invokeBuild(spec, mapper.readTree("{}")); assertTrue(result.isTextual()); diff --git a/src/test/java/io/naftiko/engine/exposes/mcp/ToolHandlerTest.java b/src/test/java/io/naftiko/engine/exposes/mcp/ToolHandlerTest.java index 7915b474..9cf490d9 100644 --- a/src/test/java/io/naftiko/engine/exposes/mcp/ToolHandlerTest.java +++ b/src/test/java/io/naftiko/engine/exposes/mcp/ToolHandlerTest.java @@ -42,29 +42,30 @@ public void handleToolCallShouldThrowForUnknownTool() { } @Test - public void handleToolCallShouldHandleNullArguments() { + public void handleToolCallShouldHandleNullArguments() throws Exception { McpServerToolSpec tool = new McpServerToolSpec(); tool.setName("test-tool"); tool.setWith(Map.of("default_param", "default_value")); - // This will fail at execution because we have no real capability/steps setup, - // but it tests that null arguments are handled gracefully before that point - + // No call or steps — mock mode returns an empty JSON object + ToolHandler handler = new ToolHandler(null, List.of(tool)); - assertThrows(Exception.class, () -> handler.handleToolCall("test-tool", null)); + var result = handler.handleToolCall("test-tool", null); + assertNotNull(result); } @Test - public void handleToolCallShouldMergeToolWithParameters() { + public void handleToolCallShouldMergeToolWithParameters() throws Exception { McpServerToolSpec tool = new McpServerToolSpec(); tool.setName("test-tool"); tool.setWith(Map.of("fromTool", "fromToolValue")); ToolHandler handler = new ToolHandler(null, List.of(tool)); - // Execution will fail beyond argument merging, but the tool is properly set up - assertThrows(Exception.class, () -> handler.handleToolCall("test-tool", - Map.of("fromArgs", "fromArgsValue"))); + // No call or steps — mock mode returns an empty JSON object + var result = handler.handleToolCall("test-tool", + Map.of("fromArgs", "fromArgsValue")); + assertNotNull(result); } /** diff --git a/src/test/java/io/naftiko/engine/exposes/rest/AuthenticationIntegrationTest.java b/src/test/java/io/naftiko/engine/exposes/rest/AuthenticationIntegrationTest.java index ff4ee43e..bdd95a30 100644 --- a/src/test/java/io/naftiko/engine/exposes/rest/AuthenticationIntegrationTest.java +++ b/src/test/java/io/naftiko/engine/exposes/rest/AuthenticationIntegrationTest.java @@ -55,7 +55,7 @@ public void bearerAuthenticationShouldReturnUnauthorizedWithoutTokenAndOkWithTok - method: "GET" outputParameters: - type: "string" - const: "ok" + value: "ok" """; Restlet root = buildRootRestlet(yaml); @@ -78,7 +78,7 @@ public void bearerAuthenticationShouldReturnUnauthorizedWithoutTokenAndOkWithTok assertEquals(MediaType.APPLICATION_JSON, authorizedResponse.getEntity().getMediaType(), "Output mapping should return json payload"); String payload = authorizedResponse.getEntity().getText(); - assertTrue(payload.contains("ok"), "Payload should contain mapped const output"); + assertTrue(payload.contains("ok"), "Payload should contain mapped output value"); } @Test @@ -107,7 +107,7 @@ public void apiKeyAuthenticationShouldReturnUnauthorizedWithoutHeaderAndOkWithHe - method: "GET" outputParameters: - type: "string" - const: "ok" + value: "ok" """; Restlet root = buildRootRestlet(yaml); diff --git a/src/test/java/io/naftiko/engine/exposes/rest/ResourceRestletTest.java b/src/test/java/io/naftiko/engine/exposes/rest/ResourceRestletTest.java index 50b4fd3a..304f6207 100644 --- a/src/test/java/io/naftiko/engine/exposes/rest/ResourceRestletTest.java +++ b/src/test/java/io/naftiko/engine/exposes/rest/ResourceRestletTest.java @@ -19,6 +19,7 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import java.net.ServerSocket; +import java.util.Map; import org.junit.jupiter.api.Test; import org.restlet.Application; import org.restlet.Component; @@ -35,6 +36,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import io.naftiko.Capability; +import io.naftiko.engine.Resolver; import io.naftiko.engine.exposes.OperationStepExecutor; import io.naftiko.spec.NaftikoSpec; import io.naftiko.spec.OutputParameterSpec; @@ -68,7 +70,7 @@ public void handleShouldBuildMockResponseFromConstOutputParameters() throws Exce tags.setType("array"); OutputParameterSpec tagItem = new OutputParameterSpec(); tagItem.setType("string"); - tagItem.setConstant("active"); + tagItem.setValue("active"); tags.setItems(tagItem); payload.getProperties().add(tags); operation.getOutputParameters().add(payload); @@ -76,7 +78,7 @@ public void handleShouldBuildMockResponseFromConstOutputParameters() throws Exce Request request = new Request(Method.GET, "http://localhost/preview"); Response response = new Response(request); - restlet.sendMockResponse(operation, response); + restlet.sendMockResponse(operation, response, Map.of()); assertEquals(Status.SUCCESS_OK, response.getStatus()); assertNotNull(response.getEntity()); @@ -208,7 +210,7 @@ public void canBuildMockResponseShouldCheckNestedConstValues() throws Exception OutputParameterSpec child = new OutputParameterSpec(); child.setName("status"); child.setType("string"); - child.setConstant("ok"); + child.setValue("ok"); root.getProperties().add(child); nestedConst.getOutputParameters().add(root); @@ -216,13 +218,7 @@ public void canBuildMockResponseShouldCheckNestedConstValues() throws Exception } @Test - public void buildParameterValueShouldHandleObjectArrayAndPrimitiveFallback() throws Exception { - Capability capability = capabilityFromYaml(minimalCapabilityYaml()); - RestServerSpec serverSpec = (RestServerSpec) capability.getServerAdapters().get(0) - .getSpec(); - ResourceRestlet restlet = new ResourceRestlet(capability, serverSpec, - serverSpec.getResources().get(0)); - + public void buildMockValueShouldHandleObjectArrayAndPrimitiveFallback() throws Exception { ObjectMapper mapper = new ObjectMapper(); OutputParameterSpec objectParam = new OutputParameterSpec(); @@ -230,26 +226,26 @@ public void buildParameterValueShouldHandleObjectArrayAndPrimitiveFallback() thr OutputParameterSpec field = new OutputParameterSpec(); field.setName("status"); field.setType("string"); - field.setConstant("ok"); + field.setValue("ok"); objectParam.getProperties().add(field); - JsonNode objectNode = restlet.buildParameterValue(objectParam, mapper); + JsonNode objectNode = Resolver.buildMockValue(objectParam, mapper, Map.of()); assertEquals("ok", objectNode.path("status").asText()); OutputParameterSpec arrayParam = new OutputParameterSpec(); arrayParam.setType("array"); OutputParameterSpec item = new OutputParameterSpec(); item.setType("string"); - item.setConstant("v"); + item.setValue("v"); arrayParam.setItems(item); - JsonNode arrayNode = restlet.buildParameterValue(arrayParam, mapper); + JsonNode arrayNode = Resolver.buildMockValue(arrayParam, mapper, Map.of()); assertTrue(arrayNode.isArray()); assertEquals("v", arrayNode.get(0).asText()); OutputParameterSpec primitiveNoConst = new OutputParameterSpec(); primitiveNoConst.setType("string"); - JsonNode primitive = restlet.buildParameterValue(primitiveNoConst, mapper); + JsonNode primitive = Resolver.buildMockValue(primitiveNoConst, mapper, Map.of()); assertTrue(primitive.isNull()); } @@ -407,6 +403,20 @@ private static int findFreePort() throws Exception { } } + @Test + public void buildMockValueShouldResolveMustacheTemplatesInValue() throws Exception { + ObjectMapper mapper = new ObjectMapper(); + + OutputParameterSpec param = new OutputParameterSpec(); + param.setName("greeting"); + param.setType("string"); + param.setValue("Hello, {{name}}!"); + + Map inputParameters = Map.of("name", "Alice"); + JsonNode node = Resolver.buildMockValue(param, mapper, inputParameters); + assertEquals("Hello, Alice!", node.asText()); + } + private static Capability capabilityFromYaml(String yaml) throws Exception { ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); @@ -432,11 +442,11 @@ private static String minimalCapabilityYaml() { """; } - private static OutputParameterSpec stringOutput(String name, String constant) { + private static OutputParameterSpec stringOutput(String name, String value) { OutputParameterSpec spec = new OutputParameterSpec(); spec.setName(name); spec.setType("string"); - spec.setConstant(constant); + spec.setValue(value); return spec; } } \ No newline at end of file diff --git a/src/test/java/io/naftiko/spec/OutputParameterDeserializationTest.java b/src/test/java/io/naftiko/spec/OutputParameterDeserializationTest.java index 0d70c06f..93f39171 100644 --- a/src/test/java/io/naftiko/spec/OutputParameterDeserializationTest.java +++ b/src/test/java/io/naftiko/spec/OutputParameterDeserializationTest.java @@ -117,4 +117,95 @@ public void testNamedOutputSerializesAsValue() throws Exception { "Named output should not serialize using mapping field"); } + /** + * Regression test for PR #287 review comment: when an object output parameter + * has nested properties with {@code value} (mock mode), the deserializer must + * call {@code setValue()} — not {@code setMapping()} — on the child properties. + * + * The bug occurs because the property key ("status") is injected as "name" into + * the child node before the recursive dispatch, causing the + * {@code (value && name)} branch to misroute the value into mapping. + */ + @Test + public void deserializeShouldSetValueNotMappingOnNestedPropertyWithValue() throws Exception { + String yamlSnippet = """ + type: object + properties: + status: + type: string + value: "ok" + greeting: + type: string + value: "Hello, {{name}}!" + """; + + ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); + OutputParameterSpec spec = mapper.readValue(yamlSnippet, OutputParameterSpec.class); + + assertEquals("object", spec.getType()); + + OutputParameterSpec statusProp = spec.getProperties().stream() + .filter(p -> "status".equals(p.getName())).findFirst().orElse(null); + assertNotNull(statusProp, "status property should exist"); + assertEquals("string", statusProp.getType()); + assertEquals("ok", statusProp.getValue(), + "Static value should be set via setValue(), not routed to mapping"); + assertNull(statusProp.getMapping(), + "Mapping should be null for a static-value property"); + + OutputParameterSpec greetingProp = spec.getProperties().stream() + .filter(p -> "greeting".equals(p.getName())).findFirst().orElse(null); + assertNotNull(greetingProp, "greeting property should exist"); + assertEquals("Hello, {{name}}!", greetingProp.getValue(), + "Mustache template value should be set via setValue()"); + assertNull(greetingProp.getMapping(), + "Mapping should be null for a Mustache-template-value property"); + } + + /** + * Ensures that nested properties with {@code mapping} still deserialize + * correctly (setMapping), and are not affected by the value-fix. + */ + @Test + public void deserializeShouldPreserveMappingOnNestedPropertyWithMapping() throws Exception { + String yamlSnippet = """ + type: object + properties: + status: + type: string + mapping: "$.status" + """; + + ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); + OutputParameterSpec spec = mapper.readValue(yamlSnippet, OutputParameterSpec.class); + + OutputParameterSpec statusProp = spec.getProperties().stream() + .filter(p -> "status".equals(p.getName())).findFirst().orElse(null); + assertNotNull(statusProp, "status property should exist"); + assertEquals("$.status", statusProp.getMapping(), + "Mapping should be preserved for properties using mapping"); + assertNull(statusProp.getValue(), + "Value should be null for a mapped property"); + } + + /** + * Regression test: consumed output parameters (with explicit name + value in YAML) + * must still route value to mapping for backward compatibility. + */ + @Test + public void deserializeShouldRoutValueToMappingForConsumedOutputParameter() throws Exception { + String yamlSnippet = """ + name: userid + type: string + value: "$.id" + """; + + ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); + OutputParameterSpec spec = mapper.readValue(yamlSnippet, OutputParameterSpec.class); + + assertEquals("userid", spec.getName()); + assertEquals("$.id", spec.getMapping(), + "Consumed output parameter value should be routed to mapping"); + } + }